mirror of
https://github.com/tiennm99/loto.git
synced 2026-10-11 03:13:40 +00:00
fix(ui): four small bugs surfaced after the UI/UX merge
1. Settings gear was unclickable because H1's `drop-shadow` filter created a stacking context that painted the H1 over the absolutely- positioned trigger. Bump gear wrapper to z-10. (src/routes/+page.svelte) 2. Switch rows in settings had a dead zone — `<label>` wrapped a `<span role=switch>` (not a labelable element), so taps on the text fell through. Move role/click/keydown to the wrapping div so the whole row toggles. Pill stays decorative. (src/lib/SettingsButton.svelte) 3. Empty cells used `dark:[filter:brightness(...)]` which creates a per-cell stacking context — same hazard class as bug #1. Replace with a pointer-events-none overlay span (`hidden dark:block`). (src/lib/PlayerBoard.svelte) 4. Audio cache never evicted on voice swap, leaking ~92 Audio elements per change over a session. New `clearAudioCache()` in voice.js, called from `pickVoice` when the id actually changes. (src/lib/voice.js, src/lib/SettingsButton.svelte) Tests 98/98 pass; build clean.
This commit is contained in:
1 parent
7a6a19f242
commit
7d1bb5e68a
4 files changed
+43
-19
No files matched your search
@@ -222,11 +222,19 @@
|
||||
{@const rowComplete = hasNumber && rowCompleteness[row]}
|
||||
|
||||
{#if !hasNumber}
|
||||
<!-- Empty cell. Dark-mode dim is an overlay div, not a CSS
|
||||
filter, so we don't accidentally create a stacking
|
||||
context on every cell. -->
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="relative aspect-[3/4] sm:aspect-[3/5] border border-slate-400/50 dark:border-slate-600/40 dark:[filter:brightness(0.85)_saturate(0.9)]"
|
||||
class="relative aspect-[3/4] sm:aspect-[3/5] border border-slate-400/50 dark:border-slate-600/40"
|
||||
style:background-color="var(--empty-cell-bg)"
|
||||
></div>
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="hidden dark:block absolute inset-0 bg-black/15 pointer-events-none"
|
||||
></span>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
saveSettings,
|
||||
settings,
|
||||
} from "$lib/settings-store.svelte.js";
|
||||
import { clearAudioCache } from "$lib/voice.js";
|
||||
|
||||
let open = $state(false);
|
||||
|
||||
@@ -79,6 +80,7 @@
|
||||
|
||||
/** @param {string} id */
|
||||
function pickVoice(id) {
|
||||
if (settings.voice !== id) clearAudioCache();
|
||||
settings.voice = id;
|
||||
saveSettings();
|
||||
}
|
||||
@@ -182,26 +184,28 @@
|
||||
</fieldset>
|
||||
|
||||
{#snippet switchRow(label, isOn, onToggle)}
|
||||
<label
|
||||
<!-- Whole row is the switch — role/tabindex/click here, not on the
|
||||
inner pill, so tapping the label text also toggles. -->
|
||||
<div
|
||||
role="switch"
|
||||
tabindex="0"
|
||||
aria-checked={isOn}
|
||||
onclick={onToggle}
|
||||
onkeydown={(/** @type {KeyboardEvent} */ e) => {
|
||||
if (e.key === " " || e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onToggle();
|
||||
}
|
||||
}}
|
||||
class="flex items-center justify-between gap-3 px-3 py-2 rounded-lg
|
||||
border-2 border-slate-200 dark:border-slate-600 cursor-pointer
|
||||
hover:border-slate-300 dark:hover:border-slate-500 transition-colors"
|
||||
hover:border-slate-300 dark:hover:border-slate-500 transition-colors
|
||||
focus:outline-none focus:ring-2 focus:ring-indigo-400"
|
||||
>
|
||||
<span class="text-sm text-slate-700 dark:text-slate-200">{label}</span>
|
||||
<span
|
||||
role="switch"
|
||||
tabindex="0"
|
||||
aria-checked={isOn}
|
||||
onclick={onToggle}
|
||||
onkeydown={(/** @type {KeyboardEvent} */ e) => {
|
||||
if (e.key === " " || e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onToggle();
|
||||
}
|
||||
}}
|
||||
aria-hidden="true"
|
||||
class="relative inline-block w-10 h-6 rounded-full transition-colors flex-none
|
||||
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-400
|
||||
focus:ring-offset-white dark:focus:ring-offset-slate-800
|
||||
{isOn ? 'bg-emerald-500' : 'bg-slate-300 dark:bg-slate-600'}"
|
||||
>
|
||||
<span
|
||||
@@ -209,7 +213,7 @@
|
||||
{isOn ? 'translate-x-4' : 'translate-x-0'}"
|
||||
></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<!-- Master mode -->
|
||||
|
||||
@@ -39,6 +39,15 @@ function clipUrl(name) {
|
||||
return `${base}/audio/${settings.voice}/${name}.mp3`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop all cached <audio> elements. Call when the active voice changes
|
||||
* so the old voice's clips don't leak across a long session.
|
||||
*/
|
||||
export function clearAudioCache() {
|
||||
cancelPlayback();
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
export function cancelPlayback() {
|
||||
if (!isBrowser()) return;
|
||||
if (activeClip) {
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
>
|
||||
<div class="w-full max-w-2xl">
|
||||
<header class="relative text-center mb-6">
|
||||
<div class="absolute right-0 top-0">
|
||||
<!-- z-10: H1 has a CSS `drop-shadow` filter which creates a stacking
|
||||
context. Without z-index here, that filtered H1 (later in tree
|
||||
order) paints over the gear button and swallows clicks. -->
|
||||
<div class="absolute right-0 top-0 z-10">
|
||||
<SettingsButton />
|
||||
</div>
|
||||
<h1
|
||||
@@ -23,7 +26,7 @@
|
||||
<p
|
||||
class="text-[0.65rem] sm:text-xs uppercase tracking-[0.3em] text-slate-500 dark:text-slate-400 italic mt-1"
|
||||
>
|
||||
Hội chợ Tân Tân
|
||||
Hội chợ TN1
|
||||
</p>
|
||||
</header>
|
||||
|
||||
|
||||
Reference in new issue
Block a user