Files
scrabble-game/ui/src/screens/About.svelte
T
Ilia Denisov 1a456c4847
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s
feat(offline): transport kill switch + gate the network-requiring UI
Offline mode was 'fiction': the UI only disabled the Stats tab, but Profile was
reachable and a profile save actually hit the server and reported 'saved'.

Two layers now:
- Transport kill switch (transport.ts): in offline mode every network op — each
  unary RPC (exec), the live stream (subscribe), the dict/metrics fetches and
  the reachability probe — is refused before it leaves the device. Offline
  truly means offline regardless of any UI that slips through. The local
  (device-only) game path never uses this transport, so it is unaffected.
- UI gating: the Profile and Friends tabs (SettingsHub) and the Feedback entry
  (About) are disabled offline, and the hub falls back to Settings (which holds
  the online/offline toggle); the lobby Stats tab was already disabled. In-game
  social/export are already hidden for a vs_ai game.

Online unaffected (offlineMode is off): e2e 196. Offline gating is
contour-verified (the mock e2e cannot enter offline).
2026-07-06 15:46:22 +02:00

109 lines
2.5 KiB
Svelte

<script lang="ts">
import { t } from '../lib/i18n/index.svelte';
import { app } from '../lib/app.svelte';
import { navigate } from '../lib/router.svelte';
import { offlineMode } from '../lib/offline.svelte';
import { aboutContent } from '../lib/aboutContent';
import { onExternalLinkClick } from '../lib/links';
// The auto-match move clock (mirrors backend game.DefaultTurnTimeout = 24h).
const AUTO_MATCH_HOURS = 24;
const version = __APP_VERSION__;
const c = $derived(aboutContent(app.locale, AUTO_MATCH_HOURS));
</script>
<div class="page">
<h1>{c.title}</h1>
<p>
{c.rulesPrefix}<a href={c.rulesUrl} target="_blank" rel="noopener noreferrer" onclick={onExternalLinkClick}>{c.rulesLink}</a>.
</p>
<section>
<h2>{c.randomTitle}</h2>
<p class="respect">❗️{c.randomRespect}</p>
<ul>
{#each c.random as item (item)}<li>{item}</li>{/each}
</ul>
</section>
<section>
<h2>{c.friendsTitle}</h2>
<ul>
{#each c.friends as item (item)}<li>{item}</li>{/each}
</ul>
</section>
<p class="muted">{t('about.version', { v: version })}</p>
{#if !(app.profile?.isGuest ?? true)}
<button class="feedback" disabled={offlineMode.active} onclick={() => navigate('/feedback')}>
{t('feedback.title')}{#if app.feedbackReplyUnread}<span class="fbadge">1</span>{/if}
</button>
{/if}
</div>
<style>
.page {
padding: var(--pad);
display: flex;
flex-direction: column;
gap: 14px;
}
h1 {
margin: 0;
font-size: 1.5rem;
}
h2 {
margin: 0 0 6px;
font-size: 1.05rem;
color: var(--text-muted);
}
section {
display: flex;
flex-direction: column;
gap: 4px;
}
ul {
margin: 0;
padding-left: 20px;
display: flex;
flex-direction: column;
gap: 4px;
}
a {
color: var(--accent);
}
.respect {
margin: 0;
font-weight: 600;
}
.muted {
color: var(--text-muted);
font-size: 0.9rem;
margin: 0;
}
.feedback {
align-self: flex-start;
padding: 12px 18px;
border: 1px solid var(--accent);
background: var(--accent);
color: var(--accent-text);
border-radius: var(--radius);
font-weight: 600;
}
/* The reply badge sits on the accent button, so it inverts the accent colours. */
.fbadge {
display: inline-block;
margin-left: 8px;
min-width: 18px;
padding: 0 5px;
border-radius: 999px;
background: var(--accent-text);
color: var(--accent);
font-size: 0.78rem;
font-weight: 700;
line-height: 1.55;
text-align: center;
}
</style>