a11y: fix CSS foundation - contrast, overflow, focus ring, reduced motion

This commit is contained in:
TypoGenie
2026-02-18 23:22:04 +02:00
parent 3b8e80c3a3
commit 7d5af9e39c
7 changed files with 58 additions and 26 deletions

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TypoGenie</title> <title>TypoGenie - Markdown to Word Converter</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet">
@@ -18,6 +18,8 @@
</style> </style>
</head> </head>
<body> <body>
<a href="#main-content" class="sr-only" style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;" onfocus="this.style.position='static';this.style.width='auto';this.style.height='auto';this.style.overflow='visible';" onblur="this.style.position='absolute';this.style.left='-9999px';this.style.width='1px';this.style.height='1px';this.style.overflow='hidden';">Skip to main content</a>
<noscript><p style="padding:2rem;color:#e4e4e7;background:#09090b;font-family:sans-serif;">TypoGenie requires JavaScript to run.</p></noscript>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>

View File

@@ -71,7 +71,7 @@ const KeyboardShortcutsHelp: React.FC<{ onClose: () => void }> = ({ onClose }) =
</motion.div> </motion.div>
))} ))}
</div> </div>
<p className="mt-6 text-xs text-zinc-500 text-center"> <p className="mt-6 text-xs text-zinc-400 text-center">
Press Escape or click outside to close Press Escape or click outside to close
</p> </p>
</motion.div> </motion.div>
@@ -233,7 +233,7 @@ const App: React.FC = () => {
onClick={refresh} onClick={refresh}
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
className="hidden sm:flex items-center gap-2 px-3 py-1.5 text-xs text-zinc-500 hover:text-zinc-300 bg-zinc-900 hover:bg-zinc-800 rounded-lg transition-colors border border-zinc-800" className="hidden sm:flex items-center gap-2 px-3 py-1.5 text-xs text-zinc-400 hover:text-zinc-300 bg-zinc-900 hover:bg-zinc-800 rounded-lg transition-colors border border-zinc-800"
title="Reload templates from disk" title="Reload templates from disk"
> >
<RefreshCw size={14} /> <RefreshCw size={14} />
@@ -246,7 +246,7 @@ const App: React.FC = () => {
onClick={() => setShowShortcuts(true)} onClick={() => setShowShortcuts(true)}
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
className="hidden sm:flex items-center gap-2 px-3 py-1.5 text-xs text-zinc-500 hover:text-zinc-300 bg-zinc-900 hover:bg-zinc-800 rounded-lg transition-colors border border-zinc-800" className="hidden sm:flex items-center gap-2 px-3 py-1.5 text-xs text-zinc-400 hover:text-zinc-300 bg-zinc-900 hover:bg-zinc-800 rounded-lg transition-colors border border-zinc-800"
title="Show keyboard shortcuts" title="Show keyboard shortcuts"
> >
<Keyboard size={14} /> <Keyboard size={14} />
@@ -260,7 +260,7 @@ const App: React.FC = () => {
initial={{ opacity: 0, x: 20 }} initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }} exit={{ opacity: 0, x: -20 }}
className="flex items-center gap-4 text-sm text-zinc-500" className="flex items-center gap-4 text-sm text-zinc-400"
> >
<span className={appState === AppState.CONFIG ? "text-indigo-400 font-medium" : ""}>Configure</span> <span className={appState === AppState.CONFIG ? "text-indigo-400 font-medium" : ""}>Configure</span>
<span>/</span> <span>/</span>
@@ -433,7 +433,7 @@ const App: React.FC = () => {
initial={{ y: 20, opacity: 0 }} initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }} animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.5, duration: 0.5 }} transition={{ delay: 0.5, duration: 0.5 }}
className="flex-none py-4 text-center text-zinc-600 text-sm border-t border-zinc-900 bg-zinc-950" className="flex-none py-4 text-center text-zinc-400 text-sm border-t border-zinc-900 bg-zinc-950"
> >
<p>TypoGenie - Professional Typesetting Engine</p> <p>TypoGenie - Professional Typesetting Engine</p>
</motion.footer> </motion.footer>

View File

