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:
parent
f7ec04241b
commit
4d5052d731
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user