From 1c16df4db00fd41484c1d50d7bcf4cdfa15bca2e Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Sat, 7 Mar 2026 16:51:53 +0800 Subject: [PATCH 01/16] Phase 1: mobile responsive foundation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useMediaQuery hook extracted from CalendarPage inline pattern - h-screen → h-dvh for mobile address bar viewport fix - px-6 → px-4 md:px-6 on all page containers/toolbars (14 files) - Input/Select text-base on mobile to prevent iOS auto-zoom - Sheet full-width on mobile, max-w-[540px] on sm+ - Button icon size touch-friendly (44px mobile, 40px desktop) - Tailwind hoverOnlyWhenSupported: true (fixes 157 hover interactions) - PWA meta tags (apple-mobile-web-app-capable, theme-color) Co-Authored-By: Claude Opus 4.6 --- frontend/index.html | 4 ++++ frontend/src/App.tsx | 4 ++-- .../src/components/admin/AdminDashboardPage.tsx | 2 +- frontend/src/components/admin/AdminPortal.tsx | 2 +- frontend/src/components/admin/ConfigPage.tsx | 2 +- frontend/src/components/admin/IAMPage.tsx | 2 +- frontend/src/components/calendar/CalendarPage.tsx | 11 +++-------- .../src/components/dashboard/DashboardPage.tsx | 8 ++++---- frontend/src/components/layout/AppLayout.tsx | 2 +- .../src/components/locations/LocationsPage.tsx | 4 ++-- .../components/notifications/NotificationsPage.tsx | 4 ++-- frontend/src/components/people/PeoplePage.tsx | 8 ++++---- frontend/src/components/projects/ProjectDetail.tsx | 12 ++++++------ frontend/src/components/projects/ProjectsPage.tsx | 4 ++-- .../src/components/reminders/RemindersPage.tsx | 4 ++-- frontend/src/components/settings/SettingsPage.tsx | 2 +- frontend/src/components/todos/TodosPage.tsx | 4 ++-- frontend/src/components/ui/button.tsx | 2 +- frontend/src/components/ui/input.tsx | 2 +- frontend/src/components/ui/select.tsx | 2 +- frontend/src/components/ui/sheet.tsx | 2 +- frontend/src/hooks/useMediaQuery.ts | 14 ++++++++++++++ frontend/tailwind.config.ts | 1 + 23 files changed, 58 insertions(+), 44 deletions(-) create mode 100644 frontend/src/hooks/useMediaQuery.ts diff --git a/frontend/index.html b/frontend/index.html index 1abf41b..2da949e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,6 +3,10 @@ + + + + UMBRA diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 66de45d..98d7a3e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -21,7 +21,7 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) { if (isLoading) { return ( -
+
Loading...
); @@ -39,7 +39,7 @@ function AdminRoute({ children }: { children: React.ReactNode }) { if (isLoading) { return ( -
+
Loading...
); diff --git a/frontend/src/components/admin/AdminDashboardPage.tsx b/frontend/src/components/admin/AdminDashboardPage.tsx index 0b5cd04..fac3982 100644 --- a/frontend/src/components/admin/AdminDashboardPage.tsx +++ b/frontend/src/components/admin/AdminDashboardPage.tsx @@ -23,7 +23,7 @@ export default function AdminDashboardPage() { dashboard ? dashboard.total_users - dashboard.active_users : null; return ( -
+
{/* Stats grid */}
{isLoading ? ( diff --git a/frontend/src/components/admin/AdminPortal.tsx b/frontend/src/components/admin/AdminPortal.tsx index b519f6f..3776dd7 100644 --- a/frontend/src/components/admin/AdminPortal.tsx +++ b/frontend/src/components/admin/AdminPortal.tsx @@ -18,7 +18,7 @@ export default function AdminPortal() {
{/* Portal header with tab navigation */}
-
+
diff --git a/frontend/src/components/admin/ConfigPage.tsx b/frontend/src/components/admin/ConfigPage.tsx index 2b1131a..5821cbe 100644 --- a/frontend/src/components/admin/ConfigPage.tsx +++ b/frontend/src/components/admin/ConfigPage.tsx @@ -54,7 +54,7 @@ export default function ConfigPage() { const totalPages = data ? Math.ceil(data.total / PER_PAGE) : 1; return ( -
+
diff --git a/frontend/src/components/admin/IAMPage.tsx b/frontend/src/components/admin/IAMPage.tsx index 9eb3166..6f3d4a0 100644 --- a/frontend/src/components/admin/IAMPage.tsx +++ b/frontend/src/components/admin/IAMPage.tsx @@ -95,7 +95,7 @@ export default function IAMPage() { : null; return ( -
+
{/* Stats row */}
window.matchMedia('(min-width: 1024px)').matches); - useEffect(() => { - const mql = window.matchMedia('(min-width: 1024px)'); - const handler = (e: MediaQueryListEvent) => setIsDesktop(e.matches); - mql.addEventListener('change', handler); - return () => mql.removeEventListener('change', handler); - }, []); + const isDesktop = useMediaQuery('(min-width: 1024px)'); // Continuously resize calendar during panel open/close CSS transition useEffect(() => { @@ -483,7 +478,7 @@ export default function CalendarPage() {
{/* Custom toolbar */} -
+

Loading...

-
+
@@ -375,7 +375,7 @@ export default function ProjectDetail() { return (
{/* Header */} -
+
@@ -417,7 +417,7 @@ export default function ProjectDetail() { {/* Content area */}
{/* Summary section - scrolls with left panel on small, fixed on large */} -
+
{/* Description */} {project.description && (

{project.description}

@@ -490,7 +490,7 @@ export default function ProjectDetail() {
{/* Task list header + view controls */} -
+

Tasks

{/* View toggle */} @@ -544,7 +544,7 @@ export default function ProjectDetail() {
{/* Left panel: task list or kanban */}
-
+
{topLevelTasks.length === 0 ? ( {/* Header */} -
+

Projects

@@ -111,7 +111,7 @@ export default function ProjectsPage() {
-
+
{/* Summary stats */} {!isLoading && projects.length > 0 && (
diff --git a/frontend/src/components/reminders/RemindersPage.tsx b/frontend/src/components/reminders/RemindersPage.tsx index 7fdb1b6..c88f391 100644 --- a/frontend/src/components/reminders/RemindersPage.tsx +++ b/frontend/src/components/reminders/RemindersPage.tsx @@ -99,7 +99,7 @@ export default function RemindersPage() { return (
{/* Header */} -
+

Reminders

@@ -148,7 +148,7 @@ export default function RemindersPage() { panelOpen ? 'w-full lg:w-[55%]' : 'w-full' }`} > -
+
{/* Summary stats */} {!isLoading && reminders.length > 0 && (
diff --git a/frontend/src/components/settings/SettingsPage.tsx b/frontend/src/components/settings/SettingsPage.tsx index a6fd474..46e47eb 100644 --- a/frontend/src/components/settings/SettingsPage.tsx +++ b/frontend/src/components/settings/SettingsPage.tsx @@ -344,7 +344,7 @@ export default function SettingsPage() { return (
{/* Page header — matches Stage 4-5 pages */} -
+
diff --git a/frontend/src/components/todos/TodosPage.tsx b/frontend/src/components/todos/TodosPage.tsx index 40d1367..1656c19 100644 --- a/frontend/src/components/todos/TodosPage.tsx +++ b/frontend/src/components/todos/TodosPage.tsx @@ -128,7 +128,7 @@ export default function TodosPage() { return (
{/* Header */} -
+

Todos

{/* Priority filter */} @@ -183,7 +183,7 @@ export default function TodosPage() { panelOpen ? 'w-full lg:w-[55%]' : 'w-full' }`} > -
+
{/* Summary stats */} {!isLoading && todos.length > 0 && (
diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index 643041c..0a9968a 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -18,7 +18,7 @@ const buttonVariants = cva( default: 'h-10 px-4 py-2', sm: 'h-9 rounded-md px-3', lg: 'h-11 rounded-md px-8', - icon: 'h-10 w-10', + icon: 'h-11 w-11 md:h-10 md:w-10', }, }, defaultVariants: { diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx index 04db187..3e70a9d 100644 --- a/frontend/src/components/ui/input.tsx +++ b/frontend/src/components/ui/input.tsx @@ -9,7 +9,7 @@ const Input = React.forwardRef( (
changeView(e.target.value as CalendarView)} + className="h-8 text-sm w-auto md:hidden" + > + {(Object.entries(viewLabels) as [CalendarView, string][]).map(([view, label]) => ( + + ))} + + +
{(Object.entries(viewLabels) as [CalendarView, string][]).map(([view, label]) => (
diff --git a/frontend/src/components/locations/LocationsPage.tsx b/frontend/src/components/locations/LocationsPage.tsx index f40de57..3bc6206 100644 --- a/frontend/src/components/locations/LocationsPage.tsx +++ b/frontend/src/components/locations/LocationsPage.tsx @@ -285,10 +285,10 @@ export default function LocationsPage() { return (
{/* Header */} -
+

Locations

-
+
diff --git a/frontend/src/components/people/PeoplePage.tsx b/frontend/src/components/people/PeoplePage.tsx index 595eb6c..133240e 100644 --- a/frontend/src/components/people/PeoplePage.tsx +++ b/frontend/src/components/people/PeoplePage.tsx @@ -555,9 +555,9 @@ export default function PeoplePage() { return (
{/* Header */} -
+

People

-
+
- - Add Person + Add Person @@ -396,8 +396,7 @@ export default function ProjectDetail() {
@@ -490,7 +488,7 @@ export default function ProjectDetail() {
{/* Task list header + view controls */} -
+

Tasks

{/* View toggle */} diff --git a/frontend/src/components/projects/ProjectsPage.tsx b/frontend/src/components/projects/ProjectsPage.tsx index f2cf87b..b172aa6 100644 --- a/frontend/src/components/projects/ProjectsPage.tsx +++ b/frontend/src/components/projects/ProjectsPage.tsx @@ -5,6 +5,7 @@ import { useQuery } from '@tanstack/react-query'; import api from '@/lib/api'; import type { Project } from '@/types'; import { Button } from '@/components/ui/button'; +import { Select } from '@/components/ui/select'; import { Input } from '@/components/ui/input'; import { Card, CardContent } from '@/components/ui/card'; import { GridSkeleton } from '@/components/ui/skeleton'; @@ -70,10 +71,19 @@ export default function ProjectsPage() { return (
{/* Header */} -
+

Projects

-
+ +
{statusFilters.map((sf) => (
diff --git a/frontend/src/components/reminders/RemindersPage.tsx b/frontend/src/components/reminders/RemindersPage.tsx index c88f391..55f892a 100644 --- a/frontend/src/components/reminders/RemindersPage.tsx +++ b/frontend/src/components/reminders/RemindersPage.tsx @@ -6,6 +6,7 @@ import { isPast, isToday, parseISO } from 'date-fns'; import api from '@/lib/api'; import type { Reminder } from '@/types'; import { Button } from '@/components/ui/button'; +import { Select } from '@/components/ui/select'; import { Input } from '@/components/ui/input'; import { Card, CardContent } from '@/components/ui/card'; import { ListSkeleton } from '@/components/ui/skeleton'; @@ -99,10 +100,19 @@ export default function RemindersPage() { return (
{/* Header */} -
+

Reminders

-
+ +
{statusFilters.map((sf) => (
diff --git a/frontend/src/components/todos/TodosPage.tsx b/frontend/src/components/todos/TodosPage.tsx index 1656c19..57356ac 100644 --- a/frontend/src/components/todos/TodosPage.tsx +++ b/frontend/src/components/todos/TodosPage.tsx @@ -6,6 +6,7 @@ import api from '@/lib/api'; import type { Todo } from '@/types'; import { isTodoOverdue } from '@/lib/utils'; import { Button } from '@/components/ui/button'; +import { Select } from '@/components/ui/select'; import { Card, CardContent } from '@/components/ui/card'; import { ListSkeleton } from '@/components/ui/skeleton'; import { CategoryFilterBar } from '@/components/shared'; @@ -128,11 +129,20 @@ export default function TodosPage() { return (
{/* Header */} -
+

Todos

{/* Priority filter */} -
+ +
{priorityFilters.map((pf) => (
From 09c35752c6267828bc59c65d8ba57a32c76094c6 Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Sat, 7 Mar 2026 16:59:58 +0800 Subject: [PATCH 03/16] Add mobile card view to EntityTable with renderers for People and Locations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EntityTable: add useMediaQuery hook, mobileCardRender prop, and mobile card path that replaces the table on screens <768px when a renderer is provided - PeoplePage: add mobileCardRender showing name, category, email, phone - LocationsPage: add mobileCardRender showing name, category, address Note: TodosPage and RemindersPage use custom list components (TodoList, ReminderList), not EntityTable directly — no changes needed there. Co-Authored-By: Claude Opus 4.6 --- .../components/locations/LocationsPage.tsx | 11 +++++ frontend/src/components/people/PeoplePage.tsx | 12 ++++++ .../src/components/shared/EntityTable.tsx | 43 +++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/frontend/src/components/locations/LocationsPage.tsx b/frontend/src/components/locations/LocationsPage.tsx index 3bc6206..36a7238 100644 --- a/frontend/src/components/locations/LocationsPage.tsx +++ b/frontend/src/components/locations/LocationsPage.tsx @@ -356,6 +356,17 @@ export default function LocationsPage() { sortDir={sortDir} onSort={handleSort} visibilityMode={visibilityMode} + mobileCardRender={(location) => ( +
+
+ {location.name} + {location.category && {location.category}} +
+ {location.address && ( +

{location.address}

+ )} +
+ )} /> )}
diff --git a/frontend/src/components/people/PeoplePage.tsx b/frontend/src/components/people/PeoplePage.tsx index 133240e..d51cc3c 100644 --- a/frontend/src/components/people/PeoplePage.tsx +++ b/frontend/src/components/people/PeoplePage.tsx @@ -744,6 +744,18 @@ export default function PeoplePage() { sortDir={sortDir} onSort={handleSort} visibilityMode={visibilityMode} + mobileCardRender={(person) => ( +
+
+ {person.name} + {person.category && {person.category}} +
+
+ {person.email && {person.email}} + {person.phone && {person.phone}} +
+
+ )} /> )}
diff --git a/frontend/src/components/shared/EntityTable.tsx b/frontend/src/components/shared/EntityTable.tsx index de4a244..e6ccf27 100644 --- a/frontend/src/components/shared/EntityTable.tsx +++ b/frontend/src/components/shared/EntityTable.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'; import type { VisibilityMode } from '@/hooks/useTableVisibility'; +import { useMediaQuery } from '@/hooks/useMediaQuery'; export interface ColumnDef { key: string; @@ -28,6 +29,7 @@ interface EntityTableProps { onSort: (key: string) => void; visibilityMode: VisibilityMode; loading?: boolean; + mobileCardRender?: (item: T) => React.ReactNode; } const LEVEL_ORDER: VisibilityMode[] = ['essential', 'filtered', 'all']; @@ -127,10 +129,51 @@ export function EntityTable({ onSort, visibilityMode, loading = false, + mobileCardRender, }: EntityTableProps) { const visibleColumns = columns.filter((col) => isVisible(col.visibilityLevel, visibilityMode)); const colCount = visibleColumns.length; const showPinnedSection = showPinned && pinnedRows.length > 0; + const isMobile = useMediaQuery('(max-width: 767px)'); + + if (isMobile && mobileCardRender) { + return ( +
+ {loading ? ( + Array.from({ length: 6 }).map((_, i) => ( +
+ )) + ) : ( + <> + {showPinnedSection && ( + <> +

{pinnedLabel}

+ {pinnedRows.map((item) => ( +
onRowClick(item.id)} className="cursor-pointer"> + {mobileCardRender(item)} +
+ ))} + + )} + {groups.map((group) => ( + + {group.rows.length > 0 && ( + <> +

{group.label}

+ {group.rows.map((item) => ( +
onRowClick(item.id)} className="cursor-pointer"> + {mobileCardRender(item)} +
+ ))} + + )} +
+ ))} + + )} +
+ ); + } return (
From b05adf7f1296af0e2cfc1dc31bd22d5800c0e296 Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Sat, 7 Mar 2026 17:03:14 +0800 Subject: [PATCH 04/16] Phase 3: complex component mobile adaptations 3a. CalendarSidebar mobile collapse: - Desktop sidebar + resize handle hidden below lg breakpoint - Mobile Sheet overlay with PanelLeft toggle in toolbar - Template selection closes mobile sidebar automatically 3b. KanbanBoard touch support: - TouchSensor added alongside PointerSensor (200ms delay) - Column min-width reduced on mobile (160px vs 200px) - iOS smooth scroll enabled on horizontal container 3c. EntityTable mobile card view: - mobileCardRender optional prop renders cards instead of table on mobile - PeoplePage: card with name, category, email, phone - LocationsPage: card with name, category, address - TodosPage/RemindersPage use custom list components, not EntityTable 3d. DatePicker mobile bottom sheet: - Renders as full-width bottom sheet on mobile (< 768px) - Safe area inset padding for iOS home indicator - Desktop positioned dropdown unchanged Co-Authored-By: Claude Opus 4.6 --- .../src/components/calendar/CalendarPage.tsx | 28 +++++++++++++++---- .../src/components/projects/KanbanBoard.tsx | 4 +-- frontend/src/components/ui/date-picker.tsx | 2 ++ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/calendar/CalendarPage.tsx b/frontend/src/components/calendar/CalendarPage.tsx index c1cef0f..51262e8 100644 --- a/frontend/src/components/calendar/CalendarPage.tsx +++ b/frontend/src/components/calendar/CalendarPage.tsx @@ -8,7 +8,7 @@ import dayGridPlugin from '@fullcalendar/daygrid'; import timeGridPlugin from '@fullcalendar/timegrid'; import interactionPlugin from '@fullcalendar/interaction'; import type { EventClickArg, DateSelectArg, EventDropArg, DatesSetArg } from '@fullcalendar/core'; -import { ChevronLeft, ChevronRight, Plus, Search } from 'lucide-react'; +import { ChevronLeft, ChevronRight, PanelLeft, Plus, Search } from 'lucide-react'; import api, { getErrorMessage } from '@/lib/api'; import axios from 'axios'; import type { CalendarEvent, EventTemplate, Location as LocationType, CalendarPermission } from '@/types'; @@ -17,6 +17,7 @@ import { useSettings } from '@/hooks/useSettings'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Select } from '@/components/ui/select'; +import { Sheet, SheetContent } from '@/components/ui/sheet'; import CalendarSidebar from './CalendarSidebar'; import EventDetailPanel from './EventDetailPanel'; import type { CreateDefaults } from './EventDetailPanel'; @@ -164,6 +165,8 @@ export default function CalendarPage() { // Track desktop breakpoint to prevent dual EventDetailPanel mount const isDesktop = useMediaQuery('(min-width: 1024px)'); + const isMobile = useMediaQuery('(max-width: 1023px)'); + const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); // Continuously resize calendar during panel open/close CSS transition useEffect(() => { @@ -471,15 +474,28 @@ export default function CalendarPage() { return (
- -
+
+ +
+
+ + {isMobile && ( + + + { setMobileSidebarOpen(false); handleUseTemplate(tmpl); }} onSharedVisibilityChange={setVisibleSharedIds} width={288} /> + + + )}
{/* Custom toolbar */}
+
@@ -184,7 +184,7 @@ const CalendarSidebar = forwardRef(functio setEditingTemplate(tmpl); setShowTemplateForm(true); }} - className="opacity-0 group-hover:opacity-100 transition-opacity duration-150 text-muted-foreground hover:text-foreground" + className="opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity duration-150 text-muted-foreground hover:text-foreground" > @@ -194,7 +194,7 @@ const CalendarSidebar = forwardRef(functio if (!window.confirm(`Delete template "${tmpl.name}"?`)) return; deleteTemplateMutation.mutate(tmpl.id); }} - className="opacity-0 group-hover:opacity-100 transition-opacity duration-150 text-muted-foreground hover:text-destructive" + className="opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity duration-150 text-muted-foreground hover:text-destructive" > diff --git a/frontend/src/components/calendar/SharedCalendarSection.tsx b/frontend/src/components/calendar/SharedCalendarSection.tsx index d42308c..b0ecee6 100644 --- a/frontend/src/components/calendar/SharedCalendarSection.tsx +++ b/frontend/src/components/calendar/SharedCalendarSection.tsx @@ -73,7 +73,7 @@ export default function SharedCalendarSection({ @@ -104,7 +104,7 @@ export default function SharedCalendarSection({ {m.calendar_name} diff --git a/frontend/src/components/notifications/NotificationsPage.tsx b/frontend/src/components/notifications/NotificationsPage.tsx index f9d6eda..7ad45f9 100644 --- a/frontend/src/components/notifications/NotificationsPage.tsx +++ b/frontend/src/components/notifications/NotificationsPage.tsx @@ -316,7 +316,7 @@ export default function NotificationsPage() { {formatDistanceToNow(new Date(notification.created_at), { addSuffix: true })} -
+
{!notification.is_read && ( From 4d5052d7315a81f42f3ac5e996b0d6ae8825bada Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Sat, 7 Mar 2026 17:16:47 +0800 Subject: [PATCH 06/16] Action QA findings: fix all critical/warning/suggestion items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical fixes: - C-01: DatePicker isMobile now actually used for bottom sheet positioning - C-02: Calendar title always visible (text-sm on mobile, text-lg on sm+) - C-03: Mobile card text-[10px] → text-xs (meets 12px minimum) Warning fixes: - W-01: useMediaQuery SSR-safe (typeof window guard) - W-02: KanbanBoard TouchSensor added (was lost during branch ops) - W-03: Removed duplicate isMobile query, derived from !isDesktop - W-04: Search restored on mobile for Calendar/Reminders/Projects (w-32 sm:w-52) - W-05: SheetClose added to CalendarSidebar mobile Sheet - W-06: Button icon uses min-h/min-w for touch targets instead of h-11 Suggestion fixes: - S-01: Removed deprecated WebkitOverflowScrolling from KanbanBoard - S-02: Added role/tabIndex/onKeyDown to EntityTable mobile card wrappers - S-03: Added overflow-y-auto to mobile event detail panel Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/calendar/CalendarPage.tsx | 14 +++++++------- .../src/components/locations/LocationsPage.tsx | 2 +- frontend/src/components/people/PeoplePage.tsx | 2 +- frontend/src/components/projects/KanbanBoard.tsx | 3 ++- frontend/src/components/projects/ProjectsPage.tsx | 4 ++-- .../src/components/reminders/RemindersPage.tsx | 4 ++-- frontend/src/components/shared/EntityTable.tsx | 4 ++-- frontend/src/components/ui/button.tsx | 2 +- frontend/src/components/ui/date-picker.tsx | 4 ++-- frontend/src/hooks/useMediaQuery.ts | 4 +++- 10 files changed, 23 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/calendar/CalendarPage.tsx b/frontend/src/components/calendar/CalendarPage.tsx index b4ee310..e547f56 100644 --- a/frontend/src/components/calendar/CalendarPage.tsx +++ b/frontend/src/components/calendar/CalendarPage.tsx @@ -17,7 +17,7 @@ import { useSettings } from '@/hooks/useSettings'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Select } from '@/components/ui/select'; -import { Sheet, SheetContent } from '@/components/ui/sheet'; +import { Sheet, SheetContent, SheetClose } from '@/components/ui/sheet'; import CalendarSidebar from './CalendarSidebar'; import EventDetailPanel from './EventDetailPanel'; import type { CreateDefaults } from './EventDetailPanel'; @@ -165,7 +165,6 @@ export default function CalendarPage() { // Track desktop breakpoint to prevent dual EventDetailPanel mount const isDesktop = useMediaQuery('(min-width: 1024px)'); - const isMobile = useMediaQuery('(max-width: 1023px)'); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); // Continuously resize calendar during panel open/close CSS transition @@ -484,9 +483,10 @@ export default function CalendarPage() { />
- {isMobile && ( + {!isDesktop && ( + setMobileSidebarOpen(false)} /> { setMobileSidebarOpen(false); handleUseTemplate(tmpl); }} onSharedVisibilityChange={setVisibleSharedIds} width={288} /> @@ -540,12 +540,12 @@ export default function CalendarPage() { ))}
-

{calendarTitle}

+

{calendarTitle}

{/* Event search */} -
+
setEventSearch(e.target.value)} onFocus={() => setSearchFocused(true)} onBlur={() => setTimeout(() => setSearchFocused(false), 200)} - className="w-52 h-8 pl-8 text-sm ring-inset" + className="w-32 sm:w-52 h-8 pl-8 text-sm ring-inset" /> {searchFocused && searchResults.length > 0 && (
@@ -644,7 +644,7 @@ export default function CalendarPage() { onClick={handlePanelClose} >
e.stopPropagation()} >
{location.name} - {location.category && {location.category}} + {location.category && {location.category}}
{location.address && (

{location.address}

diff --git a/frontend/src/components/people/PeoplePage.tsx b/frontend/src/components/people/PeoplePage.tsx index d51cc3c..5fa89c8 100644 --- a/frontend/src/components/people/PeoplePage.tsx +++ b/frontend/src/components/people/PeoplePage.tsx @@ -748,7 +748,7 @@ export default function PeoplePage() {
{person.name} - {person.category && {person.category}} + {person.category && {person.category}}
{person.email && {person.email}} diff --git a/frontend/src/components/projects/KanbanBoard.tsx b/frontend/src/components/projects/KanbanBoard.tsx index 6658dbc..91b258c 100644 --- a/frontend/src/components/projects/KanbanBoard.tsx +++ b/frontend/src/components/projects/KanbanBoard.tsx @@ -2,6 +2,7 @@ import { DndContext, closestCorners, PointerSensor, + TouchSensor, useSensor, useSensors, type DragEndEvent, @@ -200,7 +201,7 @@ export default function KanbanBoard({ collisionDetection={closestCorners} onDragEnd={handleDragEnd} > -
+
{tasksByStatus.map(({ column, tasks: colTasks }) => ( -
+
setSearch(e.target.value)} - className="w-52 h-8 pl-8 text-sm" + className="w-32 sm:w-52 h-8 pl-8 text-sm" />
diff --git a/frontend/src/components/reminders/RemindersPage.tsx b/frontend/src/components/reminders/RemindersPage.tsx index 55f892a..e00b61d 100644 --- a/frontend/src/components/reminders/RemindersPage.tsx +++ b/frontend/src/components/reminders/RemindersPage.tsx @@ -135,13 +135,13 @@ export default function RemindersPage() {
-
+
setSearch(e.target.value)} - className="w-52 h-8 pl-8 text-sm ring-inset" + className="w-32 sm:w-52 h-8 pl-8 text-sm ring-inset" />
diff --git a/frontend/src/components/shared/EntityTable.tsx b/frontend/src/components/shared/EntityTable.tsx index e6ccf27..529493d 100644 --- a/frontend/src/components/shared/EntityTable.tsx +++ b/frontend/src/components/shared/EntityTable.tsx @@ -149,7 +149,7 @@ export function EntityTable({ <>

{pinnedLabel}

{pinnedRows.map((item) => ( -
onRowClick(item.id)} className="cursor-pointer"> +
onRowClick(item.id)} className="cursor-pointer" role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onRowClick(item.id); } }}> {mobileCardRender(item)}
))} @@ -161,7 +161,7 @@ export function EntityTable({ <>

{group.label}

{group.rows.map((item) => ( -
onRowClick(item.id)} className="cursor-pointer"> +
onRowClick(item.id)} className="cursor-pointer" role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onRowClick(item.id); } }}> {mobileCardRender(item)}
))} diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index 0a9968a..6d826aa 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -18,7 +18,7 @@ const buttonVariants = cva( default: 'h-10 px-4 py-2', sm: 'h-9 rounded-md px-3', lg: 'h-11 rounded-md px-8', - icon: 'h-11 w-11 md:h-10 md:w-10', + icon: 'h-10 w-10 min-h-[44px] min-w-[44px] md:min-h-0 md:min-w-0', }, }, defaultVariants: { diff --git a/frontend/src/components/ui/date-picker.tsx b/frontend/src/components/ui/date-picker.tsx index f53ca6c..4f9f7bb 100644 --- a/frontend/src/components/ui/date-picker.tsx +++ b/frontend/src/components/ui/date-picker.tsx @@ -326,8 +326,8 @@ const DatePicker = React.forwardRef(
e.stopPropagation()} - style={{ position: 'fixed', top: pos.top, left: pos.left, zIndex: 60 }} - className="w-[280px] rounded-lg border border-input bg-card shadow-lg animate-fade-in" + style={isMobile ? { position: 'fixed', bottom: 0, left: 0, right: 0, zIndex: 60 } : { position: 'fixed', top: pos.top, left: pos.left, zIndex: 60 }} + className={isMobile ? 'w-full rounded-t-lg border border-input bg-card shadow-lg animate-fade-in pb-[env(safe-area-inset-bottom)]' : 'w-[280px] rounded-lg border border-input bg-card shadow-lg animate-fade-in'} > {/* Month/Year nav */}
diff --git a/frontend/src/hooks/useMediaQuery.ts b/frontend/src/hooks/useMediaQuery.ts index b3c4279..6fc4f90 100644 --- a/frontend/src/hooks/useMediaQuery.ts +++ b/frontend/src/hooks/useMediaQuery.ts @@ -1,7 +1,9 @@ import { useState, useEffect } from 'react'; export function useMediaQuery(query: string): boolean { - const [matches, setMatches] = useState(() => window.matchMedia(query).matches); + const [matches, setMatches] = useState(() => + typeof window !== 'undefined' ? window.matchMedia(query).matches : false + ); useEffect(() => { const mql = window.matchMedia(query); From 0b84352b094fc524c6f49136e819d68bd3a161ae Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Sat, 7 Mar 2026 17:42:27 +0800 Subject: [PATCH 07/16] Fix KanbanBoard: actually wire TouchSensor into useSensors The import was added but the sensors config replacement failed silently due to line ending mismatch. TouchSensor now properly registered with 200ms delay / 5px tolerance alongside PointerSensor. Co-Authored-By: Claude Opus 4.6 --- .../src/components/projects/KanbanBoard.tsx | 435 +++++++++--------- 1 file changed, 218 insertions(+), 217 deletions(-) diff --git a/frontend/src/components/projects/KanbanBoard.tsx b/frontend/src/components/projects/KanbanBoard.tsx index 91b258c..a27a3df 100644 --- a/frontend/src/components/projects/KanbanBoard.tsx +++ b/frontend/src/components/projects/KanbanBoard.tsx @@ -1,218 +1,219 @@ -import { - DndContext, - closestCorners, +import { + DndContext, + closestCorners, PointerSensor, - TouchSensor, - useSensor, - useSensors, - type DragEndEvent, - useDroppable, - useDraggable, -} from '@dnd-kit/core'; -import { format, parseISO } from 'date-fns'; -import type { ProjectTask } from '@/types'; -import { Badge } from '@/components/ui/badge'; - -const COLUMNS: { id: string; label: string; color: string }[] = [ - { id: 'pending', label: 'Pending', color: 'text-gray-400' }, - { id: 'in_progress', label: 'In Progress', color: 'text-blue-400' }, - { id: 'blocked', label: 'Blocked', color: 'text-red-400' }, - { id: 'on_hold', label: 'On Hold', color: 'text-orange-400' }, - { id: 'review', label: 'Review', color: 'text-yellow-400' }, - { id: 'completed', label: 'Completed', color: 'text-green-400' }, -]; - -const priorityColors: Record = { - none: 'bg-gray-500/20 text-gray-400', - low: 'bg-green-500/20 text-green-400', - medium: 'bg-yellow-500/20 text-yellow-400', - high: 'bg-red-500/20 text-red-400', -}; - -interface KanbanBoardProps { - tasks: ProjectTask[]; - selectedTaskId: number | null; - kanbanParentTask?: ProjectTask | null; - onSelectTask: (taskId: number) => void; - onStatusChange: (taskId: number, status: string) => void; - onBackToAllTasks?: () => void; -} - -function KanbanColumn({ - column, - tasks, - selectedTaskId, - onSelectTask, -}: { - column: (typeof COLUMNS)[0]; - tasks: ProjectTask[]; - selectedTaskId: number | null; - onSelectTask: (taskId: number) => void; -}) { - const { setNodeRef, isOver } = useDroppable({ id: column.id }); - - return ( -
- {/* Column header */} -
-
- - {column.label} - - - {tasks.length} - -
-
- - {/* Cards */} -
- {tasks.map((task) => ( - onSelectTask(task.id)} - /> - ))} -
-
- ); -} - -function KanbanCard({ - task, - isSelected, - onSelect, -}: { - task: ProjectTask; - isSelected: boolean; - onSelect: () => void; -}) { - const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ - id: task.id, - data: { task }, - }); - - const style = transform - ? { - transform: `translate(${transform.x}px, ${transform.y}px)`, - opacity: isDragging ? 0.5 : 1, - } - : undefined; - - const completedSubtasks = task.subtasks?.filter((s) => s.status === 'completed').length ?? 0; - const totalSubtasks = task.subtasks?.length ?? 0; - - return ( -
-

{task.title}

-
- - {task.priority} - - {task.due_date && ( - - {format(parseISO(task.due_date), 'MMM d')} - - )} - {totalSubtasks > 0 && ( - - {completedSubtasks}/{totalSubtasks} - - )} -
-
- ); -} - -export default function KanbanBoard({ - tasks, - selectedTaskId, - kanbanParentTask, - onSelectTask, - onStatusChange, - onBackToAllTasks, -}: KanbanBoardProps) { - const sensors = useSensors( - useSensor(PointerSensor, { activationConstraint: { distance: 5 } }) - ); - - // Subtask view is driven by kanbanParentTask (decoupled from selected task) - const isSubtaskView = kanbanParentTask != null && (kanbanParentTask.subtasks?.length ?? 0) > 0; - const activeTasks: ProjectTask[] = isSubtaskView ? (kanbanParentTask.subtasks ?? []) : tasks; - - const handleDragEnd = (event: DragEndEvent) => { - const { active, over } = event; - if (!over) return; - - const taskId = active.id as number; - const newStatus = over.id as string; - - const task = activeTasks.find((t) => t.id === taskId); - if (task && task.status !== newStatus && COLUMNS.some((c) => c.id === newStatus)) { - onStatusChange(taskId, newStatus); - } - }; - - const tasksByStatus = COLUMNS.map((col) => ({ - column: col, - tasks: activeTasks.filter((t) => t.status === col.id), - })); - - return ( -
- {/* Subtask view header */} - {isSubtaskView && kanbanParentTask && ( -
- - / - - Subtasks of: {kanbanParentTask.title} - -
- )} - - -
- {tasksByStatus.map(({ column, tasks: colTasks }) => ( - - ))} -
-
-
- ); -} + TouchSensor, + useSensor, + useSensors, + type DragEndEvent, + useDroppable, + useDraggable, +} from '@dnd-kit/core'; +import { format, parseISO } from 'date-fns'; +import type { ProjectTask } from '@/types'; +import { Badge } from '@/components/ui/badge'; + +const COLUMNS: { id: string; label: string; color: string }[] = [ + { id: 'pending', label: 'Pending', color: 'text-gray-400' }, + { id: 'in_progress', label: 'In Progress', color: 'text-blue-400' }, + { id: 'blocked', label: 'Blocked', color: 'text-red-400' }, + { id: 'on_hold', label: 'On Hold', color: 'text-orange-400' }, + { id: 'review', label: 'Review', color: 'text-yellow-400' }, + { id: 'completed', label: 'Completed', color: 'text-green-400' }, +]; + +const priorityColors: Record = { + none: 'bg-gray-500/20 text-gray-400', + low: 'bg-green-500/20 text-green-400', + medium: 'bg-yellow-500/20 text-yellow-400', + high: 'bg-red-500/20 text-red-400', +}; + +interface KanbanBoardProps { + tasks: ProjectTask[]; + selectedTaskId: number | null; + kanbanParentTask?: ProjectTask | null; + onSelectTask: (taskId: number) => void; + onStatusChange: (taskId: number, status: string) => void; + onBackToAllTasks?: () => void; +} + +function KanbanColumn({ + column, + tasks, + selectedTaskId, + onSelectTask, +}: { + column: (typeof COLUMNS)[0]; + tasks: ProjectTask[]; + selectedTaskId: number | null; + onSelectTask: (taskId: number) => void; +}) { + const { setNodeRef, isOver } = useDroppable({ id: column.id }); + + return ( +
+ {/* Column header */} +
+
+ + {column.label} + + + {tasks.length} + +
+
+ + {/* Cards */} +
+ {tasks.map((task) => ( + onSelectTask(task.id)} + /> + ))} +
+
+ ); +} + +function KanbanCard({ + task, + isSelected, + onSelect, +}: { + task: ProjectTask; + isSelected: boolean; + onSelect: () => void; +}) { + const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ + id: task.id, + data: { task }, + }); + + const style = transform + ? { + transform: `translate(${transform.x}px, ${transform.y}px)`, + opacity: isDragging ? 0.5 : 1, + } + : undefined; + + const completedSubtasks = task.subtasks?.filter((s) => s.status === 'completed').length ?? 0; + const totalSubtasks = task.subtasks?.length ?? 0; + + return ( +
+

{task.title}

+
+ + {task.priority} + + {task.due_date && ( + + {format(parseISO(task.due_date), 'MMM d')} + + )} + {totalSubtasks > 0 && ( + + {completedSubtasks}/{totalSubtasks} + + )} +
+
+ ); +} + +export default function KanbanBoard({ + tasks, + selectedTaskId, + kanbanParentTask, + onSelectTask, + onStatusChange, + onBackToAllTasks, +}: KanbanBoardProps) { + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 5 } }) , + useSensor(TouchSensor, { activationConstraint: { delay: 200, tolerance: 5 } }) + ); + + // Subtask view is driven by kanbanParentTask (decoupled from selected task) + const isSubtaskView = kanbanParentTask != null && (kanbanParentTask.subtasks?.length ?? 0) > 0; + const activeTasks: ProjectTask[] = isSubtaskView ? (kanbanParentTask.subtasks ?? []) : tasks; + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (!over) return; + + const taskId = active.id as number; + const newStatus = over.id as string; + + const task = activeTasks.find((t) => t.id === taskId); + if (task && task.status !== newStatus && COLUMNS.some((c) => c.id === newStatus)) { + onStatusChange(taskId, newStatus); + } + }; + + const tasksByStatus = COLUMNS.map((col) => ({ + column: col, + tasks: activeTasks.filter((t) => t.status === col.id), + })); + + return ( +
+ {/* Subtask view header */} + {isSubtaskView && kanbanParentTask && ( +
+ + / + + Subtasks of: {kanbanParentTask.title} + +
+ )} + + +
+ {tasksByStatus.map(({ column, tasks: colTasks }) => ( + + ))} +
+
+
+ ); +} From ec8f5a9b4e93ecaa380a9bb0d300814fad18753e Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Sat, 7 Mar 2026 18:02:42 +0800 Subject: [PATCH 08/16] Fix mobile density issues from S24 Ultra testing - Page titles: text-xl on mobile, text-2xl on desktop (7 pages) - Stat cards: reduce padding/gap on mobile, hide icons below sm (3 pages) - TodoItem: two-line layout on mobile (title row + metadata row) - ReminderItem: same two-line treatment - FullCalendar: smaller event font/padding on mobile via CSS media query Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/admin/AdminPortal.tsx | 2 +- .../components/locations/LocationsPage.tsx | 2 +- frontend/src/components/people/PeoplePage.tsx | 2 +- .../src/components/projects/ProjectDetail.tsx | 4 +- .../src/components/projects/ProjectsPage.tsx | 28 +-- .../src/components/reminders/ReminderItem.tsx | 132 +++++++------ .../components/reminders/RemindersPage.tsx | 28 +-- frontend/src/components/todos/TodoItem.tsx | 178 +++++++++--------- frontend/src/components/todos/TodosPage.tsx | 28 +-- frontend/src/index.css | 35 ++++ 10 files changed, 244 insertions(+), 195 deletions(-) diff --git a/frontend/src/components/admin/AdminPortal.tsx b/frontend/src/components/admin/AdminPortal.tsx index 3776dd7..76c2d4e 100644 --- a/frontend/src/components/admin/AdminPortal.tsx +++ b/frontend/src/components/admin/AdminPortal.tsx @@ -23,7 +23,7 @@ export default function AdminPortal() {
-

Admin Portal

+

Admin Portal

{/* Horizontal tab navigation */} diff --git a/frontend/src/components/locations/LocationsPage.tsx b/frontend/src/components/locations/LocationsPage.tsx index 9a21eb6..00bee6a 100644 --- a/frontend/src/components/locations/LocationsPage.tsx +++ b/frontend/src/components/locations/LocationsPage.tsx @@ -286,7 +286,7 @@ export default function LocationsPage() {
{/* Header */}
-

Locations

+

Locations

{/* Header */}
-

People

+

People

navigate('/projects')}> -

Loading...

+

Loading...

@@ -379,7 +379,7 @@ export default function ProjectDetail() { -

+

{project.name}

diff --git a/frontend/src/components/projects/ProjectsPage.tsx b/frontend/src/components/projects/ProjectsPage.tsx index 1b6b37c..823d397 100644 --- a/frontend/src/components/projects/ProjectsPage.tsx +++ b/frontend/src/components/projects/ProjectsPage.tsx @@ -72,7 +72,7 @@ export default function ProjectsPage() {
{/* Header */}
-

Projects

+

Projects

{/* Summary stats */} {!isLoading && reminders.length > 0 && ( -
+
- -
+ +
-

+

Active

-

{activeCount}

+

{activeCount}

- -
+ +
-

+

Overdue

-

{overdueCount}

+

{overdueCount}

- -
+ +
-

+

Dismissed

-

{dismissedCount}

+

{dismissedCount}

diff --git a/frontend/src/components/todos/TodoItem.tsx b/frontend/src/components/todos/TodoItem.tsx index cea548b..2036d07 100644 --- a/frontend/src/components/todos/TodoItem.tsx +++ b/frontend/src/components/todos/TodoItem.tsx @@ -52,7 +52,6 @@ export default function TodoItem({ todo, onEdit }: TodoItemProps) { await api.delete(`/todos/${todo.id}`); }, onMutate: async () => { - // Optimistic removal await queryClient.cancelQueries({ queryKey: ['todos'] }); const previous = queryClient.getQueryData(['todos']); queryClient.setQueryData(['todos'], (old) => @@ -65,7 +64,6 @@ export default function TodoItem({ todo, onEdit }: TodoItemProps) { toast.success('Todo deleted'); }, onError: (_err, _vars, context) => { - // Rollback on failure if (context?.previous) { queryClient.setQueryData(['todos'], context.previous); } @@ -87,7 +85,7 @@ export default function TodoItem({ todo, onEdit }: TodoItemProps) { return (
toggleMutation.mutate()} disabled={toggleMutation.isPending} + className="mt-0.5 md:mt-0" /> - onEdit(todo)} - > - {todo.title} - - - {/* Inline pills */} - - {todo.priority} - - - {todo.category && ( - - {todo.category} + {/* Content wrapper — stacks on mobile, inline on desktop */} +
+ {/* Title row — always takes full width on mobile */} + onEdit(todo)} + > + {todo.title} - )} - {todo.recurrence_rule && ( - - {recurrenceLabels[todo.recurrence_rule] || todo.recurrence_rule} - - )} - - {/* Date / time / reset info — right-aligned cluster */} - {showResetInfo ? ( -
- - - Resets {format(resetDate, 'EEE dd/MM')} - {nextDueDate && ( - <> · Due {format(nextDueDate, 'dd/MM')}{todo.due_time ? ` ${todo.due_time.slice(0, 5)}` : ''} + {/* Metadata row — wraps on second line on mobile */} +
+ + {todo.priority} -
- ) : ( - <> - {dueDate && ( - + {todo.category} + + )} + + {todo.recurrence_rule && ( + + {recurrenceLabels[todo.recurrence_rule] || todo.recurrence_rule} + + )} + + {showResetInfo ? ( +
+ + + Resets {format(resetDate, 'EEE dd/MM')} + {nextDueDate && ( + <>{' \u00b7 '}Due {format(nextDueDate, 'dd/MM')}{todo.due_time ? ` ${todo.due_time.slice(0, 5)}` : ''} + )} + +
+ ) : ( + <> + {dueDate && ( + + {isOverdue ? : } + {isOverdue ? 'Overdue \u00b7 ' : isDueToday ? 'Today \u00b7 ' : ''} + {format(dueDate, 'MMM d')} + )} - > - {isOverdue ? : } - {isOverdue ? 'Overdue · ' : isDueToday ? 'Today · ' : ''} - {format(dueDate, 'MMM d')} -
+ {todo.due_time && ( + + + {todo.due_time.slice(0, 5)} + + )} + )} - {todo.due_time && ( - - - {todo.due_time.slice(0, 5)} - - )} - - )} +
+
{/* Actions */} - - {confirmingDelete ? ( - - ) : ( - - )} + {confirmingDelete ? ( + + ) : ( + + )} +
); } diff --git a/frontend/src/components/todos/TodosPage.tsx b/frontend/src/components/todos/TodosPage.tsx index 57356ac..36ed396 100644 --- a/frontend/src/components/todos/TodosPage.tsx +++ b/frontend/src/components/todos/TodosPage.tsx @@ -130,7 +130,7 @@ export default function TodosPage() {
{/* Header */}
-

Todos

+

Todos

{/* Priority filter */} onSearchChange(e.target.value)} - className="w-52 h-8 pl-8 text-sm ring-inset" - aria-label="Search" - /> + {/* Search */} +
+
+ + onSearchChange(e.target.value)} + className="w-28 sm:w-52 h-8 pl-8 text-sm ring-inset" + aria-label="Search" + /> +
+ + {/* Expanded categories row — shows below on mobile, inline on desktop */} + {categories.length > 0 && otherOpen && ( +
+ {/* "All" chip inside categories — non-draggable */} + {onSelectAllCategories && ( + + )} + + {/* Draggable category chips */} + + + {categories.map((cat) => ( + onToggleCategory(cat)} + /> + ))} + + +
+ )}
); } diff --git a/frontend/src/index.css b/frontend/src/index.css index 87033c8..b0a5089 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -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 ── */ @media (max-width: 767px) { .fc .fc-daygrid-event { - font-size: 0.65rem; + font-size: 0.6rem; 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 { - font-size: 0.65rem; + font-size: 0.6rem; } .fc .fc-timegrid-event .fc-event-main { @@ -241,21 +279,21 @@ form[data-submitted] input:invalid + button { } .fc .fc-daygrid-day-number { - font-size: 0.75rem; + font-size: 0.7rem; padding: 2px 4px; } .fc .fc-col-header-cell-cushion { - font-size: 0.7rem; - padding: 4px 2px; + font-size: 0.65rem; + padding: 3px 2px; } .fc .fc-timegrid-slot-label { - font-size: 0.65rem; + font-size: 0.6rem; } .fc .fc-daygrid-more-link { - font-size: 0.6rem; + font-size: 0.55rem; } } /* ── Ambient background animations ── */ From 56175aaf8661b21788cc6d6f88454aa019e534cc Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Wed, 11 Mar 2026 02:13:41 +0800 Subject: [PATCH 10/16] Fix calendar popover, dropdown clipping, and header spacing across all tabs Add dark-themed FullCalendar "+more" popover with CSS X close button (replaces broken font icon). Add pr-8 to all mobile Select dropdowns to prevent text clipping under chevron. Normalize header gap to gap-2 md:gap-4 across all page headers for tighter mobile layout. Co-Authored-By: Claude Opus 4.6 --- .../components/locations/LocationsPage.tsx | 2 +- frontend/src/components/people/PeoplePage.tsx | 2 +- .../src/components/projects/ProjectsPage.tsx | 4 +- .../components/reminders/RemindersPage.tsx | 10 +-- frontend/src/components/todos/TodosPage.tsx | 4 +- frontend/src/index.css | 68 +++++++++++++++++++ 6 files changed, 79 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/locations/LocationsPage.tsx b/frontend/src/components/locations/LocationsPage.tsx index 00bee6a..0c9cfae 100644 --- a/frontend/src/components/locations/LocationsPage.tsx +++ b/frontend/src/components/locations/LocationsPage.tsx @@ -285,7 +285,7 @@ export default function LocationsPage() { return (
{/* Header */} -
+

Locations

diff --git a/frontend/src/components/people/PeoplePage.tsx b/frontend/src/components/people/PeoplePage.tsx index 3484b55..93a7058 100644 --- a/frontend/src/components/people/PeoplePage.tsx +++ b/frontend/src/components/people/PeoplePage.tsx @@ -555,7 +555,7 @@ export default function PeoplePage() { return (
{/* Header */} -
+

People

{/* Header */} -
+

Projects

setFilter(e.target.value as typeof filter)} - className="h-8 text-sm w-auto md:hidden" + className="h-8 text-sm w-auto pr-8 md:hidden" > {statusFilters.map((sf) => ( @@ -135,17 +135,17 @@ export default function RemindersPage() {
-
+
setSearch(e.target.value)} - className="w-32 sm:w-52 h-8 pl-8 text-sm ring-inset" + className="w-28 sm:w-52 h-8 pl-8 text-sm ring-inset" />
-
diff --git a/frontend/src/components/todos/TodosPage.tsx b/frontend/src/components/todos/TodosPage.tsx index 36ed396..cfbdec3 100644 --- a/frontend/src/components/todos/TodosPage.tsx +++ b/frontend/src/components/todos/TodosPage.tsx @@ -129,14 +129,14 @@ export default function TodosPage() { return (
{/* Header */} -
+

Todos

{/* Priority filter */} changeView(e.target.value as CalendarView)} - className="h-8 text-sm w-auto md:hidden" + className="h-8 text-sm w-auto pr-8 md:hidden" > {(Object.entries(viewLabels) as [CalendarView, string][]).map(([view, label]) => ( @@ -540,7 +540,7 @@ export default function CalendarPage() { ))}
-

{calendarTitle}

+

{calendarTitle}

From 9b41cb5003e1c750a9ea33cd7e6ddeab20940135 Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Wed, 11 Mar 2026 02:20:53 +0800 Subject: [PATCH 12/16] Fix task title truncation on mobile in Projects tab Hide verbose metadata columns (status badge, priority badge, date, subtask count) on mobile and replace with compact priority dot + overdue indicator. Reduce subtask indent and stack project summary card vertically on small screens. Co-Authored-By: Claude Opus 4.6 --- .../src/components/projects/ProjectDetail.tsx | 6 +++--- frontend/src/components/projects/TaskRow.tsx | 20 ++++++++++++++----- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/projects/ProjectDetail.tsx b/frontend/src/components/projects/ProjectDetail.tsx index ec4ba11..2a23787 100644 --- a/frontend/src/components/projects/ProjectDetail.tsx +++ b/frontend/src/components/projects/ProjectDetail.tsx @@ -424,7 +424,7 @@ export default function ProjectDetail() { {/* Project Summary Card */} -
+
Overall Progress @@ -442,7 +442,7 @@ export default function ProjectDetail() { {completedTasks} of {totalTasks} tasks completed

-
+
@@ -597,7 +597,7 @@ export default function ProjectDetail() { /> {/* Expanded subtasks */} {isExpanded && hasSubtasks && ( -
+
{task.subtasks.map((subtask) => ( {/* Metadata columns */} - +