UI readability pass and Bazaar in-app purchase bridge

- muted text #9e9ec0 -> #727299 (contrast 2.6 -> 4.5 on white); tab labels 11px
- map: chapter titles wrap to two lines instead of being cut off
- reduced-motion respected (MotionConfig + CSS), focus-visible ring,
  no pull-to-refresh, short vibration on correct/wrong answers
- src/lib/bazaar.ts: bridge to the Android WebView wrapper; checkout uses
  Bazaar IAP only inside that app, Zarinpal everywhere else (unchanged)
- android/: WebView wrapper with Poolakey; written but never built

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 19:13:12 +03:30
parent adecf25486
commit f8cf86e4e8
19 changed files with 619 additions and 68 deletions

View File

@@ -43,12 +43,29 @@ function tone(frequency: number, start: number, duration: number, volume = 0.05)
osc.stop(t + duration + 0.02);
}
/**
* لرزشِ کوتاه کنارِ صدا، با همان کلیدِ «صدا» خاموش می‌شود. فقط اندروید/کروم دارد؛
* Safariِ آیفون `vibrate` ندارد و این‌جا هیچ کاری نمی‌کند.
*/
function buzz(pattern: number | number[]) {
if (!soundEnabled() || typeof navigator.vibrate !== 'function') return;
try {
navigator.vibrate(pattern);
} catch {
/* بعضی WebViewها بی‌اجازه throw می‌کنند؛ لرزش فقط تزئینی است */
}
}
export const sounds = {
tick: () => tone(720, 0, 0.045, 0.04),
correct: () => {
tone(660, 0, 0.12);
tone(990, 0.1, 0.18);
buzz(30);
},
wrong: () => {
tone(220, 0, 0.25, 0.06);
buzz([40, 60, 40]);
},
wrong: () => tone(220, 0, 0.25, 0.06),
finish: () => [523, 659, 784, 1047].forEach((f, i) => tone(f, i * 0.11, 0.2)),
};