a11y: add error boundary, reduced motion, remaining fixes
This commit is contained in:
15
src/App.tsx
15
src/App.tsx
@@ -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" />
|
||||
|
||||
43
src/components/ErrorBoundary.tsx
Normal file
43
src/components/ErrorBoundary.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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'}`}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user