Action deferred QA items: shared overlay, sort, touch, a11y
- S-01/W-06/S-02/S-04: Extract MobileDetailOverlay shared component with Escape key, body scroll lock, and ARIA dialog attributes. Refactored Todos, Reminders, People, Locations, ProjectDetail. - W-02: Add specificity contract comment to mobile-scale CSS - W-03: Enforce 10px floor for text-[9px] on mobile - W-05: Add sort dropdown to EntityTable mobile card view - S-03: Export MOBILE/DESKTOP breakpoint constants from useMediaQuery, updated all 8 consumer files to use constants - S-06: Bump KanbanBoard TouchSensor tolerance from 5 to 8 - S-07: Hover state audit — no action needed, hoverOnlyWhenSupported in Tailwind config already handles touch devices correctly Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e51b09f9c5
commit
a737f06e85
@ -1,5 +1,5 @@
|
||||
import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { useMediaQuery, DESKTOP } from '@/hooks/useMediaQuery';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
@ -164,7 +164,7 @@ export default function CalendarPage() {
|
||||
const panelOpen = panelMode !== 'closed';
|
||||
|
||||
// Track desktop breakpoint to prevent dual EventDetailPanel mount
|
||||
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||
const isDesktop = useMediaQuery(DESKTOP);
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||
|
||||
// Continuously resize calendar during panel open/close CSS transition
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useMemo, useRef, useEffect } from 'react';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { useMediaQuery, DESKTOP } 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';
|
||||
@ -17,10 +17,11 @@ import {
|
||||
import { useTableVisibility } from '@/hooks/useTableVisibility';
|
||||
import { useCategoryOrder } from '@/hooks/useCategoryOrder';
|
||||
import LocationForm from './LocationForm';
|
||||
import MobileDetailOverlay from '@/components/shared/MobileDetailOverlay';
|
||||
|
||||
export default function LocationsPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||
const isDesktop = useMediaQuery(DESKTOP);
|
||||
|
||||
const [selectedLocationId, setSelectedLocationId] = useState<number | null>(null);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
@ -387,17 +388,9 @@ export default function LocationsPage() {
|
||||
|
||||
{/* Mobile detail panel overlay */}
|
||||
{panelOpen && selectedLocation && !isDesktop && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
|
||||
onClick={() => setSelectedLocationId(null)}
|
||||
>
|
||||
<div
|
||||
className="fixed inset-y-0 right-0 w-full sm:w-[400px] bg-card border-l border-border shadow-lg"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{renderPanel()}
|
||||
</div>
|
||||
</div>
|
||||
<MobileDetailOverlay open={true} onClose={() => setSelectedLocationId(null)}>
|
||||
{renderPanel()}
|
||||
</MobileDetailOverlay>
|
||||
)}
|
||||
|
||||
{showForm && (
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useMemo, useRef, useEffect } from 'react';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { useMediaQuery, DESKTOP } 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';
|
||||
@ -27,6 +27,7 @@ import PersonForm from './PersonForm';
|
||||
import ConnectionSearch from '@/components/connections/ConnectionSearch';
|
||||
import ConnectionRequestCard from '@/components/connections/ConnectionRequestCard';
|
||||
import { useConnections } from '@/hooks/useConnections';
|
||||
import MobileDetailOverlay from '@/components/shared/MobileDetailOverlay';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// StatCounter — inline helper
|
||||
@ -215,7 +216,7 @@ const panelFields: PanelField[] = [
|
||||
export default function PeoplePage() {
|
||||
const queryClient = useQueryClient();
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||
const isDesktop = useMediaQuery(DESKTOP);
|
||||
|
||||
const [selectedPersonId, setSelectedPersonId] = useState<number | null>(null);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
@ -776,17 +777,9 @@ export default function PeoplePage() {
|
||||
|
||||
{/* Mobile detail panel overlay */}
|
||||
{panelOpen && selectedPerson && !isDesktop && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
|
||||
onClick={() => setSelectedPersonId(null)}
|
||||
>
|
||||
<div
|
||||
className="fixed inset-y-0 right-0 w-full sm:w-[400px] bg-card border-l border-border shadow-lg"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{renderPanel()}
|
||||
</div>
|
||||
</div>
|
||||
<MobileDetailOverlay open={true} onClose={() => setSelectedPersonId(null)}>
|
||||
{renderPanel()}
|
||||
</MobileDetailOverlay>
|
||||
)}
|
||||
|
||||
{showForm && (
|
||||
|
||||
@ -154,7 +154,7 @@ export default function KanbanBoard({
|
||||
}: KanbanBoardProps) {
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } })
,
|
||||
useSensor(TouchSensor, { activationConstraint: { delay: 200, tolerance: 5 } })
|
||||
useSensor(TouchSensor, { activationConstraint: { delay: 200, tolerance: 8 } })
|
||||
);
|
||||
|
||||
// Subtask view is driven by kanbanParentTask (decoupled from selected task)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { useMediaQuery, DESKTOP } from '@/hooks/useMediaQuery';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
@ -39,6 +39,7 @@ import KanbanBoard from './KanbanBoard';
|
||||
import TaskForm from './TaskForm';
|
||||
import ProjectForm from './ProjectForm';
|
||||
import { statusColors, statusLabels } from './constants';
|
||||
import MobileDetailOverlay from '@/components/shared/MobileDetailOverlay';
|
||||
|
||||
type SortMode = 'manual' | 'priority' | 'due_date';
|
||||
type ViewMode = 'list' | 'kanban';
|
||||
@ -258,7 +259,7 @@ export default function ProjectDetail() {
|
||||
}
|
||||
}, [topLevelTasks, sortMode, sortSubtasks]);
|
||||
|
||||
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||
const isDesktop = useMediaQuery(DESKTOP);
|
||||
|
||||
const selectedTask = useMemo(() => {
|
||||
if (!selectedTaskId) return null;
|
||||
@ -653,30 +654,28 @@ export default function ProjectDetail() {
|
||||
|
||||
{/* Mobile: show detail panel as overlay when task selected on small screens */}
|
||||
{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>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedTaskId(null)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-[calc(100%-49px)]">
|
||||
<TaskDetailPanel
|
||||
task={selectedTask}
|
||||
projectId={parseInt(id!)}
|
||||
onDelete={handleDeleteTask}
|
||||
onAddSubtask={(parentId) => openTaskForm(null, parentId)}
|
||||
onClose={() => setSelectedTaskId(null)}
|
||||
onSelectTask={setSelectedTaskId}
|
||||
/>
|
||||
</div>
|
||||
<MobileDetailOverlay open={true} onClose={() => setSelectedTaskId(null)}>
|
||||
<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>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedTaskId(null)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[calc(100%-49px)]">
|
||||
<TaskDetailPanel
|
||||
task={selectedTask}
|
||||
projectId={parseInt(id!)}
|
||||
onDelete={handleDeleteTask}
|
||||
onAddSubtask={(parentId) => openTaskForm(null, parentId)}
|
||||
onClose={() => setSelectedTaskId(null)}
|
||||
onSelectTask={setSelectedTaskId}
|
||||
/>
|
||||
</div>
|
||||
</MobileDetailOverlay>
|
||||
)}
|
||||
|
||||
{showTaskForm && (
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { useMediaQuery, DESKTOP } from '@/hooks/useMediaQuery';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Plus, Bell, BellOff, AlertCircle, Search } from 'lucide-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
@ -13,6 +13,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
||||
import { ListSkeleton } from '@/components/ui/skeleton';
|
||||
import ReminderList from './ReminderList';
|
||||
import ReminderDetailPanel from './ReminderDetailPanel';
|
||||
import MobileDetailOverlay from '@/components/shared/MobileDetailOverlay';
|
||||
|
||||
const statusFilters = [
|
||||
{ value: 'active', label: 'Active' },
|
||||
@ -25,7 +26,7 @@ type StatusFilter = (typeof statusFilters)[number]['value'];
|
||||
export default function RemindersPage() {
|
||||
const location = useLocation();
|
||||
|
||||
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||
const isDesktop = useMediaQuery(DESKTOP);
|
||||
|
||||
// Panel state
|
||||
const [selectedReminderId, setSelectedReminderId] = useState<number | null>(null);
|
||||
@ -235,22 +236,14 @@ export default function RemindersPage() {
|
||||
|
||||
{/* Mobile detail panel overlay */}
|
||||
{panelOpen && !isDesktop && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
|
||||
onClick={handlePanelClose}
|
||||
>
|
||||
<div
|
||||
className="fixed inset-y-0 right-0 w-full sm:w-[400px] bg-card border-l border-border shadow-lg"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ReminderDetailPanel
|
||||
reminder={panelMode === 'view' ? selectedReminder : null}
|
||||
isCreating={panelMode === 'create'}
|
||||
onClose={handlePanelClose}
|
||||
onSaved={panelMode === 'create' ? handlePanelClose : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<MobileDetailOverlay open={true} onClose={handlePanelClose}>
|
||||
<ReminderDetailPanel
|
||||
reminder={panelMode === 'view' ? selectedReminder : null}
|
||||
isCreating={panelMode === 'create'}
|
||||
onClose={handlePanelClose}
|
||||
onSaved={panelMode === 'create' ? handlePanelClose : undefined}
|
||||
/>
|
||||
</MobileDetailOverlay>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
|
||||
import { ArrowUpDown, ArrowUp, ArrowDown, ChevronsUpDown } from 'lucide-react';
|
||||
import type { VisibilityMode } from '@/hooks/useTableVisibility';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { useMediaQuery, MOBILE } from '@/hooks/useMediaQuery';
|
||||
|
||||
export interface ColumnDef<T> {
|
||||
key: string;
|
||||
@ -134,11 +134,39 @@ export function EntityTable<T extends { id: number }>({
|
||||
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)');
|
||||
const isMobile = useMediaQuery(MOBILE);
|
||||
|
||||
const sortableColumns = columns.filter((col) => col.sortable);
|
||||
|
||||
if (isMobile && mobileCardRender) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{sortableColumns.length > 0 && !loading && (
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
<ChevronsUpDown className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<select
|
||||
value={`${sortKey}:${sortDir}`}
|
||||
onChange={(e) => {
|
||||
const [key, dir] = e.target.value.split(':');
|
||||
// Toggle sort via onSort — if key matches current, it flips direction
|
||||
// If different key, set new key. We call onSort which handles the logic.
|
||||
if (key !== sortKey) {
|
||||
onSort(key);
|
||||
} else if (dir !== sortDir) {
|
||||
onSort(key);
|
||||
}
|
||||
}}
|
||||
className="h-7 rounded-md border border-border bg-card px-2 text-xs text-foreground"
|
||||
>
|
||||
{sortableColumns.map((col) => (
|
||||
<React.Fragment key={col.key}>
|
||||
<option value={`${col.key}:asc`}>{col.label} ↑</option>
|
||||
<option value={`${col.key}:desc`}>{col.label} ↓</option>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{loading ? (
|
||||
Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="animate-pulse rounded-lg bg-card border border-border p-4 h-20" />
|
||||
|
||||
63
frontend/src/components/shared/MobileDetailOverlay.tsx
Normal file
63
frontend/src/components/shared/MobileDetailOverlay.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { useEffect } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface MobileDetailOverlayProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full-screen overlay for mobile detail panels.
|
||||
* - Backdrop click closes the overlay
|
||||
* - Escape key closes the overlay
|
||||
* - Body scroll is locked while open
|
||||
*/
|
||||
export default function MobileDetailOverlay({
|
||||
open,
|
||||
onClose,
|
||||
children,
|
||||
className,
|
||||
}: MobileDetailOverlayProps) {
|
||||
// Escape key handler
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
document.addEventListener('keydown', handler);
|
||||
return () => document.removeEventListener('keydown', handler);
|
||||
}, [open, onClose]);
|
||||
|
||||
// Body scroll lock
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const previous = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
return () => {
|
||||
document.body.style.overflow = previous;
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm animate-fade-in"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute right-0 top-0 h-full w-full sm:max-w-md bg-card border-l border-border shadow-xl overflow-y-auto',
|
||||
className,
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { useMediaQuery, DESKTOP } from '@/hooks/useMediaQuery';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Plus, CheckSquare, CheckCircle2, AlertCircle } from 'lucide-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
@ -11,6 +11,7 @@ import { Select } from '@/components/ui/select';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { ListSkeleton } from '@/components/ui/skeleton';
|
||||
import { CategoryFilterBar } from '@/components/shared';
|
||||
import MobileDetailOverlay from '@/components/shared/MobileDetailOverlay';
|
||||
import { useCategoryOrder } from '@/hooks/useCategoryOrder';
|
||||
import TodoList from './TodoList';
|
||||
import TodoDetailPanel from './TodoDetailPanel';
|
||||
@ -26,7 +27,7 @@ const priorityFilters = [
|
||||
export default function TodosPage() {
|
||||
const location = useLocation();
|
||||
|
||||
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||
const isDesktop = useMediaQuery(DESKTOP);
|
||||
|
||||
// Panel state
|
||||
const [selectedTodoId, setSelectedTodoId] = useState<number | null>(null);
|
||||
@ -270,22 +271,14 @@ export default function TodosPage() {
|
||||
|
||||
{/* Mobile detail panel overlay */}
|
||||
{panelOpen && !isDesktop && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
|
||||
onClick={handlePanelClose}
|
||||
>
|
||||
<div
|
||||
className="fixed inset-y-0 right-0 w-full sm:w-[400px] bg-card border-l border-border shadow-lg"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<TodoDetailPanel
|
||||
todo={panelMode === 'view' ? selectedTodo : null}
|
||||
isCreating={panelMode === 'create'}
|
||||
onClose={handlePanelClose}
|
||||
onSaved={panelMode === 'create' ? handlePanelClose : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<MobileDetailOverlay open={true} onClose={handlePanelClose}>
|
||||
<TodoDetailPanel
|
||||
todo={panelMode === 'view' ? selectedTodo : null}
|
||||
isCreating={panelMode === 'create'}
|
||||
onClose={handlePanelClose}
|
||||
onSaved={panelMode === 'create' ? handlePanelClose : undefined}
|
||||
/>
|
||||
</MobileDetailOverlay>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Calendar, ChevronLeft, ChevronRight, Clock } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { useMediaQuery, MOBILE } from '@/hooks/useMediaQuery';
|
||||
|
||||
// ── Browser detection (stable — checked once at module load) ──
|
||||
|
||||
@ -128,7 +128,7 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
||||
const blurTimeoutRef = React.useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
const [pos, setPos] = React.useState<{ top: number; left: number }>({ top: 0, left: 0 });
|
||||
const isMobile = useMediaQuery('(max-width: 767px)');
|
||||
const isMobile = useMediaQuery(MOBILE);
|
||||
|
||||
React.useImperativeHandle(ref, () => triggerRef.current!);
|
||||
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export const MOBILE = '(max-width: 767px)';
|
||||
export const DESKTOP = '(min-width: 1024px)';
|
||||
|
||||
export function useMediaQuery(query: string): boolean {
|
||||
const [matches, setMatches] = useState(() =>
|
||||
typeof window !== 'undefined' ? window.matchMedia(query).matches : false
|
||||
|
||||
@ -292,6 +292,12 @@ form[data-submitted] input:invalid + button {
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Mobile font scaling — overrides Tailwind text utilities below 768px.
|
||||
* Applied via .mobile-scale class on <main> in AppLayout.tsx.
|
||||
* These selectors rely on cascade order (loaded after Tailwind utilities).
|
||||
* Arbitrary sizes like text-[9px] are NOT captured — see W-03 override below.
|
||||
*/
|
||||
/* ── 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. */
|
||||
@ -323,6 +329,10 @@ form[data-submitted] input:invalid + button {
|
||||
.mobile-scale .text-3xl {
|
||||
font-size: 1.375rem; /* 22px */
|
||||
}
|
||||
/* W-03: Enforce 10px floor for arbitrary small sizes */
|
||||
.mobile-scale [class*="text-\[9px\]"] {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Mobile touch optimisation ── */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user