feat(ui): batch of UI polish tweaks
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
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.
This commit is contained in:
@@ -51,10 +51,25 @@
|
||||
line-height: 1;
|
||||
transition: background-color 0.12s;
|
||||
}
|
||||
/* A tab that navigates between peer views (Settings / Comms hubs) stays highlighted
|
||||
while selected: a filled square with an accent underline. A tap itself leaves no
|
||||
highlight — there is no press tint, and the WebKit tap flash is disabled on .tab. */
|
||||
:global(.tab.active .sq) {
|
||||
/* A tab that navigates between peer views (Settings / Comms hubs) wraps its icon and label
|
||||
in a .face so the selected highlight hugs both — a filled pill with an accent underline,
|
||||
sized to the content with a small padding. A tap itself leaves no highlight (no press
|
||||
tint, and the WebKit tap flash is disabled on .tab). Plain action tabs (the lobby nav,
|
||||
the in-game controls) carry no .face and are never .active, so they are unaffected. */
|
||||
:global(.tab .face) {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
transition: background-color 0.12s;
|
||||
}
|
||||
/* Inside a face the icon square needs no padding of its own — the face provides the
|
||||
surround (the bare .sq padding still applies to the unwrapped action tabs). */
|
||||
:global(.tab .face .sq) {
|
||||
padding: 0;
|
||||
}
|
||||
:global(.tab.active .face) {
|
||||
background: var(--surface-2);
|
||||
box-shadow: inset 0 -2px 0 var(--accent);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
import { app } from '../lib/app.svelte';
|
||||
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 (out:fade) and replays
|
||||
the entrance (in:fly): the newest toast cancels the previous one and starts its own cycle. -->
|
||||
<!-- 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"
|
||||
in:fly={{ y: 32, duration: dur }}
|
||||
out:fade={{ duration: dur }}
|
||||
onclick={dismissToast}
|
||||
in:fly={{ y: isInfo ? 0 : 32, duration: isInfo ? 0 : dur }}
|
||||
out:fade={{ duration: isInfo ? 0 : dur }}
|
||||
>
|
||||
{app.toast.text}
|
||||
</div>
|
||||
@@ -36,9 +44,45 @@
|
||||
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>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { app, dismissWelcomeRedeem } from '../lib/app.svelte';
|
||||
import { t } from '../lib/i18n/index.svelte';
|
||||
import { botUsername } from '../lib/deeplink';
|
||||
import { telegramOpenLink } from '../lib/telegram';
|
||||
import Modal from './Modal.svelte';
|
||||
|
||||
// Point at the bot the player signed in through (its service language), falling back to the
|
||||
// interface locale, so an ru player is sent to the ru bot and an en player to the en one.
|
||||
const username = $derived(botUsername(app.session?.serviceLanguage || app.locale));
|
||||
// Greet the arriving player by their own display name.
|
||||
const name = $derived(app.profile?.displayName || app.session?.displayName || '');
|
||||
// Interpolate the name, then split the rest around the {bot} token so the bot handle renders
|
||||
// as an inline link (the {name} value is substituted first; {bot} is left for the split).
|
||||
const parts = $derived(t('friends.welcomeRedeem', { name }).split('{bot}'));
|
||||
|
||||
function openBot() {
|
||||
if (username) {
|
||||
const url = `https://t.me/${username}`;
|
||||
// Inside Telegram, openTelegramLink navigates to the bot chat natively; elsewhere fall
|
||||
// back to a new tab.
|
||||
if (!telegramOpenLink(url) && typeof window !== 'undefined') window.open(url, '_blank');
|
||||
}
|
||||
dismissWelcomeRedeem();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if app.welcomeRedeem}
|
||||
<Modal title={t('friends.welcomeRedeemTitle')} onclose={dismissWelcomeRedeem}>
|
||||
<p class="msg">{parts[0]}{#if username}<button type="button" class="bot" onclick={openBot}>@{username}</button>{/if}{parts[1] ?? ''}</p>
|
||||
<button class="ok" onclick={dismissWelcomeRedeem}>{t('common.ok')}</button>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.msg {
|
||||
margin: 0 0 16px;
|
||||
line-height: 1.5;
|
||||
/* The greeting and the bot sentence are separated by a blank line in the message. */
|
||||
white-space: pre-line;
|
||||
}
|
||||
.bot {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
.ok {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--accent);
|
||||
background: var(--accent);
|
||||
color: var(--accent-text);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user