Files
scrabble-game/ui/src/components/Toast.svelte
T
Ilia Denisov ab1ad998aa
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 52s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
feat(ui): batch of UI polish tweaks
- admin dashboard "Users" count excludes robots (CountUsers, humans only)
- lobby finished-game delete reveal: background matches header/tab-bar (--bg-elev)
- mobile: swipe up on the zoom-out board (no staged tiles) shuffles the rack
- lobby: status icons -25%, game-row top/bottom padding halved
- toast: info tier drifts up ~a tab-bar height while fading over 2s and dismisses
  on tap; error tier unchanged
- game: confirm-move button stays pinned at the right edge; rack tiles shrink to
  fit so a full rack never overflows onto it
- telegram: a successful invite-link redeem shows a welcome window pointing at the bot
- settings: remove the grid-lines toggle; the board is always a gapless checkerboard
- settings/comms hubs: the selected tab highlight wraps icon + label as a pill

Docs: UI_DESIGN (toast, board surface, selected tab), PLAN; e2e updated for the
removed grid-lines toggle.
2026-06-19 08:56:29 +02:00

89 lines
2.4 KiB
Svelte

<script lang="ts">
import { fade, fly } from 'svelte/transition';
import { app, dismissToast } from '../lib/app.svelte';
const dur = $derived(app.reduceMotion ? 0 : 260);
// An info bubble owns its whole 2s life through a CSS rise-and-fade animation, so it takes
// no Svelte enter/leave transition; an error keeps the fly-in / fade-out dwell behaviour.
const isInfo = $derived(app.toast?.kind !== 'error');
</script>
{#if app.toast}
<!-- Re-key on the toast's seq so a fresh message tears the old node down and replays the
appear cycle: the newest toast cancels the previous one and starts its own. -->
{#key app.toast.seq}
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="toast {app.toast.kind}"
class:rise={isInfo && !app.reduceMotion}
class:rise-reduced={isInfo && app.reduceMotion}
role="status"
aria-live="polite"
onclick={dismissToast}
in:fly={{ y: isInfo ? 0 : 32, duration: isInfo ? 0 : dur }}
out:fade={{ duration: isInfo ? 0 : dur }}
>
{app.toast.text}
</div>
{/key}
{/if}
<style>
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translateX(-50%);
max-width: min(92vw, 420px);
padding: 10px 16px;
border-radius: var(--radius);
background: var(--surface);
color: var(--text);
border: 1px solid var(--border);
box-shadow: var(--shadow);
z-index: 50;
text-align: center;
cursor: pointer;
}
.error {
border-color: var(--danger);
color: var(--danger);
}
/* An info bubble fades in, then drifts up by roughly the tab-bar height while fading out,
all within 2s; the X centring is preserved across the rise. */
.toast.rise {
animation: toast-rise 2s ease-out forwards;
}
@keyframes toast-rise {
0% {
opacity: 0;
transform: translateX(-50%) translateY(8px);
}
12% {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
100% {
opacity: 0;
transform: translateX(-50%) translateY(-56px);
}
}
/* Reduced motion: the same 2s life, fade only — no upward travel. */
.toast.rise-reduced {
animation: toast-rise-reduced 2s ease-out forwards;
}
@keyframes toast-rise-reduced {
0% {
opacity: 0;
}
12%,
70% {
opacity: 1;
}
100% {
opacity: 0;
}
}
</style>