Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
9becfc4
fix(web): icons render as literal text, not glyphs (missing font-family)
janakhpon Sep 13, 2026
46d3f7f
fix(a11y): modal Escape-to-close was dead code in all three modals
janakhpon Sep 13, 2026
c97b265
fix(design-system): shadow-huge, button states, contrast, keyboard gap
janakhpon Sep 13, 2026
1ceabf0
fix(a11y): 5 more opacity-stacked contrast failures
janakhpon Sep 13, 2026
6714c59
fix(pwa): manifest icons 404, and the shipping manifest had drifted
janakhpon Sep 13, 2026
51a49cb
fix(pwa): manifest screenshot declared wrong dimensions
janakhpon Sep 13, 2026
8baba7f
fix(a11y): 3 more contrast gaps a verifier caught in the last sweep
janakhpon Sep 13, 2026
0f2a909
fix(a11y): 3 contrast failures a fourth verification pass found
janakhpon Sep 13, 2026
6ba745d
fix(i18n): drop the "Vulnerable Language" tag from the language dropdown
janakhpon Sep 13, 2026
d976d06
fix(docs): drop the Core Contributors section, per direct request
janakhpon Sep 13, 2026
7645113
fix(privacy): literal markdown asterisks rendered as plain text
janakhpon Sep 13, 2026
21a93fe
fix(privacy): version string was hardcoded and stale
janakhpon Sep 13, 2026
1479e09
feat(web): replace Material Symbols ligature font with inline SVG icons
janakhpon Sep 13, 2026
86f9893
fix(report): SVG icons, and a compact redesign of consent/actions
janakhpon Sep 13, 2026
837e942
fix(buttons): stop stretching primary/secondary buttons full-width
janakhpon Sep 13, 2026
751f24d
fix(modal): destructive button rendered near-black, not red
janakhpon Sep 13, 2026
6bc45c0
docs(benchmark): before/after rating for this session's UI/UX work
janakhpon Sep 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions apps/web/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"footer_tech": "Open Source OCR running in your browser. Using {ocrs} and {onnx}.",
"footer_github": "GitHub",
"nav_privacy": "Privacy",
"nav_mon_heritage": "Vulnerable Language",
"about_title": "About MonOCR",
"about_vulnerable": "Mon (mnw) is my mother tongue. It is currently classified as a vulnerable language by UNESCO.",
"about_digital_presence": "The biggest challenge is the lack of digital presence for Mon. This makes AI development difficult since most texts are in scanned books.",
Expand Down Expand Up @@ -92,7 +91,6 @@
"docs_governance": "Governance",
"docs_governance_privacy": "Privacy Policy",
"docs_governance_license": "MIT License",
"docs_governance_contributors": "Contributors",
"docs_hero_title": "Academic-grade Inference",
"docs_hero_desc": "High-performance, private, and localized optical character recognition framework optimized for archival language preservation.",
"docs_install_title": "Installation",
Expand All @@ -109,7 +107,6 @@
"docs_cli_desc": "Batch processing for historical archives.",
"docs_privacy_guarantee": "Local recognition guarantee. Data integrity is maintained on-device. No manuscript information ever leaves your environment.",
"docs_license_desc": "Open-source governance for research and preservation project. You are free to use and modify the engine for any purpose.",
"docs_contributors_title": "Core Contributors",
"docs_footer_prev": "Workspace",
"docs_footer_next": "Feedback Service"
}
3 changes: 0 additions & 3 deletions apps/web/messages/mnw.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"footer_tech": "ဒှ် Open Source သွက်ဂွံစကာအာ ပ္ဍဲကဵု browser။ စကာလဝ် {ocrs} ကဵု {onnx}။",
"footer_github": "GitHub",
"nav_privacy": "ပရောပူဂဵု တင်မဒးဂိုင်ဒေပ်ပၞုက်လဝ်",
"nav_mon_heritage": "ဘာသာ မဒးမင်မွဲ",
"about_title": "ပရော MonOCR",
"about_vulnerable": "ဘာသာမန် (mnw) ဂှ် ဒှ်အရေဝ်မိအဲရ။ ပ္ဍဲအခိင်လၟုဟ် ညးစကာအောန်စှ်ေကၠုင်တုဲ UNESCO ဟီုလဝ် ဒှ်ဘာသာ မဒးမင်မွဲရ။",
"about_digital_presence": "ပြဒ္ဒညာဇၞော်အိုတ်ဂှ် ပ္ဍဲကဵုလောက ဒစ်ဂျစ်တယ်ဂှ် လိခ်မန်ပိုဲ နွံအောန်ကွေဟ်ရ။ ဟိုတ်ဂှ်တုဲ သွက်ဂွံပတုဲအာ ကမၠောန် AI ဂှ် ဝါတ်ဂါတ်ဒၟံင်ရ။ လိခ်ပိုဲ ဗွဲမဂၠိုင်ဂှ် နွံဒၟံင် ပ္ဍဲကဵု လိခ်ကၞပ် စကင်န်တအ်ရ။",
Expand Down Expand Up @@ -92,7 +91,6 @@
"docs_governance": "Governance",
"docs_governance_privacy": "Privacy Policy",
"docs_governance_license": "MIT License",
"docs_governance_contributors": "Contributors",
"docs_hero_title": "Academic-grade Inference",
"docs_hero_desc": "High-performance, private, and localized optical character recognition framework optimized for archival language preservation.",
"docs_install_title": "Installation",
Expand All @@ -109,7 +107,6 @@
"docs_cli_desc": "Batch processing for historical archives.",
"docs_privacy_guarantee": "Local recognition guarantee. Data integrity is maintained on-device. No manuscript information ever leaves your environment.",
"docs_license_desc": "Open-source governance for research and preservation project. You are free to use and modify the engine for any purpose.",
"docs_contributors_title": "Core Contributors",
"docs_footer_prev": "Workspace",
"docs_footer_next": "Feedback Service"
}
3 changes: 0 additions & 3 deletions apps/web/messages/my.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"footer_tech": "ဘရောက်ဇာတွင် အသုံးပြုနိုင်သော Open Source OCR။ {ocrs} နှင့် {onnx} ကို အသုံးပြုထားသည်။",
"footer_github": "GitHub",
"nav_privacy": "ကိုယ်ရေးလုံခြုံမှု",
"nav_mon_heritage": "ထိန်းသိမ်းရန် လိုအပ်သော ဘာသာစကား",
"about_title": "မွန် OCR အကြောင်း",
"about_vulnerable": "မွန်ဘာသာစကား (mnw) သည် ကျွန်ုပ်၏ မိခင်ဘာသာစကား ဖြစ်သည်။ လက်ရှိတွင် UNESCO မှ ထိန်းသိမ်းရန် လိုအပ်သော ဘာသာစကားအဖြစ် သတ်မှတ်ထားသည်။",
"about_digital_presence": "အကြီးမားဆုံး စိန်ခေါ်မှုမှာ မွန်ဘာသာစကား၏ ဒစ်ဂျစ်တယ် တည်ရှိမှု နည်းပါးခြင်းဖြစ်သည်။ ထို့ကြောင့် AI ဖွံ့ဖြိုးတိုးတက်မှုအတွက် ခက်ခဲစေသည်။",
Expand Down Expand Up @@ -92,7 +91,6 @@
"docs_governance": "စီမံခန့်ခွဲမှု",
"docs_governance_privacy": "ကိုယ်ရေးလုံခြုံမှု မူဝါဒ",
"docs_governance_license": "MIT လိုင်စင်",
"docs_governance_contributors": "ပါဝင်ကူညီသူများ",
"docs_hero_title": "အဆင့်မြင့် OCR အင်ဂျင်",
"docs_hero_desc": "မွန်စာပေ ထိန်းသိမ်းရန်အတွက် အထူးပြုလုပ်ထားသော မြန်နှုန်းမြင့် OCR စနစ်။",
"docs_install_title": "တပ်ဆင်ခြင်း",
Expand All @@ -109,7 +107,6 @@
"docs_cli_desc": "ဖိုင်အများအပြားကို တစ်ပြိုင်နက် လုပ်ဆောင်ရန် CLI ကို အသုံးပြုပါ။",
"docs_privacy_guarantee": "သင်၏ အချက်အလက်များသည် သင့်စက်ထဲတွင်သာ ရှိနေမည်ဖြစ်ပြီး မည်သည့်နေရာသို့မျှ ပေးပို့ခြင်း မရှိပါ။",
"docs_license_desc": "ဤပရောဂျက်သည် Open Source ဖြစ်ပြီး မည်သူမဆို လွတ်လပ်စွာ အသုံးပြုနိုင်ပါသည်။",
"docs_contributors_title": "အဓိက ပါဝင်သူများ",
"docs_footer_prev": "အလုပ်နေရာ",
"docs_footer_next": "တုံ့ပြန်ချက်စနစ်"
}
74 changes: 43 additions & 31 deletions apps/web/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,23 @@
/* Professional Productivity Palette - Neutral & Utilitarian */
--primary: #1a1a1a; /* Pure, deep professional gray/black */
--primary-tint: #4338ca; /* Indigo 700 tint for subtle accents */
--accent-gold: #b2940a;
/* #b2940a computed to 2.85:1 on --bg-canvas -- fails WCAG AA (4.5:1). Darkened for
light mode only; the original value is restored under .dark below, where it
already passes (6.19:1) against the much darker canvas there. */
--accent-gold: #7a6200;
--background-light: #ffffff;
--background-dark: #0a0a0a;
--color-border-raw: 224, 224, 224; /* RGB for --bg-border */

