diff --git a/src/App.tsx b/src/App.tsx index f0c0b4d..593bbed 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -90,6 +90,7 @@ const App: React.FC = () => { const [generatedHtml, setGeneratedHtml] = useState(''); const [error, setError] = useState(null); const [showShortcuts, setShowShortcuts] = useState(false); + const [statusMessage, setStatusMessage] = useState(''); const { uiZoom, setUiZoom, isLoaded } = useSettings(); const { templates, categories, isLoading: templatesLoading, error: templatesError, refresh, openFolder } = useTemplates(); @@ -103,6 +104,17 @@ const App: React.FC = () => { // The Tauri file drop is disabled to allow the webview to handle drag events // This preserves the drag hover animations in the FileUpload component + // Announce state changes to screen readers + useEffect(() => { + const messages: Record = { + [AppState.UPLOAD]: 'Upload screen', + [AppState.CONFIG]: 'Style configuration', + [AppState.GENERATING]: 'Generating document', + [AppState.PREVIEW]: 'Document preview', + }; + setStatusMessage(messages[appState] || ''); + }, [appState]); + // Global keydown listener for shortcuts help useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -168,7 +180,11 @@ const App: React.FC = () => { }; if (!isLoaded) { - return null; + return ( +
+ Loading TypoGenie +
+ ); } if (appState === AppState.PREVIEW) { @@ -177,6 +193,7 @@ const App: React.FC = () => { className="h-screen w-screen overflow-hidden" style={{ fontSize: `${uiZoom}%` }} > +
{statusMessage}
{ className="h-screen bg-zinc-950 text-zinc-100 flex flex-col font-sans selection:bg-indigo-500/30 overflow-hidden" style={{ fontSize: `${uiZoom}%` }} > +
{statusMessage}
{/* Keyboard Shortcuts Modal */} setShowShortcuts(false)} /> @@ -380,7 +398,6 @@ const App: React.FC = () => { exit={{ opacity: 0, y: -10, scale: 0.95 }} className="mt-4 p-4 bg-red-900/20 border border-red-800 rounded-xl text-center text-red-300" role="alert" - aria-live="polite" > {error} @@ -392,6 +409,8 @@ const App: React.FC = () => { {appState === AppState.GENERATING && ( = ({ onFileLoaded }) => { }, }, [isFocused]); - // Clear error after 5 seconds - useEffect(() => { - if (error) { - const timer = setTimeout(() => setError(null), 5000); - return () => clearTimeout(timer); - } - }, [error]); - return ( = ({ onFileLoaded }) => { role="button" tabIndex={0} aria-label="Drop zone. Click or press Enter to select a file." + aria-describedby={error ? "upload-error" : undefined} className={`relative group flex flex-col items-center justify-center w-full h-64 border-2 border-dashed rounded-2xl cursor-pointer overflow-hidden outline-none transition-all ${dragActive ? 'border-indigo-500 bg-indigo-500/10' : 'border-zinc-700 bg-zinc-900/50'} ${isFocused ? 'ring-2 ring-indigo-500 ring-offset-2 ring-offset-zinc-950 border-indigo-400' : ''} @@ -208,9 +201,9 @@ export const FileUpload: React.FC = ({ onFileLoaded }) => { initial={{ opacity: 0, y: -10, height: 0 }} animate={{ opacity: 1, y: 0, height: 'auto' }} exit={{ opacity: 0, y: -10, height: 0 }} + id="upload-error" className="mt-4 p-4 bg-red-900/20 border border-red-800 rounded-lg flex items-center gap-3 text-red-200" role="alert" - aria-live="polite" > = ({ const [successMsg, setSuccessMsg] = useState(false); const [showExportModal, setShowExportModal] = useState(false); const [focusedElement, setFocusedElement] = useState<'back' | 'fonts' | 'save'>('save'); + const [exportError, setExportError] = useState(null); // Get current style from templates const style = templates.find(s => s.id === selectedStyleId) || templates[0] || null; @@ -242,7 +243,7 @@ export const Preview: React.FC = ({ } } catch (e) { console.error("Docx Gen Error", e); - alert("Failed to generate DOCX: " + e); + setExportError("Failed to generate DOCX: " + e); } finally { setIsExporting(false); } @@ -375,7 +376,7 @@ export const Preview: React.FC = ({ }, [htmlContent, paperSize, selectedStyleId, templates, style]); if (!style) { - return
Loading...
; + return
Loading...
; } return ( @@ -399,6 +400,7 @@ export const Preview: React.FC = ({ onClick={handleSave} onFocus={() => setFocusedElement('save')} disabled={isExporting} + aria-live="polite" whileHover={{ scale: 1.05, boxShadow: '0 0 20px rgba(59, 130, 246, 0.4)' }} whileTap={{ scale: 0.95 }} className={`flex items-center gap-2 px-5 py-2.5 rounded-lg font-semibold transition-colors shadow-lg ${successMsg ? 'bg-emerald-600 text-white' : 'bg-blue-600 text-white hover:bg-blue-500'} ${isExporting ? 'opacity-50 cursor-wait' : ''}`} @@ -431,6 +433,12 @@ export const Preview: React.FC = ({ onClose={() => setShowExportModal(false)} onExport={handleExportConfirm} /> + {exportError && ( +
+ {exportError} + +
+ )}
); }; diff --git a/src/components/StyleSelector.tsx b/src/components/StyleSelector.tsx index a145d25..ba7e975 100644 --- a/src/components/StyleSelector.tsx +++ b/src/components/StyleSelector.tsx @@ -250,7 +250,7 @@ export const StyleSelector: React.FC = ({ if (isLoading) { return ( -
+

Loading templates...

@@ -261,10 +261,10 @@ export const StyleSelector: React.FC = ({ if (error) { return ( -
+
- ⚠️ +

Template Error

{error}

@@ -372,6 +372,9 @@ export const StyleSelector: React.FC = ({ 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" />
+
+ {searchQuery.trim() ? `${filteredStyles.length} template${filteredStyles.length !== 1 ? 's' : ''} found` : ''} +