@@ -73,7 +73,7 @@ export default function ExportOptionsModal({ isOpen, onClose, onExport }: Export
<span className="text-zinc-300">Document outline/navigation disabled</span> <span className="text-zinc-300">Document outline/navigation disabled</span>
</div> </div>
</div> </div>
<div className="mt-3 text-xs text-zinc-500 italic"> <div className="mt-3 text-xs text-zinc-400 italic">
Best for: Portfolios, brochures, print-ready designs Best for: Portfolios, brochures, print-ready designs
</div> </div>
</div> </div>
@@ -116,7 +116,7 @@ export default function ExportOptionsModal({ isOpen, onClose, onExport }: Export
<span className="text-zinc-300">Minor padding/border alignment issues</span> <span className="text-zinc-300">Minor padding/border alignment issues</span>
</div> </div>
</div> </div>
<div className="mt-3 text-xs text-zinc-500 italic"> <div className="mt-3 text-xs text-zinc-400 italic">
Best for: Academic papers, reports, accessible documents Best for: Academic papers, reports, accessible documents
</div> </div>
</div> </div>

View File

@@ -149,7 +149,7 @@ export const FileUpload: React.FC<FileUploadProps> = ({ onFileLoaded }) => {
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
> >
<motion.div <motion.div
className={`p-4 rounded-full mb-4 ${dragActive ? 'bg-indigo-500/20 text-indigo-400' : 'bg-zinc-800 text-zinc-500'}`} className={`p-4 rounded-full mb-4 ${dragActive ? 'bg-indigo-500/20 text-indigo-400' : 'bg-zinc-800 text-zinc-400'}`}
animate={dragActive ? { animate={dragActive ? {
scale: [1, 1.2, 1], scale: [1, 1.2, 1],
rotate: [0, 10, -10, 0] rotate: [0, 10, -10, 0]
@@ -174,7 +174,7 @@ export const FileUpload: React.FC<FileUploadProps> = ({ onFileLoaded }) => {
or drag and drop or drag and drop
</motion.p> </motion.p>
<motion.p <motion.p
className="text-sm text-zinc-500" className="text-sm text-zinc-400"
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ delay: 0.7 }} transition={{ delay: 0.7 }}
@@ -182,7 +182,7 @@ export const FileUpload: React.FC<FileUploadProps> = ({ onFileLoaded }) => {
Markdown or Plain Text files Markdown or Plain Text files
</motion.p> </motion.p>
<motion.p <motion.p
className="text-xs text-zinc-600 mt-2" className="text-xs text-zinc-400 mt-2"
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ delay: 0.8 }} transition={{ delay: 0.8 }}

View File

@@ -85,7 +85,7 @@ const FontList: React.FC<{ fonts: string[] }> = ({ fonts }) => {
}; };
return ( return (
<div className="flex items-center gap-2 text-sm text-zinc-500"> <div className="flex items-center gap-2 text-sm text-zinc-400">
<span className="hidden md:inline">Fonts:</span> <span className="hidden md:inline">Fonts:</span>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{fonts.map((font, index) => ( {fonts.map((font, index) => (
@@ -391,7 +391,7 @@ export const Preview: React.FC<PreviewProps> = ({
<ZoomControl zoom={uiZoom} onZoomChange={onZoomChange} /> <ZoomControl zoom={uiZoom} onZoomChange={onZoomChange} />
<div className="h-4 w-px bg-zinc-800 hidden sm:block" /> <div className="h-4 w-px bg-zinc-800 hidden sm:block" />
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<span className="text-zinc-500 text-sm hidden sm:inline">Format: {paperSize}</span> <span className="text-zinc-400 text-sm hidden sm:inline">Format: {paperSize}</span>
<motion.button <motion.button
ref={saveButtonRef} ref={saveButtonRef}
onClick={handleSave} onClick={handleSave}

View File

@@ -300,7 +300,7 @@ export const StyleSelector: React.FC<StyleSelectorProps> = ({
onClick={() => onSelectPaperSize(size)} onClick={() => onSelectPaperSize(size)}
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${selectedPaperSize === size className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${selectedPaperSize === size
? 'bg-zinc-700 text-white shadow-sm' ? 'bg-zinc-700 text-white shadow-sm'
: 'text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800' : 'text-zinc-400 hover:text-zinc-300 hover:bg-zinc-800'
}`} }`}
> >
{size} {size}
@@ -358,13 +358,13 @@ export const StyleSelector: React.FC<StyleSelectorProps> = ({
{/* Search Input */} {/* Search Input */}
<div className="px-4 pb-4"> <div className="px-4 pb-4">
<div className="relative group"> <div className="relative group">
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-500 group-focus-within:text-indigo-400 transition-colors" /> <Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400 group-focus-within:text-indigo-400 transition-colors" />
<input <input
type="text" type="text"
placeholder="Search templates..." placeholder="Search templates..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-zinc-900 border border-zinc-700 rounded-xl py-2 pl-9 pr-4 text-sm text-zinc-200 placeholder:text-zinc-600 focus:outline-none focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/50 transition-all" className="w-full bg-zinc-900 border border-zinc-700 rounded-xl py-2 pl-9 pr-4 text-sm text-zinc-200 placeholder:text-zinc-500 focus:outline-none focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/50 transition-all"
/> />
</div> </div>
</div> </div>
@@ -373,7 +373,7 @@ export const StyleSelector: React.FC<StyleSelectorProps> = ({
{/* Scrollable List */} {/* Scrollable List */}
<div className="flex-1 overflow-y-auto p-4 space-y-3 custom-scrollbar"> <div className="flex-1 overflow-y-auto p-4 space-y-3 custom-scrollbar">
{filteredStyles.length === 0 ? ( {filteredStyles.length === 0 ? (
<div className="text-center py-8 text-zinc-500"> <div className="text-center py-8 text-zinc-400">
<p className="text-sm">No templates found</p> <p className="text-sm">No templates found</p>
</div> </div>
) : filteredStyles.map((style) => ( ) : filteredStyles.map((style) => (
@@ -394,7 +394,7 @@ export const StyleSelector: React.FC<StyleSelectorProps> = ({
onClick={(e) => toggleFavorite(e, style.id)} onClick={(e) => toggleFavorite(e, style.id)}
className={`p-1.5 rounded-full transition-all ${favorites.includes(style.id) className={`p-1.5 rounded-full transition-all ${favorites.includes(style.id)
? 'text-rose-400 bg-rose-500/10 hover:bg-rose-500/20' ? 'text-rose-400 bg-rose-500/10 hover:bg-rose-500/20'
: 'text-zinc-600 hover:text-zinc-400 hover:bg-zinc-700/50' : 'text-zinc-400 hover:text-zinc-300 hover:bg-zinc-700/50'
}`} }`}
> >
<Heart size={14} className={favorites.includes(style.id) ? 'fill-current' : ''} /> <Heart size={14} className={favorites.includes(style.id) ? 'fill-current' : ''} />
@@ -406,10 +406,10 @@ export const StyleSelector: React.FC<StyleSelectorProps> = ({
)} )}
</div> </div>
</div> </div>
<p className="text-xs text-zinc-500 line-clamp-2 leading-relaxed mb-2">{style.description}</p> <p className="text-xs text-zinc-400 line-clamp-2 leading-relaxed mb-2">{style.description}</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className={`text-[10px] uppercase font-mono tracking-wider px-1.5 py-0.5 rounded <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-500'}`}> ${selectedStyle === style.id ? 'bg-indigo-500/20 text-indigo-300' : 'bg-zinc-800 text-zinc-400'}`}>
{style.category} {style.category}
</span> </span>
</div> </div>
@@ -430,7 +430,7 @@ export const StyleSelector: React.FC<StyleSelectorProps> = ({
<div className="w-3 h-3 rounded-full bg-emerald-500/20 border border-emerald-500/50"></div> <div className="w-3 h-3 rounded-full bg-emerald-500/20 border border-emerald-500/50"></div>
</div> </div>
<div className="h-4 w-px bg-zinc-800 mx-2"></div> <div className="h-4 w-px bg-zinc-800 mx-2"></div>
<span className="text-xs text-zinc-500 font-medium">Live Preview</span> <span className="text-xs text-zinc-400 font-medium">Live Preview</span>
</div> </div>
{currentStyleObj && ( {currentStyleObj && (
<span className="text-xs text-indigo-400 font-mono">{currentStyleObj.name}</span> <span className="text-xs text-indigo-400 font-mono">{currentStyleObj.name}</span>
@@ -446,7 +446,7 @@ export const StyleSelector: React.FC<StyleSelectorProps> = ({
title="Style Preview" title="Style Preview"
/> />
) : ( ) : (
<div className="absolute inset-0 flex flex-col items-center justify-center text-zinc-600"> <div className="absolute inset-0 flex flex-col items-center justify-center text-zinc-400">
<Search size={48} className="mb-4 opacity-20" /> <Search size={48} className="mb-4 opacity-20" />
<p className="text-lg font-medium">Select a style to preview</p> <p className="text-lg font-medium">Select a style to preview</p>
<p className="text-sm">Choose from the list on the left</p> <p className="text-sm">Choose from the list on the left</p>

View File

@@ -32,14 +32,14 @@
@layer base { @layer base {
:root { :root {
font-size: 16px; font-size: 100%;
} }
body { body {
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
background-color: #09090b; background-color: #09090b;
color: #e4e4e7; color: #e4e4e7;
overflow: hidden; overflow-x: hidden;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
@@ -47,7 +47,7 @@
#root { #root {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
overflow: hidden; overflow-x: hidden;
} }
} }
@@ -104,7 +104,37 @@ body ::-webkit-scrollbar-thumb:hover,
} }
.focus-ring-spacing:focus-within { .focus-ring-spacing:focus-within {
outline: 2px solid rgba(99, 102, 241, 0.3); outline: 2px solid rgba(99, 102, 241, 0.8);
outline-offset: -2px; outline-offset: -2px;
border-radius: 8px; border-radius: 8px;
} }
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Forced colors / high contrast mode */
@media (forced-colors: active) {
.focus-ring-spacing:focus-within {
outline: 2px solid LinkText;
}
}
/* Screen reader only utility */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}