fix(ux-audit): CRITICAL + HIGH fixes — a11y, reduced-motion, forms, keyboard nav
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 6m24s
CI / Deploy production (on server) (push) Successful in 23s

Global (globals.css):
- prefers-reduced-motion: désactive toutes les animations/transitions
- focus-visible:ring global sur tous les éléments interactifs

Search modal:
- role="dialog" + aria-modal="true" + aria-label
- onClick backdrop ferme la modale

Insights peek:
- DOMPurify.sanitize() sur dangerouslySetInnerHTML (XSS fix)

Login form:
- autoComplete="email" / "current-password"
- htmlFor sur tous les labels

Register form:
- autoComplete="name" / "email" / "new-password"
- htmlFor sur tous les labels

GridCard (notes-list-views):
- Actions visibles sur mobile (opacity-100 sm:opacity-0)
- aria-sort sur colonne triable
- role="button" + tabIndex + onKeyDown sur lignes table

Editorial view:
- role="button" + tabIndex + onKeyDown sur articles
- Menu trigger aria-label + visible mobile

Toolbar:
- aria-label voice i18n
This commit is contained in:
Antigravity
2026-07-04 21:37:56 +00:00
parent cf5d6a202b
commit e72ca26f97
8 changed files with 52 additions and 12 deletions

View File