/* Text Hierarchy - Softered for Eye Comfort */
--fg-primary: #2a2a2b; /* Soft Deep Gray */
--fg-secondary: #5c5c5e; /* Muted Zinc */
--fg-muted: #8a8a8c; /* Zinc 500 equivalent */
/* #8a8a8c computed to 3.33:1 on --bg-canvas -- fails WCAG AA (4.5:1) for normal text,
and this token is used at full size (docs body copy) as well as small/uppercase
labels. Darkened to a value that clears 4.5:1 against both --bg-canvas (5.14:1)
and --bg-canvas-subtle (4.83:1); dark mode's #8e8e93 already passes and is
untouched. */
--fg-muted: #6b6b6d;
--fg-accent: #3a3a3c; /* Zinc 900 equivalent */

/* Buttons/Interactive */
Expand All @@ -54,6 +62,9 @@
--bg-canvas-subtle: #1c1c1e;
--bg-canvas-glass: rgba(21, 21, 23, 0.8);
--bg-border: #222224;
/* Restore the original gold -- it's the light-mode value above that fails contrast,
not this one (6.19:1 against this dark canvas). */
--accent-gold: #b2940a;
--fg-primary: #d1d1d6;
--fg-secondary: #8e8e93;
--fg-muted: #8e8e93;
Expand Down Expand Up @@ -89,7 +100,6 @@
--color-accent-gold: var(--accent-gold);

