Fix QA findings: dual panel mount, touch-action, font floor, a11y
- Replace CSS-only panel hiding with isDesktop media query guard in Todos, Reminders, People, Locations, ProjectDetail (W-01) - Add touch-action: manipulation for mobile interactive elements (W-04) - Bump FullCalendar more-link from 0.55rem to 0.625rem (W-07) - Add aria-label on admin portal tab NavLinks (S-05) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
98ad83ae5f
commit
89f72895c1
@ -35,6 +35,7 @@ export default function AdminPortal() {
|
||||
key={path}
|
||||
to={path}
|
||||
title={label}
|
||||
aria-label={label}
|
||||
className={cn(
|
||||
'flex items-center justify-center gap-1.5 px-2.5 md:px-4 h-full text-sm font-medium transition-colors duration-150 border-b-2 -mb-px whitespace-nowrap',
|
||||
isActive
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useState, useMemo, useRef, useEffect } from 'react';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { Plus, MapPin, Phone, Mail, Tag, AlignLeft } from 'lucide-react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
@ -19,6 +20,7 @@ import LocationForm from './LocationForm';
|
||||
|
||||
export default function LocationsPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||
|
||||
const [selectedLocationId, setSelectedLocationId] = useState<number | null>(null);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
@ -373,20 +375,20 @@ export default function LocationsPage() {
|
||||
</div>
|
||||
|
||||
{/* Detail panel (desktop) */}
|
||||
{panelOpen && isDesktop && (
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] ${
|
||||
panelOpen ? 'hidden lg:block lg:w-[45%]' : 'w-0 opacity-0'
|
||||
}`}
|
||||
className="overflow-hidden transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] w-[45%]"
|
||||
>
|
||||
{renderPanel()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile detail panel overlay */}
|
||||
{panelOpen && selectedLocation && (
|
||||
{panelOpen && selectedLocation && !isDesktop && (
|
||||
<div
|
||||
className="lg:hidden fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
|
||||
className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
|
||||
onClick={() => setSelectedLocationId(null)}
|
||||
>
|
||||
<div
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useState, useMemo, useRef, useEffect } from 'react';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { Plus, Users, Star, Cake, Phone, Mail, MapPin, Tag, Building2, Briefcase, AlignLeft, Ghost, ChevronDown, Unlink, Link2, User2 } from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
@ -214,6 +215,7 @@ const panelFields: PanelField[] = [
|
||||
export default function PeoplePage() {
|
||||
const queryClient = useQueryClient();
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||
|
||||
const [selectedPersonId, setSelectedPersonId] = useState<number | null>(null);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
@ -762,20 +764,20 @@ export default function PeoplePage() {
|
||||
</div>
|
||||
|
||||
{/* Detail panel (desktop) */}
|
||||
{panelOpen && isDesktop && (
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] ${
|
||||
panelOpen ? 'hidden lg:block lg:w-[45%]' : 'w-0 opacity-0'
|
||||
}`}
|
||||
className="overflow-hidden transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] w-[45%]"
|
||||
>
|
||||
{renderPanel()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile detail panel overlay */}
|
||||
{panelOpen && selectedPerson && (
|
||||
{panelOpen && selectedPerson && !isDesktop && (
|
||||
<div
|
||||
className="lg:hidden fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
|
||||
className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
|
||||
onClick={() => setSelectedPersonId(null)}
|
||||
>
|
||||
<div
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
@ -257,6 +258,8 @@ export default function ProjectDetail() {
|
||||
}
|
||||
}, [topLevelTasks, sortMode, sortSubtasks]);
|
||||
|
||||
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||
|
||||
const selectedTask = useMemo(() => {
|
||||
if (!selectedTaskId) return null;
|
||||
// Search top-level and subtasks
|
||||
@ -628,11 +631,10 @@ export default function ProjectDetail() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right panel: task detail (hidden on small screens) */}
|
||||
{/* Right panel: task detail (desktop only) */}
|
||||
{selectedTaskId && isDesktop && (
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] border-l border-border bg-card ${
|
||||
selectedTaskId ? 'hidden lg:flex lg:w-[45%]' : 'w-0 opacity-0 border-l-0'
|
||||
}`}
|
||||
className="overflow-hidden transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] border-l border-border bg-card flex w-[45%]"
|
||||
>
|
||||
<div className="flex-1 overflow-hidden min-w-[360px]">
|
||||
<TaskDetailPanel
|
||||
@ -645,12 +647,13 @@ export default function ProjectDetail() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile: show detail panel as overlay when task selected on small screens */}
|
||||
{selectedTaskId && selectedTask && (
|
||||
<div className="lg:hidden fixed inset-0 z-50 bg-background/80 backdrop-blur-sm">
|
||||
{selectedTaskId && selectedTask && !isDesktop && (
|
||||
<div className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm">
|
||||
<div className="fixed inset-y-0 right-0 w-full sm:w-[400px] bg-card border-l border-border shadow-lg">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
|
||||
<span className="text-sm font-medium text-muted-foreground">Task Details</span>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Plus, Bell, BellOff, AlertCircle, Search } from 'lucide-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
@ -24,6 +25,8 @@ type StatusFilter = (typeof statusFilters)[number]['value'];
|
||||
export default function RemindersPage() {
|
||||
const location = useLocation();
|
||||
|
||||
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||
|
||||
// Panel state
|
||||
const [selectedReminderId, setSelectedReminderId] = useState<number | null>(null);
|
||||
const [panelMode, setPanelMode] = useState<'closed' | 'view' | 'create'>('closed');
|
||||
@ -216,10 +219,9 @@ export default function RemindersPage() {
|
||||
</div>
|
||||
|
||||
{/* Detail panel (desktop) */}
|
||||
{panelOpen && isDesktop && (
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] ${
|
||||
panelOpen ? 'hidden lg:block lg:w-[45%]' : 'w-0 opacity-0'
|
||||
}`}
|
||||
className="overflow-hidden transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] w-[45%]"
|
||||
>
|
||||
<ReminderDetailPanel
|
||||
reminder={panelMode === 'view' ? selectedReminder : null}
|
||||
@ -228,12 +230,13 @@ export default function RemindersPage() {
|
||||
onSaved={panelMode === 'create' ? handlePanelClose : undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile detail panel overlay */}
|
||||
{panelOpen && (
|
||||
{panelOpen && !isDesktop && (
|
||||
<div
|
||||
className="lg:hidden fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
|
||||
className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
|
||||
onClick={handlePanelClose}
|
||||
>
|
||||
<div
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Plus, CheckSquare, CheckCircle2, AlertCircle } from 'lucide-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
@ -25,6 +26,8 @@ const priorityFilters = [
|
||||
export default function TodosPage() {
|
||||
const location = useLocation();
|
||||
|
||||
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||
|
||||
// Panel state
|
||||
const [selectedTodoId, setSelectedTodoId] = useState<number | null>(null);
|
||||
const [panelMode, setPanelMode] = useState<'closed' | 'view' | 'create'>('closed');
|
||||
@ -251,10 +254,9 @@ export default function TodosPage() {
|
||||
</div>
|
||||
|
||||
{/* Detail panel (desktop) */}
|
||||
{panelOpen && isDesktop && (
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] ${
|
||||
panelOpen ? 'hidden lg:block lg:w-[45%]' : 'w-0 opacity-0'
|
||||
}`}
|
||||
className="overflow-hidden transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] w-[45%]"
|
||||
>
|
||||
<TodoDetailPanel
|
||||
todo={panelMode === 'view' ? selectedTodo : null}
|
||||
@ -263,12 +265,13 @@ export default function TodosPage() {
|
||||
onSaved={panelMode === 'create' ? handlePanelClose : undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile detail panel overlay */}
|
||||
{panelOpen && (
|
||||
{panelOpen && !isDesktop && (
|
||||
<div
|
||||
className="lg:hidden fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
|
||||
className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
|
||||
onClick={handlePanelClose}
|
||||
>
|
||||
<div
|
||||
|
||||
@ -325,6 +325,13 @@ form[data-submitted] input:invalid + button {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Mobile touch optimisation ── */
|
||||
@media (max-width: 767px) {
|
||||
button, a, [role="button"], input, select, textarea {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── FullCalendar mobile overrides ── */
|
||||
@media (max-width: 767px) {
|
||||
.fc .fc-daygrid-event {
|
||||
@ -361,7 +368,7 @@ form[data-submitted] input:invalid + button {
|
||||
}
|
||||
|
||||
.fc .fc-daygrid-more-link {
|
||||
font-size: 0.55rem;
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
}
|
||||
/* ── Ambient background animations ── */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user