@@ -86,7 +86,7 @@ export function LoginForm({
<form action={dispatch} className="space-y-4">
<div className="space-y-1.5">
<label className="text-[10px] uppercase tracking-widest font-bold text-[var(--muted-foreground)] px-4">
<label htmlFor="email" className="text-[10px] uppercase tracking-widest font-bold text-[var(--muted-foreground)] px-4">
{t('auth.email')}
</label>
<div className="relative group">
@@ -98,6 +98,7 @@ export function LoginForm({
id="email"
type="email"
name="email"
autoComplete="email"
placeholder={t('auth.emailPlaceholder')}
required
/>
@@ -106,7 +107,7 @@ export function LoginForm({
<div className="space-y-1.5">
<div className="flex justify-between items-center px-4">
<label className="text-[10px] uppercase tracking-widest font-bold text-[var(--muted-foreground)]">
<label htmlFor="password" className="text-[10px] uppercase tracking-widest font-bold text-[var(--muted-foreground)]">
{t('auth.password')}
</label>
<Link
@@ -125,6 +126,7 @@ export function LoginForm({
id="password"
type="password"
name="password"
autoComplete="current-password"
placeholder="••••••••"
required
minLength={6}

View File

@@ -668,7 +668,7 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
title={voiceState === 'listening'
? (t('editor.voiceStop') || 'Arrêter la dictée')
: (t('editor.voiceStart') || 'Dicter du texte')}
aria-label={voiceState === 'listening' ? 'Stop voice' : 'Start voice'}
aria-label={voiceState === 'listening' ? (t('richTextEditor.stopVoice') || 'Stop voice') : (t('richTextEditor.startVoice') || 'Start voice')}
onClick={toggleVoice}
className={cn(
'p-1.5 rounded-full border transition-all',

View File

@@ -157,7 +157,8 @@ export function EditorialNoteMenu({
<DropdownMenuTrigger asChild onClick={e => e.stopPropagation()}>
<button
ref={menuTriggerRef}
className="opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded-md hover:bg-muted/60 text-muted-foreground hover:text-foreground"
className="opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity p-1.5 rounded-md hover:bg-muted/60 text-muted-foreground hover:text-foreground cursor-pointer"
aria-label={t('notes.moreOptions') || 'Options'}
>
<MoreHorizontal size={15} />
</button>
@@ -396,6 +397,9 @@ export function NotesEditorialView({
transition={hydrated ? { delay: 0.05 * index, duration: 0.6 } : { duration: 0 }}
className="space-y-4 group cursor-pointer relative pb-8"
onClick={() => onOpen(note)}
onKeyDown={(e) => { if (e.key === 'Enter') onOpen(note) }}
tabIndex={0}
role="button"
>
{/* Date / breadcrumb — isolated bidi so Latin notebook name + Jalali date dont reorder wrongly */}
<div

View File

@@ -385,6 +385,7 @@ export function NotesListViews({
<th
className="w-[18%] px-4 py-3 text-[10px] uppercase tracking-widest font-black text-muted-foreground cursor-pointer hover:text-foreground"
onClick={() => handleSort('modified')}
aria-sort={sortColumn === 'modified' ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'}
>
<span className="inline-flex items-center gap-1">
{t('notes.tableModified')} <SortIcon field="modified" />
@@ -401,7 +402,10 @@ export function NotesListViews({
<tr
key={note.id}
onClick={() => onOpen(note)}
className="h-11 hover:bg-foreground/[0.02] cursor-pointer transition-colors group"
onKeyDown={(e) => { if (e.key === 'Enter') onOpen(note) }}
tabIndex={0}
role="button"
className="h-11 hover:bg-foreground/[0.02] cursor-pointer transition-colors group focus-visible:bg-foreground/[0.04]"
>
<td className="px-4 py-2 font-memento-serif text-[13px] font-medium truncate max-w-[280px]">
<span className="inline-flex items-center gap-2 truncate group-hover:text-brand-accent transition-colors">
@@ -781,7 +785,7 @@ const GridCard = memo(function GridCard({
</div>
<div className="flex items-center justify-between pt-3 mt-auto border-t border-foreground/[0.03] dark:border-white/[0.03] text-[9.5px] text-muted-foreground font-medium uppercase tracking-wider">
<span>{formattedDate}</span>
<div className="flex items-center gap-1 opacity-0 group-hover/card:opacity-100 transition-opacity">
<div className="flex items-center gap-1 opacity-100 sm:opacity-0 sm:group-hover/card:opacity-100 transition-opacity">
<button
type="button"
onClick={handleBrainstormClick}

View File

@@ -66,7 +66,7 @@ export function RegisterForm({ googleAuthEnabled = false }: { googleAuthEnabled?
<form action={dispatch} className="space-y-4">
<div className="space-y-1.5">
<label className="text-[10px] uppercase tracking-widest font-bold text-[var(--muted-foreground)] px-4">
<label htmlFor="name" className="text-[10px] uppercase tracking-widest font-bold text-[var(--muted-foreground)] px-4">
{t('auth.name')}
</label>
<div className="relative group">
@@ -78,6 +78,7 @@ export function RegisterForm({ googleAuthEnabled = false }: { googleAuthEnabled?
id="name"
type="text"
name="name"
autoComplete="name"
placeholder={t('auth.namePlaceholder')}
required
/>
@@ -85,7 +86,7 @@ export function RegisterForm({ googleAuthEnabled = false }: { googleAuthEnabled?
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase tracking-widest font-bold text-[var(--muted-foreground)] px-4">
<label htmlFor="email" className="text-[10px] uppercase tracking-widest font-bold text-[var(--muted-foreground)] px-4">
{t('auth.email')}
</label>
<div className="relative group">
@@ -97,6 +98,7 @@ export function RegisterForm({ googleAuthEnabled = false }: { googleAuthEnabled?
id="email"
type="email"
name="email"
autoComplete="email"
placeholder={t('auth.emailPlaceholder')}
required
/>
@@ -104,7 +106,7 @@ export function RegisterForm({ googleAuthEnabled = false }: { googleAuthEnabled?
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase tracking-widest font-bold text-[var(--muted-foreground)] px-4">
<label htmlFor="password" className="text-[10px] uppercase tracking-widest font-bold text-[var(--muted-foreground)] px-4">
{t('auth.password')}
</label>
<div className="relative group">
@@ -116,6 +118,7 @@ export function RegisterForm({ googleAuthEnabled = false }: { googleAuthEnabled?
id="password"
type="password"
name="password"
autoComplete="new-password"
placeholder={t('auth.passwordMinChars')}
required
minLength={6}
@@ -124,7 +127,7 @@ export function RegisterForm({ googleAuthEnabled = false }: { googleAuthEnabled?
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase tracking-widest font-bold text-[var(--muted-foreground)] px-4">
<label htmlFor="confirmPassword" className="text-[10px] uppercase tracking-widest font-bold text-[var(--muted-foreground)] px-4">
{t('auth.confirmPassword')}
</label>
<div className="relative group">
@@ -136,6 +139,7 @@ export function RegisterForm({ googleAuthEnabled = false }: { googleAuthEnabled?
id="confirmPassword"
type="password"
name="confirmPassword"
autoComplete="new-password"
placeholder={t('auth.confirmPasswordPlaceholder')}
required
minLength={6}

View File

@@ -409,7 +409,13 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
if (!isOpen) return null
return (
<div className="fixed inset-0 bg-black/40 dark:bg-black/60 backdrop-blur-sm flex items-center justify-center z-[200] p-4 sm:p-6 select-none">
<div
className="fixed inset-0 bg-black/40 dark:bg-black/60 backdrop-blur-sm flex items-center justify-center z-[200] p-4 sm:p-6 select-none"
role="dialog"
aria-modal="true"
aria-label="Recherche"
onClick={(e) => { if (e.target === e.currentTarget) onClose() }}
>
<motion.div
initial={{ opacity: 0, scale: 0.98, y: 8 }}
animate={{ opacity: 1, scale: 1, y: 0 }}