From 9becfc4574c1458ae5681f779086a5149ff83042 Mon Sep 17 00:00:00 2001 From: janakhpon Date: Sun, 13 Sep 2026 11:06:40 +0700 Subject: [PATCH 01/17] fix(web): icons render as literal text, not glyphs (missing font-family) .material-symbols-outlined only set font-variation-settings; nothing ever told it to actually use the Material Symbols font. The Google Fonts link in +layout.svelte was loading correctly the whole time -- an earlier read of this bug (mine included) wrongly assumed a CDN-timing/FOUC race. It isn't: this is deterministic and reproduces on every load, on any connection, which is what four independent role-based audits converged on after checking the actual rule in app.css rather than guessing from the symptom. Confirmed in the compiled output, not just the source: built CSS now reads font-family:Material Symbols Outlined on the rule. Also removed docs/+page.svelte's local scoped override of the same selector -- it never set font-family either, so it was already dead (the global rule's !important on font-variation-settings dominated it), and keeping it only would have invited a future edit to 'fix' a rule with no visible effect either way. Verified: type-check clean (1507 files, 0 errors), lint clean, vitest 172/172, build succeeds, and the fix confirmed present in the actual built CSS asset -- not just inferred from source. --- apps/web/src/app.css | 1 + apps/web/src/routes/docs/+page.svelte | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/web/src/app.css b/apps/web/src/app.css index 95af672..a2fd31a 100644 --- a/apps/web/src/app.css +++ b/apps/web/src/app.css @@ -376,6 +376,7 @@ label { /* Material Symbols - Minimalist & Elegant */ .material-symbols-outlined { + font-family: 'Material Symbols Outlined'; font-variation-settings: 'FILL' 0, 'wght' 300, diff --git a/apps/web/src/routes/docs/+page.svelte b/apps/web/src/routes/docs/+page.svelte index 7914acc..64fe604 100644 --- a/apps/web/src/routes/docs/+page.svelte +++ b/apps/web/src/routes/docs/+page.svelte @@ -587,12 +587,6 @@ monocr batch ./scans/ --output results.txt margin: 0; } - .material-symbols-outlined { - font-variation-settings: - 'wght' 300, - 'opsz' 20; - } - @media (max-width: 1023px) { .scroll-mt-32 { scroll-margin-top: 6rem; From 46d3f7f7d8bd65d9a927ec7ebf3bf6ca4493d1d4 Mon Sep 17 00:00:00 2001 From: janakhpon Date: Sun, 13 Sep 2026 11:50:06 +0700 Subject: [PATCH 02/17] fix(a11y): modal Escape-to-close was dead code in all three modals Each modal's Escape handler lived on the backdrop div -- a sibling, not an ancestor, of the focus-trapped content. Once focusTrap moves focus inside on open (which it always does), a keydown on the backdrop can never fire: events bubble through ancestors only. Fixed once, in focus-trap.ts itself, via an optional onEscape callback wired into the action -- every consumer inherits correct behavior from one place instead of re-implementing (and mis-wiring) it per modal. Also added role="dialog" aria-modal="true" aria-labelledby to all three modals (ConfirmationModal, SuccessModal, HistorySection's record viewer), and aria-hidden="true" on their decorative icons -- screen readers previously got no announcement that a dialog opened, or what it was called, and would speak raw icon-ligature text next to labels that already state the same thing. Verified: type-check clean, lint clean, vitest 172/172, build succeeds. --- apps/web/src/lib/actions/focus-trap.ts | 19 +++++++++++++- .../lib/components/ConfirmationModal.svelte | 25 ++++++++++++------- .../src/lib/components/HistorySection.svelte | 23 +++++++++-------- .../src/lib/components/ui/SuccessModal.svelte | 19 ++++++++------ 4 files changed, 58 insertions(+), 28 deletions(-) diff --git a/apps/web/src/lib/actions/focus-trap.ts b/apps/web/src/lib/actions/focus-trap.ts index 6f9d322..80e41bb 100644 --- a/apps/web/src/lib/actions/focus-trap.ts +++ b/apps/web/src/lib/actions/focus-trap.ts @@ -6,8 +6,17 @@ * - Focus Restoration: Returns focus to previous element on destroy. * - Dynamic Support: Uses MutationObserver to handle content changes. * - Safety: Guarded against empty sets. + * - Escape-to-close: an optional `onEscape` callback, called on Escape. + * + * Escape handling lives here, not on each modal's backdrop element. Focus + * moves inside this node as soon as the trap activates, so a keydown + * listener on a backdrop `
` — a sibling, not an ancestor, of this node + * — never receives the event once the trap is live. Every caller gets + * correct Escape behavior from this one place instead of each modal + * re-implementing (and mis-wiring) it. */ -export function focusTrap(node: HTMLElement) { +export function focusTrap(node: HTMLElement, params?: { onEscape?: () => void }) { + let onEscape = params?.onEscape; let focusableElements: HTMLElement[] = []; let firstElement: HTMLElement | undefined; let lastElement: HTMLElement | undefined; @@ -23,6 +32,11 @@ export function focusTrap(node: HTMLElement) { } function handleKeydown(e: KeyboardEvent) { + if (e.key === 'Escape') { + onEscape?.(); + return; + } + if (e.key !== 'Tab' || focusableElements.length === 0) return; if (e.shiftKey) { @@ -59,6 +73,9 @@ export function focusTrap(node: HTMLElement) { } return { + update(newParams?: { onEscape?: () => void }) { + onEscape = newParams?.onEscape; + }, destroy() { node.removeEventListener('keydown', handleKeydown); observer.disconnect(); diff --git a/apps/web/src/lib/components/ConfirmationModal.svelte b/apps/web/src/lib/components/ConfirmationModal.svelte index f2e6dca..d67374a 100644 --- a/apps/web/src/lib/components/ConfirmationModal.svelte +++ b/apps/web/src/lib/components/ConfirmationModal.svelte @@ -30,32 +30,39 @@ in:fade={{ duration: 200 }} out:fade={{ duration: 150 }} > - +
e.key === 'Escape' && onCancel()} - role="button" - tabindex="-1" + aria-hidden="true" >
@@ -189,17 +187,20 @@ in:fade={{ duration: 200 }} out:fade={{ duration: 200 }} > +
e.key === 'Escape' && closeRecordView()} + aria-hidden="true" >
{#if processingTime > 0 && !loading && resultText}
{resultText @@ -491,7 +494,7 @@
@@ -271,7 +271,7 @@ text = ocr.predict('manuscript.jpg')
-

+

{m.docs_standards_dpi_title()}

@@ -281,7 +281,7 @@ text = ocr.predict('manuscript.jpg')

-

+

{m.docs_standards_light_title()}

@@ -333,7 +333,7 @@ text = ocr.predict('manuscript.jpg')

{sdks.find((s) => s.id === selectedSdk)?.pkg}
@@ -421,7 +421,8 @@ text, _ := engine.Predict(
- Terminal
From 0f2a9090ebf28ca12cecbb1c3516777a7dc21e66 Mon Sep 17 00:00:00 2001 From: janakhpon Date: Sun, 13 Sep 2026 12:07:44 +0700 Subject: [PATCH 08/17] fix(a11y): 3 contrast failures a fourth verification pass found - HistorySection.svelte's 'Retry' link (real actionable text on a failed sync record) sat at opacity-60 against its row background, computing to 4.49:1 idle / 4.46:1 on hover -- just under the 4.5:1 AA line in the opposite direction from a boundary case checked earlier this branch. Removed the opacity (and the now-meaningless hover:opacity-100/ transition-opacity that existed only to compensate for it). - contribute/+page.svelte's Mon-script textarea and report/+page.svelte's corrected-text textarea both used placeholder:text-fg-muted/30 and /40 -- computing to ~1.49:1 and ~1.72:1. Same opacity-stacked-muted-text bug already fixed elsewhere on this branch, just on the placeholder: variant, which the earlier greps for plain opacity-NN and non-placeholder text-*/NN patterns didn't catch. Also corrects a false claim in the immediately prior commit's message: it said contribute's OR-divider had 'an existing asymmetry' between its two border lines. Checked full file history this time -- there wasn't one; both lines always shared one wrapping div's opacity equally. The fix itself (splitting the opacity onto each line individually) was still correct, only that one sentence describing prior state was wrong. Verified: type-check clean, lint clean, vitest 172/172, build succeeds. --- apps/web/src/lib/components/HistorySection.svelte | 2 +- apps/web/src/routes/contribute/+page.svelte | 2 +- apps/web/src/routes/report/+page.svelte | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/lib/components/HistorySection.svelte b/apps/web/src/lib/components/HistorySection.svelte index cca441e..8f5a5c2 100644 --- a/apps/web/src/lib/components/HistorySection.svelte +++ b/apps/web/src/lib/components/HistorySection.svelte @@ -126,7 +126,7 @@ await syncService.syncAll(); await loadHistory(); }} - class="text-primary hover:text-primary/80 focus-ring ml-1 rounded-sm px-1 text-[9px] font-bold tracking-tighter uppercase underline underline-offset-2 opacity-60 transition-opacity hover:opacity-100" + class="text-primary hover:text-primary/80 focus-ring ml-1 rounded-sm px-1 text-[9px] font-bold tracking-tighter uppercase underline underline-offset-2 transition-colors" > {m.history_retry()} diff --git a/apps/web/src/routes/contribute/+page.svelte b/apps/web/src/routes/contribute/+page.svelte index 2df65a0..c7fe96f 100644 --- a/apps/web/src/routes/contribute/+page.svelte +++ b/apps/web/src/routes/contribute/+page.svelte @@ -106,7 +106,7 @@
diff --git a/apps/web/src/routes/report/+page.svelte b/apps/web/src/routes/report/+page.svelte index 074345c..4c93151 100644 --- a/apps/web/src/routes/report/+page.svelte +++ b/apps/web/src/routes/report/+page.svelte @@ -172,7 +172,7 @@
@@ -210,9 +212,11 @@ >
- +

{selectedRecord.fileName}

@@ -222,7 +226,7 @@ class="text-fg-secondary hover:text-fg-primary focus-ring flex h-8 w-8 items-center justify-center rounded-full transition-colors" aria-label={m.history_close_view()} > - close +
diff --git a/apps/web/src/lib/components/index.ts b/apps/web/src/lib/components/index.ts index 9786840..354d73b 100644 --- a/apps/web/src/lib/components/index.ts +++ b/apps/web/src/lib/components/index.ts @@ -1,9 +1,8 @@ -export { default as Button } from './ui/Button.svelte'; export { default as ThemeToggle } from './ui/ThemeToggle.svelte'; export { default as SEO } from './ui/SEO.svelte'; -export { default as Breadcrumb } from './ui/Breadcrumb.svelte'; export { default as Badge } from './ui/Badge.svelte'; export { default as ActionBox } from './ui/ActionBox.svelte'; +export { default as Icon } from './ui/Icon.svelte'; // Layout Components export { default as Header } from './layout/Header.svelte'; @@ -11,7 +10,6 @@ export { default as Footer } from './layout/Footer.svelte'; // Main Components export { default as HistorySection } from './HistorySection.svelte'; -export { default as Dropzone } from './Dropzone.svelte'; // Feedback & Validation export { default as SuccessModal } from './ui/SuccessModal.svelte'; diff --git a/apps/web/src/lib/components/ui/Badge.svelte b/apps/web/src/lib/components/ui/Badge.svelte index ec5fb76..4efe24c 100644 --- a/apps/web/src/lib/components/ui/Badge.svelte +++ b/apps/web/src/lib/components/ui/Badge.svelte @@ -1,4 +1,6 @@ - - diff --git a/apps/web/src/lib/components/ui/Button.svelte b/apps/web/src/lib/components/ui/Button.svelte deleted file mode 100644 index 6469a77..0000000 --- a/apps/web/src/lib/components/ui/Button.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - - - {@render children()} - diff --git a/apps/web/src/lib/components/ui/Icon.svelte b/apps/web/src/lib/components/ui/Icon.svelte new file mode 100644 index 0000000..11ba965 --- /dev/null +++ b/apps/web/src/lib/components/ui/Icon.svelte @@ -0,0 +1,265 @@ + + +{#if name === 'upload_file'} + + + + + +{:else if name === 'visibility'} + + + + +{:else if name === 'delete_outline'} + + + + + + + +{:else if name === 'close'} + + + + +{:else if name === 'warning'} + + + + + +{:else if name === 'check_circle'} + + + + +{:else if name === 'error'} + + + + + +{:else if name === 'progress_activity'} + + + +{:else if name === 'add'} + + + + +{:else if name === 'flag'} + + + + +{:else if name === 'content_copy'} + + + + +{:else if name === 'info'} + + + + + +{:else if name === 'description' || name === 'picture_as_pdf'} + + + + + + + +{:else if name === 'image'} + + + + + +{/if} diff --git a/apps/web/src/lib/components/ui/SuccessModal.svelte b/apps/web/src/lib/components/ui/SuccessModal.svelte index f2e1fba..5b25916 100644 --- a/apps/web/src/lib/components/ui/SuccessModal.svelte +++ b/apps/web/src/lib/components/ui/SuccessModal.svelte @@ -2,6 +2,7 @@ import { fade, fly } from 'svelte/transition'; import { focusTrap } from '$lib/actions/focus-trap'; import { m } from '$lib/paraglide/messages'; + import Icon from './Icon.svelte'; interface Props { isOpen: boolean; @@ -42,9 +43,7 @@ >
- +

{title} diff --git a/apps/web/src/routes/+layout.svelte b/apps/web/src/routes/+layout.svelte index 33cb589..4dfab87 100644 --- a/apps/web/src/routes/+layout.svelte +++ b/apps/web/src/routes/+layout.svelte @@ -72,16 +72,6 @@ - - - - - - upload_file - +

@@ -434,7 +434,7 @@ title="Clear result" aria-label="Clear result" > - close +
{#if processingTime > 0 && !loading && resultText} @@ -517,7 +517,7 @@ onclick={reset} aria-label="Process another image or PDF" > - add + {m.main_process_another()}
@@ -527,10 +527,11 @@ onclick={reportError} aria-label="Report Error or feedback for this result" > - flag + {m.nav_feedback()} diff --git a/apps/web/src/routes/contribute/+page.svelte b/apps/web/src/routes/contribute/+page.svelte index c7fe96f..fd5a382 100644 --- a/apps/web/src/routes/contribute/+page.svelte +++ b/apps/web/src/routes/contribute/+page.svelte @@ -1,6 +1,6 @@