--font-display: 'Inter', system-ui, sans-serif;
--font-public-sans: 'Public Sans', system-ui, sans-serif;

--color-background: var(--bg-canvas);
--color-foreground: var(--fg-primary);
Expand Down Expand Up @@ -134,11 +144,28 @@
}

.btn-primary {
@apply bg-primary-action text-fg-on-primary inline-flex items-center justify-center rounded-[var(--radius-lg)] px-5 py-2.5 text-[11px] font-semibold tracking-[0.05em] uppercase transition-all duration-150 ease-out hover:brightness-110 active:scale-[0.98];
@apply bg-primary-action text-fg-on-primary inline-flex items-center justify-center rounded-[var(--radius-lg)] border border-transparent px-5 py-2.5 text-[11px] font-semibold tracking-[0.05em] uppercase transition-all duration-150 ease-out hover:brightness-110 active:scale-[0.98] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50;
}

.btn-secondary {
@apply border-border bg-btn-bg text-fg-secondary dark:text-fg-primary hover:bg-btn-hover hover:text-fg-primary inline-flex items-center justify-center rounded-[var(--radius-lg)] border px-4 py-2.5 text-[11px] font-semibold tracking-[0.05em] uppercase transition-all duration-150 ease-out active:scale-[0.98];
@apply border-border bg-btn-bg text-fg-secondary dark:text-fg-primary hover:bg-btn-hover hover:text-fg-primary inline-flex items-center justify-center rounded-[var(--radius-lg)] border px-4 py-2.5 text-[11px] font-semibold tracking-[0.05em] uppercase transition-all duration-150 ease-out active:scale-[0.98] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50;
}

/* A real compact variant, not a utility-class override: .btn-primary/.btn-secondary's
own padding is @apply'd in this same layer, so a call site adding `px-3 py-1` on top
loses the cascade -- later-in-layer wins at equal specificity, regardless of source
order in the markup. Combine with .btn-primary/.btn-secondary instead of overriding. */
.btn-compact {
@apply px-3 py-1.5;
}

/* Same cascade issue as .btn-compact above, caught by a verifier on ConfirmationModal's
destructive button: `border-red-500 bg-red-500` layered on top of .btn-primary lost to
its own @apply'd background, so the button rendered near-black instead of red --
undercutting the one signal that a confirm action is irreversible. A real modifier,
defined after .btn-primary in this same layer, wins the cascade instead of losing it. */
.btn-danger {
@apply border-red-500 bg-red-500 hover:bg-red-600;
}
}

Expand Down Expand Up @@ -252,11 +279,17 @@ body {
}

.section-label {
@apply mb-4 block text-[9px] font-bold tracking-[0.3em] text-slate-400 uppercase dark:text-slate-400;
/* Was hardcoded text-slate-400 in both themes (2.48:1 on light canvas, failing WCAG AA)
-- text-fg-muted is theme-aware and, with --fg-muted's light-mode value corrected
above, now clears 4.5:1 in both themes. */
@apply text-fg-muted mb-4 block text-[9px] font-bold tracking-[0.3em] uppercase;
}

