a11y: add error boundary, reduced motion, remaining fixes

This commit is contained in:
TypoGenie
2026-02-18 23:52:06 +02:00
parent 2ca1439680
commit 87dd578e92
4 changed files with 57 additions and 8 deletions

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { motion, AnimatePresence, useReducedMotion } from 'motion/react';
import { AppState, PaperSize } from './types';
import { FileUpload } from './components/FileUpload';
import { StyleSelector } from './components/StyleSelector';
@@ -82,6 +82,7 @@ const KeyboardShortcutsHelp: React.FC<{ isOpen: boolean; onClose: () => void }>
};
const App: React.FC = () => {
const prefersReducedMotion = useReducedMotion();
const [appState, setAppState] = useState<AppState>(AppState.UPLOAD);
const [content, setContent] = useState<string>('');
const [inputFileName, setInputFileName] = useState<string>('');
@@ -276,6 +277,8 @@ const App: React.FC = () => {
<kbd className="px-1.5 py-0.5 bg-zinc-800 rounded text-zinc-400 font-mono">?</kbd>
</motion.button>
<kbd className="sm:hidden px-1.5 py-0.5 bg-zinc-800 rounded text-zinc-400 font-mono text-xs border border-zinc-700 cursor-pointer" onClick={() => setShowShortcuts(true)} role="button" aria-label="Show keyboard shortcuts">?</kbd>
<AnimatePresence mode="wait">
{appState !== AppState.UPLOAD && (
<motion.nav
@@ -304,7 +307,7 @@ const App: React.FC = () => {
<div className="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
<motion.div
className="absolute -top-[20%] -left-[10%] w-[50%] h-[50%] bg-indigo-900/10 rounded-full blur-3xl"
animate={{
animate={prefersReducedMotion ? {} : {
x: [0, 30, 0],
y: [0, -20, 0],
scale: [1, 1.1, 1]
@@ -313,7 +316,7 @@ const App: React.FC = () => {
/>
<motion.div
className="absolute top-[20%] -right-[10%] w-[40%] h-[40%] bg-violet-900/10 rounded-full blur-3xl"
animate={{
animate={prefersReducedMotion ? {} : {
x: [0, -20, 0],
y: [0, 30, 0],
scale: [1, 1.15, 1]
@@ -348,7 +351,7 @@ const App: React.FC = () => {
Turn Markdown into <br />
<motion.span
className="text-transparent bg-clip-text bg-gradient-to-r from-indigo-400 to-violet-400"
animate={{
animate={prefersReducedMotion ? {} : {
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"]
}}
transition={{ duration: 5, repeat: Infinity, ease: "linear" }}
@@ -421,12 +424,12 @@ const App: React.FC = () => {
>
<motion.div
className="relative"
animate={{ rotate: 360 }}
animate={prefersReducedMotion ? {} : { rotate: 360 }}
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
>
<motion.div
className="absolute inset-0 bg-indigo-500 blur-xl opacity-20"
animate={{ scale: [1, 1.2, 1], opacity: [0.2, 0.4, 0.2] }}
animate={prefersReducedMotion ? {} : { scale: [1, 1.2, 1], opacity: [0.2, 0.4, 0.2] }}
transition={{ duration: 1.5, repeat: Infinity }}
/>
<Loader2 size={64} className="text-indigo-400 relative z-10" aria-hidden="true" />

View File

@@ -0,0 +1,43 @@
import React from 'react';
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends React.Component<{ children: React.ReactNode }, ErrorBoundaryState> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return (
<div
role="alert"
className="h-screen w-screen flex items-center justify-center bg-zinc-950 text-zinc-100 p-8"
>
<div className="max-w-md text-center">
<h1 className="text-2xl font-bold mb-4">Something went wrong</h1>
<p className="text-zinc-400 mb-6">
TypoGenie encountered an unexpected error. Please reload the application.
</p>
<button
onClick={() => window.location.reload()}
className="px-6 py-3 bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-xl transition-colors"
>
Reload
</button>
</div>
</div>
);
}
return this.props.children;
}
}

View File

@@ -431,7 +431,7 @@ export const StyleSelector: React.FC<StyleSelectorProps> = ({
)}
</div>
</div>
<p className="text-xs text-zinc-400 line-clamp-2 leading-relaxed mb-2">{style.description}</p>
<p className="text-xs text-zinc-400 line-clamp-2 leading-relaxed mb-2" title={style.description}>{style.description}</p>
<div className="flex items-center gap-2">
<span className={`text-[10px] uppercase font-mono tracking-wider px-1.5 py-0.5 rounded
${selectedStyle === style.id ? 'bg-indigo-500/20 text-indigo-300' : 'bg-zinc-800 text-zinc-400'}`}>

View File

@@ -1,6 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { ErrorBoundary } from './components/ErrorBoundary';
import './index.css';
const rootElement = document.getElementById('root');
@@ -11,6 +12,8 @@ if (!rootElement) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
<ErrorBoundary>
<App />
</ErrorBoundary>
</React.StrictMode>
);