a11y: fix keyboard access, listbox pattern, ARIA labels, target sizes
This commit is contained in:
18
src/App.tsx
18
src/App.tsx
@@ -106,6 +106,10 @@ const App: React.FC = () => {
|
|||||||
// Global keydown listener for shortcuts help
|
// Global keydown listener for shortcuts help
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
const tag = (e.target as HTMLElement)?.tagName;
|
||||||
|
const isEditable = tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement)?.isContentEditable;
|
||||||
|
if (isEditable) return;
|
||||||
|
|
||||||
if (e.key === '?' || e.key === '/') {
|
if (e.key === '?' || e.key === '/') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setShowShortcuts(prev => !prev);
|
setShowShortcuts(prev => !prev);
|
||||||
@@ -206,20 +210,22 @@ const App: React.FC = () => {
|
|||||||
className="flex-none border-b border-zinc-800 bg-zinc-950/50 backdrop-blur-sm z-40"
|
className="flex-none border-b border-zinc-800 bg-zinc-950/50 backdrop-blur-sm z-40"
|
||||||
>
|
>
|
||||||
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
|
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
|
||||||
<motion.div
|
<motion.button
|
||||||
className="flex items-center gap-2 cursor-pointer"
|
className="flex items-center gap-2 cursor-pointer bg-transparent border-none"
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
|
aria-label="TypoGenie - Reset to home"
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="bg-gradient-to-br from-indigo-500 to-violet-600 p-2 rounded-lg"
|
className="bg-gradient-to-br from-indigo-500 to-violet-600 p-2 rounded-lg"
|
||||||
whileHover={{ rotate: [0, -10, 10, 0], transition: { duration: 0.5 } }}
|
whileHover={{ rotate: [0, -10, 10, 0], transition: { duration: 0.5 } }}
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<FileType className="text-white" size={20} />
|
<FileType className="text-white" size={20} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<h1 className="text-xl font-bold tracking-tight text-white">TypoGenie</h1>
|
<h1 className="text-xl font-bold tracking-tight text-white">TypoGenie</h1>
|
||||||
</motion.div>
|
</motion.button>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{/* UI Zoom Control */}
|
{/* UI Zoom Control */}
|
||||||
@@ -234,7 +240,7 @@ const App: React.FC = () => {
|
|||||||
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"
|
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} aria-hidden="true" />
|
||||||
<span>Refresh</span>
|
<span>Refresh</span>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
)}
|
)}
|
||||||
@@ -247,7 +253,7 @@ const App: React.FC = () => {
|
|||||||
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"
|
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} aria-hidden="true" />
|
||||||
<span>Shortcuts</span>
|
<span>Shortcuts</span>
|
||||||
<kbd className="px-1.5 py-0.5 bg-zinc-800 rounded text-zinc-400 font-mono">?</kbd>
|
<kbd className="px-1.5 py-0.5 bg-zinc-800 rounded text-zinc-400 font-mono">?</kbd>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
@@ -402,7 +408,7 @@ const App: React.FC = () => {
|
|||||||
animate={{ scale: [1, 1.2, 1], opacity: [0.2, 0.4, 0.2] }}
|
animate={{ scale: [1, 1.2, 1], opacity: [0.2, 0.4, 0.2] }}
|
||||||
transition={{ duration: 1.5, repeat: Infinity }}
|
transition={{ duration: 1.5, repeat: Infinity }}
|
||||||
/>
|
/>
|
||||||
<Loader2 size={64} className="text-indigo-400 relative z-10" />
|
<Loader2 size={64} className="text-indigo-400 relative z-10" aria-hidden="true" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<motion.h3
|
<motion.h3
|
||||||
className="mt-8 text-2xl font-bold text-white"
|
className="mt-8 text-2xl font-bold text-white"
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export const FileUpload: React.FC<FileUploadProps> = ({ onFileLoaded }) => {
|
|||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
whileHover={{ scale: 1.1, rotate: 5 }}
|
whileHover={{ scale: 1.1, rotate: 5 }}
|
||||||
>
|
>
|
||||||
<Upload size={32} />
|
<Upload size={32} aria-hidden="true" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<motion.p
|
<motion.p
|
||||||
className="mb-2 text-lg font-medium"
|
className="mb-2 text-lg font-medium"
|
||||||
@@ -216,7 +216,7 @@ export const FileUpload: React.FC<FileUploadProps> = ({ onFileLoaded }) => {
|
|||||||
animate={{ rotate: [0, 10, -10, 0] }}
|
animate={{ rotate: [0, 10, -10, 0] }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
>
|
>
|
||||||
<AlertCircle size={20} />
|
<AlertCircle size={20} aria-hidden="true" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<span>{error}</span>
|
<span>{error}</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -33,20 +33,20 @@ const ZoomControl: React.FC<{ zoom: number; onZoomChange: (zoom: number) => void
|
|||||||
onClick={decreaseZoom}
|
onClick={decreaseZoom}
|
||||||
whileHover={{ scale: 1.1 }}
|
whileHover={{ scale: 1.1 }}
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
className="p-1 text-zinc-400 hover:text-white transition-colors"
|
className="p-2 min-w-[44px] min-h-[44px] flex items-center justify-center text-zinc-400 hover:text-white transition-colors"
|
||||||
aria-label="Zoom out"
|
aria-label="Zoom out"
|
||||||
>
|
>
|
||||||
<ZoomOut size={16} />
|
<ZoomOut size={16} aria-hidden="true" />
|
||||||
</motion.button>
|
</motion.button>
|
||||||
<span className="text-xs font-medium text-zinc-300 min-w-[3rem] text-center">{zoom}%</span>
|
<span className="text-xs font-medium text-zinc-300 min-w-[3rem] text-center">{zoom}%</span>
|
||||||
<motion.button
|
<motion.button
|
||||||
onClick={increaseZoom}
|
onClick={increaseZoom}
|
||||||
whileHover={{ scale: 1.1 }}
|
whileHover={{ scale: 1.1 }}
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
className="p-1 text-zinc-400 hover:text-white transition-colors"
|
className="p-2 min-w-[44px] min-h-[44px] flex items-center justify-center text-zinc-400 hover:text-white transition-colors"
|
||||||
aria-label="Zoom in"
|
aria-label="Zoom in"
|
||||||
>
|
>
|
||||||
<ZoomIn size={16} />
|
<ZoomIn size={16} aria-hidden="true" />
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -96,18 +96,20 @@ const FontList: React.FC<{ fonts: string[] }> = ({ fonts }) => {
|
|||||||
disabled={downloadingFont === font}
|
disabled={downloadingFont === font}
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
|
aria-label={`Download ${font} font`}
|
||||||
className="px-2 py-1 bg-zinc-900 border border-zinc-800 hover:border-zinc-600 rounded-l text-zinc-300 hover:text-white transition-all flex items-center gap-1.5 text-xs font-medium"
|
className="px-2 py-1 bg-zinc-900 border border-zinc-800 hover:border-zinc-600 rounded-l text-zinc-300 hover:text-white transition-all flex items-center gap-1.5 text-xs font-medium"
|
||||||
>
|
>
|
||||||
{downloadingFont === font ? <Loader2 size={10} className="animate-spin" /> : <Download size={10} />}
|
{downloadingFont === font ? <Loader2 size={10} className="animate-spin" aria-hidden="true" /> : <Download size={10} aria-hidden="true" />}
|
||||||
{font}
|
{font}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
<motion.button
|
<motion.button
|
||||||
onClick={() => openGoogleFonts(font)}
|
onClick={() => openGoogleFonts(font)}
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
|
aria-label="View on Google Fonts"
|
||||||
className="px-1.5 py-1 bg-zinc-900 border border-l-0 border-zinc-800 hover:border-zinc-600 rounded-r text-zinc-400 hover:text-white transition-all"
|
className="px-1.5 py-1 bg-zinc-900 border border-l-0 border-zinc-800 hover:border-zinc-600 rounded-r text-zinc-400 hover:text-white transition-all"
|
||||||
>
|
>
|
||||||
<ExternalLink size={10} />
|
<ExternalLink size={10} aria-hidden="true" />
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
{downloadStatus[font] && (
|
{downloadStatus[font] && (
|
||||||
@@ -381,7 +383,7 @@ export const Preview: React.FC<PreviewProps> = ({
|
|||||||
<motion.div className="sticky top-0 z-50 bg-zinc-950/90 backdrop-blur-md border-b border-zinc-800 p-4" initial={{ y: -20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ duration: 0.4, ease: [0.22, 1, 0.36, 1] }}>
|
<motion.div className="sticky top-0 z-50 bg-zinc-950/90 backdrop-blur-md border-b border-zinc-800 p-4" initial={{ y: -20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ duration: 0.4, ease: [0.22, 1, 0.36, 1] }}>
|
||||||
<div className="max-w-7xl mx-auto flex flex-col sm:flex-row justify-between items-center gap-4">
|
<div className="max-w-7xl mx-auto flex flex-col sm:flex-row justify-between items-center gap-4">
|
||||||
<motion.button ref={backButtonRef} onClick={onBack} onFocus={() => setFocusedElement('back')} whileHover={{ x: -3 }} whileTap={{ scale: 0.95 }} className="flex items-center gap-2 text-zinc-400 hover:text-white transition-colors outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-zinc-950 rounded-lg px-2 py-1">
|
<motion.button ref={backButtonRef} onClick={onBack} onFocus={() => setFocusedElement('back')} whileHover={{ x: -3 }} whileTap={{ scale: 0.95 }} className="flex items-center gap-2 text-zinc-400 hover:text-white transition-colors outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-zinc-950 rounded-lg px-2 py-1">
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} aria-hidden="true" />
|
||||||
<span>Back to Editor</span>
|
<span>Back to Editor</span>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
|
|
||||||
@@ -404,12 +406,12 @@ export const Preview: React.FC<PreviewProps> = ({
|
|||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{successMsg ? (
|
{successMsg ? (
|
||||||
<motion.div key="success" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }} className="flex items-center gap-2">
|
<motion.div key="success" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }} className="flex items-center gap-2">
|
||||||
<CheckCircle2 size={18} />
|
<CheckCircle2 size={18} aria-hidden="true" />
|
||||||
<span>Saved!</span>
|
<span>Saved!</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
<motion.div key="default" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }} className="flex items-center gap-2">
|
<motion.div key="default" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }} className="flex items-center gap-2">
|
||||||
{isExporting ? <Loader2 size={18} className="animate-spin" /> : <FileText size={18} />}
|
{isExporting ? <Loader2 size={18} className="animate-spin" aria-hidden="true" /> : <FileText size={18} aria-hidden="true" />}
|
||||||
<span>{isExporting ? 'Generating...' : 'Save Word Doc'}</span>
|
<span>{isExporting ? 'Generating...' : 'Save Word Doc'}</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ export const StyleSelector: React.FC<StyleSelectorProps> = ({
|
|||||||
{/* Top Controls */}
|
{/* Top Controls */}
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 px-1">
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 px-1">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-2 bg-indigo-500/10 rounded-lg text-indigo-400">
|
<div className="p-2 bg-indigo-500/10 rounded-lg text-indigo-400" aria-hidden="true">
|
||||||
<Type size={24} />
|
<Type size={24} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -290,14 +290,15 @@ export const StyleSelector: React.FC<StyleSelectorProps> = ({
|
|||||||
|
|
||||||
<div className="flex items-center gap-4 bg-zinc-900/50 p-1.5 rounded-xl border border-zinc-800/50">
|
<div className="flex items-center gap-4 bg-zinc-900/50 p-1.5 rounded-xl border border-zinc-800/50">
|
||||||
<div className="flex items-center gap-2 text-zinc-400 px-3">
|
<div className="flex items-center gap-2 text-zinc-400 px-3">
|
||||||
<Printer size={16} />
|
<Printer size={16} aria-hidden="true" />
|
||||||
<span className="text-xs font-semibold uppercase tracking-wider">Format</span>
|
<span className="text-xs font-semibold uppercase tracking-wider">Format</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1" role="group" aria-label="Paper size">
|
||||||
{(['Letter', 'A4'] as PaperSize[]).map((size) => (
|
{(['Letter', 'A4'] as PaperSize[]).map((size) => (
|
||||||
<button
|
<button
|
||||||
key={size}
|
key={size}
|
||||||
onClick={() => onSelectPaperSize(size)}
|
onClick={() => onSelectPaperSize(size)}
|
||||||
|
aria-pressed={selectedPaperSize === 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-400 hover:text-zinc-300 hover:bg-zinc-800'
|
: 'text-zinc-400 hover:text-zinc-300 hover:bg-zinc-800'
|
||||||
@@ -319,20 +320,22 @@ export const StyleSelector: React.FC<StyleSelectorProps> = ({
|
|||||||
{/* Category Filter Tabs */}
|
{/* Category Filter Tabs */}
|
||||||
<div className="flex flex-col border-b border-zinc-800/50 bg-zinc-900/20">
|
<div className="flex flex-col border-b border-zinc-800/50 bg-zinc-900/20">
|
||||||
<div className="p-4 overflow-x-auto no-scrollbar pb-2">
|
<div className="p-4 overflow-x-auto no-scrollbar pb-2">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2" role="group" aria-label="Filter by category">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveCategory('fav')}
|
onClick={() => setActiveCategory('fav')}
|
||||||
|
aria-pressed={activeCategory === 'fav'}
|
||||||
className={`px-3 py-1.5 rounded-full text-xs font-semibold whitespace-nowrap transition-all flex items-center gap-1.5 ${activeCategory === 'fav'
|
className={`px-3 py-1.5 rounded-full text-xs font-semibold whitespace-nowrap transition-all flex items-center gap-1.5 ${activeCategory === 'fav'
|
||||||
? 'bg-rose-500 text-white shadow-lg shadow-rose-500/20'
|
? 'bg-rose-500 text-white shadow-lg shadow-rose-500/20'
|
||||||
: 'bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200'
|
: 'bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Heart size={12} className={activeCategory === 'fav' ? 'fill-current' : ''} />
|
<Heart size={12} className={activeCategory === 'fav' ? 'fill-current' : ''} aria-hidden="true" />
|
||||||
fav
|
fav
|
||||||
</button>
|
</button>
|
||||||
<div className="w-px h-6 bg-zinc-800 mx-1 self-center" />
|
<div className="w-px h-6 bg-zinc-800 mx-1 self-center" />
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveCategory('All')}
|
onClick={() => setActiveCategory('All')}
|
||||||
|
aria-pressed={activeCategory === 'All'}
|
||||||
className={`px-3 py-1.5 rounded-full text-xs font-semibold whitespace-nowrap transition-all ${activeCategory === 'All'
|
className={`px-3 py-1.5 rounded-full text-xs font-semibold whitespace-nowrap transition-all ${activeCategory === 'All'
|
||||||
? 'bg-white text-black'
|
? 'bg-white text-black'
|
||||||
: 'bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200'
|
: 'bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200'
|
||||||
@@ -344,6 +347,7 @@ export const StyleSelector: React.FC<StyleSelectorProps> = ({
|
|||||||
<button
|
<button
|
||||||
key={cat}
|
key={cat}
|
||||||
onClick={() => setActiveCategory(cat)}
|
onClick={() => setActiveCategory(cat)}
|
||||||
|
aria-pressed={activeCategory === cat}
|
||||||
className={`px-3 py-1.5 rounded-full text-xs font-semibold whitespace-nowrap transition-all ${activeCategory === cat
|
className={`px-3 py-1.5 rounded-full text-xs font-semibold whitespace-nowrap transition-all ${activeCategory === cat
|
||||||
? 'bg-indigo-500 text-white shadow-lg shadow-indigo-500/20'
|
? 'bg-indigo-500 text-white shadow-lg shadow-indigo-500/20'
|
||||||
: 'bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200'
|
: 'bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200'
|
||||||
@@ -358,12 +362,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-400 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" aria-hidden="true" />
|
||||||
<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)}
|
||||||
|
aria-label="Search templates"
|
||||||
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"
|
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>
|
||||||
@@ -371,7 +376,12 @@ export const StyleSelector: React.FC<StyleSelectorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 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"
|
||||||
|
role="listbox"
|
||||||
|
aria-label="Typography styles"
|
||||||
|
aria-activedescendant={selectedStyle ? `style-${selectedStyle}` : undefined}
|
||||||
|
>
|
||||||
{filteredStyles.length === 0 ? (
|
{filteredStyles.length === 0 ? (
|
||||||
<div className="text-center py-8 text-zinc-400">
|
<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>
|
||||||
@@ -379,8 +389,18 @@ export const StyleSelector: React.FC<StyleSelectorProps> = ({
|
|||||||
) : filteredStyles.map((style) => (
|
) : filteredStyles.map((style) => (
|
||||||
<div
|
<div
|
||||||
key={style.id}
|
key={style.id}
|
||||||
|
id={`style-${style.id}`}
|
||||||
|
role="option"
|
||||||
|
aria-selected={selectedStyle === style.id}
|
||||||
|
tabIndex={selectedStyle === style.id ? 0 : -1}
|
||||||
onClick={() => onSelectStyle(style.id)}
|
onClick={() => onSelectStyle(style.id)}
|
||||||
className={`p-4 rounded-xl border transition-all cursor-pointer group relative
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
onSelectStyle(style.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`p-4 rounded-xl border transition-all cursor-pointer group relative outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950
|
||||||
${selectedStyle === style.id
|
${selectedStyle === style.id
|
||||||
? 'border-indigo-500 bg-indigo-500/10 shadow-[0_0_15px_rgba(99,102,241,0.15)]'
|
? 'border-indigo-500 bg-indigo-500/10 shadow-[0_0_15px_rgba(99,102,241,0.15)]'
|
||||||
: 'border-zinc-800 bg-zinc-900/40 hover:border-zinc-700 hover:bg-zinc-800'}`}
|
: 'border-zinc-800 bg-zinc-900/40 hover:border-zinc-700 hover:bg-zinc-800'}`}
|
||||||
@@ -392,15 +412,17 @@ export const StyleSelector: React.FC<StyleSelectorProps> = ({
|
|||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => toggleFavorite(e, style.id)}
|
onClick={(e) => toggleFavorite(e, style.id)}
|
||||||
className={`p-1.5 rounded-full transition-all ${favorites.includes(style.id)
|
aria-label={favorites.includes(style.id) ? `Remove ${style.name} from favorites` : `Add ${style.name} to favorites`}
|
||||||
|
aria-pressed={favorites.includes(style.id)}
|
||||||
|
className={`min-w-[44px] min-h-[44px] flex items-center justify-center 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-400 hover:text-zinc-300 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' : ''} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
{selectedStyle === style.id && (
|
{selectedStyle === style.id && (
|
||||||
<div className="bg-indigo-500 rounded-full p-0.5">
|
<div className="bg-indigo-500 rounded-full p-0.5" aria-hidden="true">
|
||||||
<Check size={12} className="text-white" />
|
<Check size={12} className="text-white" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -444,10 +466,11 @@ export const StyleSelector: React.FC<StyleSelectorProps> = ({
|
|||||||
ref={iframeRef}
|
ref={iframeRef}
|
||||||
className="w-full h-full border-0 block"
|
className="w-full h-full border-0 block"
|
||||||
title="Style Preview"
|
title="Style Preview"
|
||||||
|
tabIndex={-1}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center text-zinc-400">
|
<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" aria-hidden="true" />
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
@@ -462,7 +485,7 @@ export const StyleSelector: React.FC<StyleSelectorProps> = ({
|
|||||||
className="flex items-center gap-2 px-6 py-3 bg-indigo-600 hover:bg-indigo-500 text-white font-bold rounded-xl transition-all shadow-lg hover:shadow-indigo-500/25 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="flex items-center gap-2 px-6 py-3 bg-indigo-600 hover:bg-indigo-500 text-white font-bold rounded-xl transition-all shadow-lg hover:shadow-indigo-500/25 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<span>Apply Style & Convert</span>
|
<span>Apply Style & Convert</span>
|
||||||
<Printer size={18} />
|
<Printer size={18} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user