Action QA findings: fix all critical/warning/suggestion items

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 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-07 17:16:47 +08:00
parent f7ec04241b
commit 4d5052d731
10 changed files with 23 additions and 20 deletions

View File

@ -17,7 +17,7 @@ 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 { 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 CalendarSidebar from './CalendarSidebar';
import EventDetailPanel from './EventDetailPanel'; import EventDetailPanel from './EventDetailPanel';
import type { CreateDefaults } from './EventDetailPanel'; import type { CreateDefaults } from './EventDetailPanel';
@ -165,7 +165,6 @@ export default function CalendarPage() {
// Track desktop breakpoint to prevent dual EventDetailPanel mount // Track desktop breakpoint to prevent dual EventDetailPanel mount
const isDesktop = useMediaQuery('(min-width: 1024px)'); const isDesktop = useMediaQuery('(min-width: 1024px)');
const isMobile = useMediaQuery('(max-width: 1023px)');
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
// Continuously resize calendar during panel open/close CSS transition // Continuously resize calendar during panel open/close CSS transition
@ -484,9 +483,10 @@ export default function CalendarPage() {
/> />
</div> </div>
{isMobile && ( {!isDesktop && (
<Sheet open={mobileSidebarOpen} onOpenChange={setMobileSidebarOpen}> <Sheet open={mobileSidebarOpen} onOpenChange={setMobileSidebarOpen}>
<SheetContent className="w-72 p-0"> <SheetContent className="w-72 p-0">
<SheetClose onClick={() => setMobileSidebarOpen(false)} />
<CalendarSidebar onUseTemplate={(tmpl) => { setMobileSidebarOpen(false); handleUseTemplate(tmpl); }} onSharedVisibilityChange={setVisibleSharedIds} width={288} /> <CalendarSidebar onUseTemplate={(tmpl) => { setMobileSidebarOpen(false); handleUseTemplate(tmpl); }} onSharedVisibilityChange={setVisibleSharedIds} width={288} />
</SheetContent> </SheetContent>
</Sheet> </Sheet>
@ -540,12 +540,12 @@ export default function CalendarPage() {
))} ))}
</div> </div>
<h2 className="text-lg font-semibold font-heading hidden sm:block">{calendarTitle}</h2> <h2 className="text-sm sm:text-lg font-semibold font-heading truncate">{calendarTitle}</h2>
<div className="flex-1" /> <div className="flex-1" />
{/* Event search */} {/* Event search */}
<div className="relative hidden md:block"> <div className="relative">
<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,7 +553,7 @@ export default function CalendarPage() {
onChange={(e) => setEventSearch(e.target.value)} onChange={(e) => setEventSearch(e.target.value)}
onFocus={() => setSearchFocused(true)} onFocus={() => setSearchFocused(true)}
onBlur={() => setTimeout(() => setSearchFocused(false), 200)} 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 && ( {searchFocused && searchResults.length > 0 && (
<div className="absolute z-50 mt-1 w-72 right-0 rounded-md border bg-popover shadow-lg overflow-hidden"> <div className="absolute z-50 mt-1 w-72 right-0 rounded-md border bg-popover shadow-lg overflow-hidden">
@ -644,7 +644,7 @@ export default function CalendarPage() {
onClick={handlePanelClose} onClick={handlePanelClose}
> >
<div <div
className="fixed inset-y-0 right-0 w-full sm:w-[400px] bg-card border-l border-border shadow-lg" className="fixed inset-y-0 right-0 w-full sm:w-[400px] bg-card border-l border-border shadow-lg overflow-y-auto"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<EventDetailPanel <EventDetailPanel

View File

@ -360,7 +360,7 @@ export default function LocationsPage() {
<div className={`rounded-lg border p-3 transition-colors ${selectedLocationId === location.id ? 'border-accent/40 bg-accent/5' : 'border-border bg-card hover:bg-card-elevated'}`}> <div className={`rounded-lg border p-3 transition-colors ${selectedLocationId === location.id ? 'border-accent/40 bg-accent/5' : 'border-border bg-card hover:bg-card-elevated'}`}>
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<span className="font-medium text-sm truncate flex-1">{location.name}</span> <span className="font-medium text-sm truncate flex-1">{location.name}</span>
{location.category && <span className="text-[10px] text-muted-foreground">{location.category}</span>} {location.category && <span className="text-xs text-muted-foreground">{location.category}</span>}
</div> </div>
{location.address && ( {location.address && (
<p className="text-xs text-muted-foreground truncate">{location.address}</p> <p className="text-xs text-muted-foreground truncate">{location.address}</p>

View File

@ -748,7 +748,7 @@ export default function PeoplePage() {
<div className={`rounded-lg border p-3 transition-colors ${selectedPersonId === person.id ? 'border-accent/40 bg-accent/5' : 'border-border bg-card hover:bg-card-elevated'}`}> <div className={`rounded-lg border p-3 transition-colors ${selectedPersonId === person.id ? 'border-accent/40 bg-accent/5' : 'border-border bg-card hover:bg-card-elevated'}`}>
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<span className="font-medium text-sm truncate flex-1">{person.name}</span> <span className="font-medium text-sm truncate flex-1">{person.name}</span>
{person.category && <span className="text-[10px] text-muted-foreground">{person.category}</span>} {person.category && <span className="text-xs text-muted-foreground">{person.category}</span>}
</div> </div>
<div className="flex items-center gap-2 text-xs text-muted-foreground"> <div className="flex items-center gap-2 text-xs text-muted-foreground">
{person.email && <span className="truncate">{person.email}</span>} {person.email && <span className="truncate">{person.email}</span>}

View File

@ -2,6 +2,7 @@ import {
DndContext, DndContext,
closestCorners, closestCorners,
PointerSensor, PointerSensor,
TouchSensor,
useSensor, useSensor,
useSensors, useSensors,
type DragEndEvent, type DragEndEvent,
@ -200,7 +201,7 @@ export default function KanbanBoard({
collisionDetection={closestCorners} collisionDetection={closestCorners}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
> >
<div className="flex gap-3 overflow-x-auto pb-2" style={{ WebkitOverflowScrolling: "touch" }}> <div className="flex gap-3 overflow-x-auto pb-2">
{tasksByStatus.map(({ column, tasks: colTasks }) => ( {tasksByStatus.map(({ column, tasks: colTasks }) => (
<KanbanColumn <KanbanColumn
key={column.id} key={column.id}

View File

@ -105,13 +105,13 @@ export default function ProjectsPage() {
<div className="flex-1" /> <div className="flex-1" />
<div className="relative hidden md:block"> <div className="relative">
<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..."
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => 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"
/> />
</div> </div>

View File

@ -135,13 +135,13 @@ export default function RemindersPage() {
<div className="flex-1" /> <div className="flex-1" />
<div className="relative hidden md:block"> <div className="relative">
<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..."
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => 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"
/> />
</div> </div>

View File

@ -149,7 +149,7 @@ export function EntityTable<T extends { id: number }>({
<> <>
<p className="text-[11px] uppercase tracking-wider text-muted-foreground font-medium pt-2">{pinnedLabel}</p> <p className="text-[11px] uppercase tracking-wider text-muted-foreground font-medium pt-2">{pinnedLabel}</p>
{pinnedRows.map((item) => ( {pinnedRows.map((item) => (
<div key={item.id} onClick={() => onRowClick(item.id)} className="cursor-pointer"> <div key={item.id} onClick={() => 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)} {mobileCardRender(item)}
</div> </div>
))} ))}
@ -161,7 +161,7 @@ export function EntityTable<T extends { id: number }>({
<> <>
<p className="text-[11px] uppercase tracking-wider text-muted-foreground font-medium pt-2">{group.label}</p> <p className="text-[11px] uppercase tracking-wider text-muted-foreground font-medium pt-2">{group.label}</p>
{group.rows.map((item) => ( {group.rows.map((item) => (
<div key={item.id} onClick={() => onRowClick(item.id)} className="cursor-pointer"> <div key={item.id} onClick={() => 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)} {mobileCardRender(item)}
</div> </div>
))} ))}

View File

@ -18,7 +18,7 @@ const buttonVariants = cva(
default: 'h-10 px-4 py-2', default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3', sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8', 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: { defaultVariants: {

View File

@ -326,8 +326,8 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
<div <div
ref={popupRef} ref={popupRef}
onMouseDown={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()}
style={{ position: 'fixed', top: pos.top, left: pos.left, zIndex: 60 }} style={isMobile ? { position: 'fixed', bottom: 0, left: 0, right: 0, zIndex: 60 } : { 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" 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 */} {/* Month/Year nav */}
<div className="flex items-center justify-between px-3 pt-3 pb-2"> <div className="flex items-center justify-between px-3 pt-3 pb-2">

View File

@ -1,7 +1,9 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
export function useMediaQuery(query: string): boolean { 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(() => { useEffect(() => {
const mql = window.matchMedia(query); const mql = window.matchMedia(query);