.zen-label {
@apply text-fg-muted/50 text-[10px] font-bold tracking-[0.2em] uppercase;
/* Was text-fg-muted/50 (1.72:1 on light canvas at this token's old value, and opacity
alone drops any color below AA regardless of the base -- dropped the opacity instead
of chasing it with an ever-darker base color). */
@apply text-fg-muted text-[10px] font-bold tracking-[0.2em] uppercase;
}

.zen-code-block {
Expand All @@ -265,7 +298,9 @@ body {

/* Improved Syntax Highlighting */
.zen-code-comment {
@apply text-fg-muted/40 italic;
/* Was text-fg-muted/40 (well under 3:1) -- italic already differentiates comments from
code without needing a contrast-failing opacity cut on top. */
@apply text-fg-muted italic;
}

.zen-code-keyword {
Expand Down Expand Up @@ -360,26 +395,3 @@ label {
animation: shimmer 1.5s infinite linear;
will-change: background-position;
}

@keyframes spin-slow {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

.animate-spin-slow {
animation: spin-slow 3s linear infinite;
}

/* Material Symbols - Minimalist & Elegant */
.material-symbols-outlined {
font-variation-settings:
'FILL' 0,
'wght' 300,
'GRAD' 0,
'opsz' 20 !important;
display: inline-block;
}
19 changes: 18 additions & 1 deletion apps/web/src/lib/actions/focus-trap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<div>` — 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;
Expand All @@ -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) {
Expand Down Expand Up @@ -59,6 +73,9 @@ export function focusTrap(node: HTMLElement) {
}

return {
update(newParams?: { onEscape?: () => void }) {
onEscape = newParams?.onEscape;
},
destroy() {
node.removeEventListener('keydown', handleKeydown);
observer.disconnect();
Expand Down
35 changes: 23 additions & 12 deletions apps/web/src/lib/components/ConfirmationModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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 './index';

interface Props {
isOpen: boolean;
Expand Down Expand Up @@ -30,41 +31,51 @@
in:fade={{ duration: 200 }}
out:fade={{ duration: 150 }}
>
<!-- Backdrop -->
<!-- Backdrop: mouse-only click-to-close. Escape is handled by focusTrap below,
once focus has moved inside -- a listener here would never see it. -->
<div
class="bg-canvas/60 absolute inset-0 backdrop-blur-md"
onclick={onCancel}
onkeydown={(e) => e.key === 'Escape' && onCancel()}
role="button"
tabindex="-1"
aria-hidden="true"
></div>

<!-- Modal Container -->
<div
use:focusTrap
class="bg-canvas border-border shadow-huge focus-ring relative w-full max-w-sm overflow-hidden rounded-[var(--radius-huge)] border"
use:focusTrap={{ onEscape: onCancel }}
role="dialog"
aria-modal="true"
aria-labelledby="confirmation-modal-title"
class="bg-canvas border-border focus-ring relative w-full max-w-sm overflow-hidden rounded-[var(--radius-huge)] border shadow-xl"
in:fly={{ y: 20, duration: 400, easing: (t) => 1 - Math.pow(1 - t, 4) /* cubic-out */ }}
>
<div class="px-8 pt-10 pb-8 text-center">
<div
class="mx-auto mb-6 flex h-14 w-14 items-center justify-center rounded-full bg-red-500/10"
>
<span class="material-symbols-outlined text-[28px] text-red-500">warning</span>
<Icon name="warning" size={28} class="text-red-500" />
</div>

<h2 class="text-fg-primary mb-3 text-xl font-bold tracking-tight">
<h2
id="confirmation-modal-title"
class="text-fg-primary mb-3 text-xl font-bold tracking-tight"
>
{title}
</h2>
<p class="text-fg-secondary leading-relaxed text-[var(--text-body)] opacity-80">
<p class="text-fg-secondary leading-relaxed text-[var(--text-body)]">
{message}
</p>
</div>

<div class="border-border bg-canvas-subtle/50 flex flex-col gap-3 border-t p-6 sm:flex-row">
<button class="btn-secondary flex-1" onclick={onCancel}>
<div
class="border-border bg-canvas-subtle/50 flex flex-col gap-3 border-t p-6 sm:flex-row sm:justify-end"
>
<button class="btn-secondary w-full sm:w-auto sm:min-w-[100px]" onclick={onCancel}>
{cancelLabel}
</button>
<button class="btn-primary flex-1 border-red-500 bg-red-500" onclick={onConfirm}>
<button
class="btn-primary btn-danger w-full sm:w-auto sm:min-w-[100px]"
onclick={onConfirm}
>
{confirmLabel}
</button>
</div>
Expand Down
Loading