Compare commits
23 Commits
v1.1.0
..
5b732a2175
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b732a2175 | |||
| c97a44a56a | |||
| ff9a73819f | |||
| 09bd5767b4 | |||
| 71b2ef418f | |||
| bb10a14a56 | |||
| a7759b5bc2 | |||
| 7cf45f56c9 | |||
| c14dde64a5 | |||
| 759f52274b | |||
| 4c806d40b8 | |||
| aff9b7301b | |||
| 68498a2374 | |||
| 6a7e167051 | |||
| 9ac6a4712a | |||
| 46316c7efb | |||
| 3ca018ba5a | |||
| b18bbb3d77 | |||
| 74f033d5e8 | |||
| ae3a339db6 | |||
| a3b566528a | |||
| cc51c5a236 | |||
| 6d35d5e95e |
@@ -1,212 +0,0 @@
|
|||||||
# WCAG 2.2 AAA Accessibility Remediation Design
|
|
||||||
|
|
||||||
**Date:** 2026-02-18
|
|
||||||
**Status:** Approved
|
|
||||||
**Scope:** 147 accessibility issues across 17 existing files, 3 new files
|
|
||||||
|
|
||||||
## Decisions
|
|
||||||
|
|
||||||
| Decision | Choice |
|
|
||||||
|----------|--------|
|
|
||||||
| Visual approach | Balance accessibility with dark aesthetic |
|
|
||||||
| Modals | Native `<dialog>` element |
|
|
||||||
| Style cards | Listbox pattern with existing `useFocusableList` hook |
|
|
||||||
| Template colors | Runtime contrast validation only, no JSON template edits |
|
|
||||||
| Justified text | Override to `left` at render time |
|
|
||||||
| Contrast enforcement | Auto-correct via `ensureContrast()` utility at render time |
|
|
||||||
|
|
||||||
## Section 1: CSS Foundation (~25 issues)
|
|
||||||
|
|
||||||
### Root font size
|
|
||||||
Change `:root { font-size: 16px }` to `font-size: 100%` in `index.css` so user browser preferences are respected.
|
|
||||||
|
|
||||||
### Overflow
|
|
||||||
Remove `overflow: hidden` from `body` and `#root`. Replace with `overflow-x: hidden` to prevent horizontal scroll but allow vertical content access when zoomed to 200%.
|
|
||||||
|
|
||||||
### Focus indicators
|
|
||||||
Replace `rgba(99, 102, 241, 0.3)` focus ring with `rgba(99, 102, 241, 0.8)` at 2px. Meets 3:1 contrast on dark backgrounds while fitting indigo theme.
|
|
||||||
|
|
||||||
### Reduced motion
|
|
||||||
Add global CSS rule:
|
|
||||||
```css
|
|
||||||
@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
|
|
||||||
Add `@media (forced-colors: active)` rules for Windows High Contrast Mode compatibility.
|
|
||||||
|
|
||||||
### Color replacements
|
|
||||||
Across all components: `text-zinc-600` -> `text-zinc-400`, `text-zinc-500` -> `text-zinc-400`. All text hits 7:1+ contrast on zinc-950. Keeps dark aesthetic.
|
|
||||||
|
|
||||||
## Section 2: Modal System (~20 issues)
|
|
||||||
|
|
||||||
### New `src/hooks/useDialog.ts`
|
|
||||||
Reusable hook wrapping native `<dialog>`:
|
|
||||||
- Manages `dialogRef`, `showModal()`/`close()`
|
|
||||||
- Restores focus to trigger element on close
|
|
||||||
- Wires `aria-labelledby`
|
|
||||||
- Framer Motion animations on inner content div
|
|
||||||
|
|
||||||
### KeyboardShortcutsHelp (App.tsx)
|
|
||||||
Convert from `motion.div` overlay to `<dialog>`. Add `aria-labelledby` to heading. Close button gets `aria-label="Close shortcuts"`. Escape/focus trap handled natively.
|
|
||||||
|
|
||||||
### ExportOptionsModal
|
|
||||||
Same `<dialog>` conversion. Radio buttons wrapped in `<fieldset><legend>`. Close button labeled.
|
|
||||||
|
|
||||||
### StylePreviewModal
|
|
||||||
Same `<dialog>` conversion. Close button labeled. Iframe HTML gets `lang="en"`.
|
|
||||||
|
|
||||||
### Animation approach
|
|
||||||
`dialog[open]` CSS selector for entrance. `::backdrop` gets fade-in. Framer Motion on inner content for exit.
|
|
||||||
|
|
||||||
## Section 3: Keyboard Access & Interactive Elements (~20 issues)
|
|
||||||
|
|
||||||
### Logo button
|
|
||||||
Replace `<motion.div onClick>` with `<motion.button aria-label="TypoGenie - Reset to home">`.
|
|
||||||
|
|
||||||
### Single-character shortcut
|
|
||||||
Scope `?`/`/` listener to only fire when `document.activeElement` is body or non-input element. Prevents intercepting typing in search.
|
|
||||||
|
|
||||||
### Style cards as listbox
|
|
||||||
Container: `role="listbox"` + `aria-label="Typography styles"`. Each card: `role="option"` + `tabIndex` + `aria-selected` + `onKeyDown`. Wire `useFocusableList` for arrow keys. Add `:focus-visible` indigo ring.
|
|
||||||
|
|
||||||
### Favorite button
|
|
||||||
Add `aria-label` (dynamic: "Add to favorites"/"Remove from favorites") + `aria-pressed`.
|
|
||||||
|
|
||||||
### Category filters
|
|
||||||
Wrap in `<div role="group" aria-label="Filter by category">`. Active button: `aria-pressed="true"`.
|
|
||||||
|
|
||||||
### Paper size buttons
|
|
||||||
Wrap in `<div role="group" aria-label="Paper size">`. Active button: `aria-pressed="true"`.
|
|
||||||
|
|
||||||
### Search input
|
|
||||||
Add `aria-label="Search templates"` + `aria-describedby` pointing to live result count region.
|
|
||||||
|
|
||||||
### Icon-only buttons
|
|
||||||
Add `aria-label` to all: zoom buttons, external link (Preview), close buttons. Decorative icons next to text: `aria-hidden="true"`.
|
|
||||||
|
|
||||||
### Target sizes
|
|
||||||
Increase padding on small buttons to hit 44x44px minimum. Use `min-w-[44px] min-h-[44px]` with centered content where inline growth not possible.
|
|
||||||
|
|
||||||
## Section 4: ARIA Live Regions & Status Announcements (~15 issues)
|
|
||||||
|
|
||||||
### Root status region
|
|
||||||
Persistent `<div role="status" aria-live="polite" className="sr-only">` in App.tsx root. Updated on `appState` changes.
|
|
||||||
|
|
||||||
### Generating state
|
|
||||||
Add `aria-busy="true"` + `role="status"` to container.
|
|
||||||
|
|
||||||
### Preview loading
|
|
||||||
Wrap "Loading..." in `role="status"`.
|
|
||||||
|
|
||||||
### Success message
|
|
||||||
`aria-live="polite"` on button text area for "Saved!" announcement.
|
|
||||||
|
|
||||||
### Search results
|
|
||||||
Visually-hidden `role="status"` announcing "{N} templates found". Debounced 300ms.
|
|
||||||
|
|
||||||
### Error fixes
|
|
||||||
- Remove `aria-live="polite"` where `role="alert"` already set (implies assertive)
|
|
||||||
- Remove 5-second auto-dismiss from FileUpload error
|
|
||||||
- Add `aria-describedby` linking FileUpload error to dropzone
|
|
||||||
- Template loading: `role="status"`. Template error: `role="alert"`
|
|
||||||
|
|
||||||
### Loading state
|
|
||||||
Replace `return null` with minimal `<div role="status">Loading TypoGenie</div>`.
|
|
||||||
|
|
||||||
## Section 5: Semantic Structure & Landmarks (~15 issues)
|
|
||||||
|
|
||||||
### Skip navigation
|
|
||||||
`<a href="#main-content" class="sr-only focus:not-sr-only">Skip to main content</a>` in index.html.
|
|
||||||
|
|
||||||
### Page title
|
|
||||||
`<title>TypoGenie - Markdown to Word Converter</title>`
|
|
||||||
|
|
||||||
### Noscript
|
|
||||||
`<noscript><p>TypoGenie requires JavaScript to run.</p></noscript>`
|
|
||||||
|
|
||||||
### Landmarks
|
|
||||||
- `id="main-content"` on `<main>` in both app states
|
|
||||||
- `<nav aria-label="Style filters">` around category buttons
|
|
||||||
- `<section aria-label="Style list">` and `<section aria-label="Style preview">` in StyleSelector
|
|
||||||
|
|
||||||
### Progress stepper
|
|
||||||
Wrap in `<nav aria-label="Progress"><ol>`. Active step: `aria-current="step"`.
|
|
||||||
|
|
||||||
### Shortcuts list
|
|
||||||
Convert to `<dl>` with `<dt>`/`<dd>`.
|
|
||||||
|
|
||||||
### Decorative elements
|
|
||||||
`aria-hidden="true"` on blob backgrounds, window-chrome dots, visual dividers.
|
|
||||||
|
|
||||||
### Iframe lang
|
|
||||||
Both Preview.tsx and StylePreviewModal.tsx: inject `<html lang="en">`.
|
|
||||||
|
|
||||||
## Section 6: DOCX Output Accessibility (~8 issues)
|
|
||||||
|
|
||||||
### Document metadata
|
|
||||||
Add `title` (from filename/first h1), `description`, `language` to Document constructor.
|
|
||||||
|
|
||||||
### Image handling
|
|
||||||
New `<img>` handler producing `[Image: {alt text}]` placeholder paragraph in italic.
|
|
||||||
|
|
||||||
### Table headers
|
|
||||||
Set `tableHeader: true` on `TableRow` containing `<th>` elements.
|
|
||||||
|
|
||||||
### Heading preservation in table mode
|
|
||||||
Add `HeadingLevel` to paragraph inside `processHeaderAsTable` table cells.
|
|
||||||
|
|
||||||
### Code block style
|
|
||||||
Add custom "Code" paragraph style to DOCX styles. Apply to code blocks.
|
|
||||||
|
|
||||||
## Section 7: Template Contrast Validation (~20 issues)
|
|
||||||
|
|
||||||
### New `src/utils/contrastUtils.ts`
|
|
||||||
- `hexToRgb(hex)` - parse hex to RGB
|
|
||||||
- `relativeLuminance(rgb)` - WCAG luminance formula
|
|
||||||
- `contrastRatio(color1, color2)` - ratio calculation
|
|
||||||
- `ensureContrast(fg, bg, minRatio)` - auto-correct foreground color until ratio met
|
|
||||||
|
|
||||||
### Integration in templateRenderer.ts
|
|
||||||
Run `ensureContrast()` on every text/background pair at CSS generation time. Body text: 7:1. Large text (18pt+ or 14pt+ bold): 4.5:1.
|
|
||||||
|
|
||||||
### `del` element fix
|
|
||||||
When resolving `del` color, if `border` palette color fails contrast, fall back to `textSecondary`.
|
|
||||||
|
|
||||||
### Justified text
|
|
||||||
Override `justify` to `left` at render time (1.4.8 AAA prohibits justified text).
|
|
||||||
|
|
||||||
### Line-height floor
|
|
||||||
Enforce minimum 1.5 for body text, 1.0 for headings. Clamp upward if template specifies lower.
|
|
||||||
|
|
||||||
## Section 8: Remaining Miscellaneous (~10 issues)
|
|
||||||
|
|
||||||
### Iframe keyboard
|
|
||||||
`tabIndex={-1}` on preview iframe in StyleSelector (passive preview, not interactive).
|
|
||||||
|
|
||||||
### Error boundary
|
|
||||||
New `src/components/ErrorBoundary.tsx` wrapping `<App />`. Renders accessible error with `role="alert"` on crash.
|
|
||||||
|
|
||||||
### Framer Motion reduced motion
|
|
||||||
Import `useReducedMotion` from `motion/react` in App.tsx. When true, disable infinite blob/gradient animations via conditional animate props.
|
|
||||||
|
|
||||||
### Hidden buttons on small screens
|
|
||||||
Add always-visible `<kbd>?</kbd>` hint regardless of breakpoint.
|
|
||||||
|
|
||||||
### Truncation
|
|
||||||
Add `title` attribute with full description on `line-clamp-2` elements.
|
|
||||||
|
|
||||||
## Files Modified (17)
|
|
||||||
|
|
||||||
`index.html`, `src/index.css`, `src/App.tsx`, `src/main.tsx`, `src/components/StyleSelector.tsx`, `src/components/Preview.tsx`, `src/components/ExportOptionsModal.tsx`, `src/components/StylePreviewModal.tsx`, `src/components/FileUpload.tsx`, `src/hooks/useSettings.ts`, `src/hooks/useTemplates.ts`, `src/services/templateRenderer.ts`, `src/utils/docxConverter.ts`, `src/utils/fontUtils.ts`
|
|
||||||
|
|
||||||
## Files Created (3)
|
|
||||||
|
|
||||||
`src/utils/contrastUtils.ts`, `src/hooks/useDialog.ts`, `src/components/ErrorBoundary.tsx`
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
|||||||
"id": "creative-portfolio",
|
"id": "creative-portfolio",
|
||||||
"name": "Creative Portfolio",
|
"name": "Creative Portfolio",
|
||||||
"category": "creative",
|
"category": "creative",
|
||||||
"description": "Minimalist gallery style for showcasing visual work. Focuses on images and clean typography.",
|
"description": "Minimalist gallery style for presenting visual work. Focuses on images and clean typography.",
|
||||||
"vibe": "Minimalist, Visual, Gallery",
|
"vibe": "Minimalist, Visual, Gallery",
|
||||||
"googleFontsImport": "https://fonts.googleapis.com/css2?family=Jost:ital,wght@0,300..900;1,300..900&family=Playfair+Display:ital,wght@0,400..900;1,400..900&display=swap",
|
"googleFontsImport": "https://fonts.googleapis.com/css2?family=Jost:ital,wght@0,300..900;1,300..900&family=Playfair+Display:ital,wght@0,400..900;1,400..900&display=swap",
|
||||||
"typography": {
|
"typography": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"id": "rococo-romance",
|
"id": "rococo-romance",
|
||||||
"name": "Rococo Romance",
|
"name": "Rococo Romance",
|
||||||
"category": "editorial",
|
"category": "editorial",
|
||||||
"description": "Inspired by 18th-century Rococo. Light, playful, and intricate with pastel tones and swirling elegance.",
|
"description": "Inspired by 18th-century Rococo. Light, playful, and delicate with pastel tones and swirling elegance.",
|
||||||
"vibe": "Romantic, Ornate, Pastel",
|
"vibe": "Romantic, Ornate, Pastel",
|
||||||
"googleFontsImport": "https://fonts.googleapis.com/css2?family=Pinyon+Script&family=Playfair+Display+SC:ital,wght@0,400;0,700;1,400;1,700&family=Lora:ital,wght@0,400..700;1,400..700&display=swap",
|
"googleFontsImport": "https://fonts.googleapis.com/css2?family=Pinyon+Script&family=Playfair+Display+SC:ital,wght@0,400;0,700;1,400;1,700&family=Lora:ital,wght@0,400..700;1,400..700&display=swap",
|
||||||
"typography": {
|
"typography": {
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"id": "artificial-intelligence",
|
"id": "neural-network",
|
||||||
"name": "Neural Network",
|
"name": "Neural Network",
|
||||||
"category": "tech",
|
"category": "tech",
|
||||||
"description": "Cutting-edge design for advanced computing and data science. Neural network inspired with gradient accents.",
|
"description": "Cutting-edge design for advanced computing and data science. Neural network inspired with gradient accents.",
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"id": "tech-startup",
|
"id": "tech-startup",
|
||||||
"name": "Tech Venture",
|
"name": "Tech Venture",
|
||||||
"category": "tech",
|
"category": "tech",
|
||||||
"description": "Modern, energetic design for technology companies and ventures. Bold gradients, vibrant colors, and confident typography.",
|
"description": "Modern, energetic design for technology companies and ventures. Bold gradients, punchy colors, and confident typography.",
|
||||||
"vibe": "Innovative, Dynamic, Bold",
|
"vibe": "Innovative, Dynamic, Bold",
|
||||||
"googleFontsImport": "https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;700&display=swap",
|
"googleFontsImport": "https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;700&display=swap",
|
||||||
"typography": {
|
"typography": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"id": "memphis-pattern",
|
"id": "memphis-pattern",
|
||||||
"name": "Memphis Pattern",
|
"name": "Memphis Pattern",
|
||||||
"category": "retro",
|
"category": "retro",
|
||||||
"description": "The chaotic 80s aesthetic. Clashing patterns, zig-zags, and vibrant pastel-neons.",
|
"description": "The chaotic 80s aesthetic. Clashing patterns, zig-zags, and loud pastel-neons.",
|
||||||
"vibe": "Playful, Chaotic, 80s",
|
"vibe": "Playful, Chaotic, 80s",
|
||||||
"googleFontsImport": "https://fonts.googleapis.com/css2?family=Righteous&family=Comfortaa:wght@400;700&display=swap",
|
"googleFontsImport": "https://fonts.googleapis.com/css2?family=Righteous&family=Comfortaa:wght@400;700&display=swap",
|
||||||
"typography": {
|
"typography": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"id": "mid-century-modern",
|
"id": "mid-century-modern",
|
||||||
"name": "Mid-Century Modern",
|
"name": "Mid-Century Modern",
|
||||||
"category": "retro",
|
"category": "retro",
|
||||||
"description": "Optimistic and playful design from the 1950s-60s. Geometric sans-serifs, vibrant atomic-age colors, and approachable warmth.",
|
"description": "Optimistic and playful design from the 1950s-60s. Geometric sans-serifs, bold atomic-age colors, and approachable warmth.",
|
||||||
"vibe": "Playful, Nostalgic, Friendly",
|
"vibe": "Playful, Nostalgic, Friendly",
|
||||||
"googleFontsImport": "https://fonts.googleapis.com/css2?family=Quicksand:wght@400;600;700&display=swap",
|
"googleFontsImport": "https://fonts.googleapis.com/css2?family=Quicksand:wght@400;600;700&display=swap",
|
||||||
"typography": {
|
"typography": {
|
||||||
|
|||||||
Reference in New Issue
Block a user