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
- 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.
89 lines
2.4 KiB
Svelte
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>
|