a11y: fix CSS foundation - contrast, overflow, focus ring, reduced motion
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
10
src/App.tsx
10
src/App.tsx
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user