feat(ui): first-run onboarding coachmarks
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m20s

A one-time coachmark overlay walks a new player through the lobby and their
first game board: a light dimmed layer draws one tail-pointed hint bubble at a
time, advancing on a tap anywhere and removing itself for good after the last
hint. Two independent series (lobby: settings/stats/new game; game:
header/pass-exchange/hints/shuffle/rack), gated by a per-device persisted flag
and marked done only after the last hint, so an interrupted run replays from
the start. A deep-link into Settings -> Friends still triggers the lobby series
on the first trip back to the lobby.

Targets carry a data-coach attribute, so one positioning engine anchors the
bubble in both portrait and landscape, re-measuring each frame until the
geometry settles (route slide, hidden-banner reflow, fonts). The promo banner
hides while the overlay is up (app.coachActive); a hidden DebugPanel "Reset
visited" control replays the walk-through. Off by default in the mock build so
the Playwright smoke is unaffected; ?coach forces it on for the dedicated e2e.

Pure geometry (step lists, nextVisibleStep, placeBubble) in lib/coachmark.ts
(unit-tested); Coachmark.svelte renders. Docs: FUNCTIONAL(+ru) onboarding
story, UI_DESIGN coachmark section.
This commit is contained in:
Ilia Denisov
2026-06-30 21:48:56 +02:00
parent 90f2427fa7
commit 6636d7c309
16 changed files with 809 additions and 14 deletions
+31 -2
View File
@@ -4,7 +4,7 @@
// snapshot (no secrets, no initData values, no IP) and shares it through the OS share sheet (or a
// clipboard copy on desktop) — a support aid for reproducing client-specific issues, e.g. the
// Telegram Android presentation quirks. Drawn from the top, just under the app header.
import { app, closeDebug } from '../lib/app.svelte';
import { app, closeDebug, resetOnboarding } from '../lib/app.svelte';
import { connection } from '../lib/connection.svelte';
import { shareText } from '../lib/share';
import { telegramChromeDiag } from '../lib/telegram';
@@ -26,12 +26,25 @@
setTimeout(() => (label = 'Share'), 1500);
}
}
// Clear the first-run coachmark "seen" flags so the onboarding replays on the next launch — a
// manual re-test aid. Stops the click from also closing the panel, like the Share control.
let resetLabel = $state('Reset visited');
function resetVisited(e: MouseEvent): void {
e.stopPropagation();
resetOnboarding();
resetLabel = 'Cleared — relaunch';
setTimeout(() => (resetLabel = 'Reset visited'), 1800);
}
</script>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="overlay" onclick={closeDebug}>
<button class="share" onclick={share}>{label}</button>
<div class="actions">
<button class="share" onclick={share}>{label}</button>
<button class="reset" onclick={resetVisited}>{resetLabel}</button>
</div>
<pre class="body">{report}</pre>
</div>
@@ -49,6 +62,12 @@
gap: 10px;
align-items: flex-start;
}
.actions {
flex: 0 0 auto;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.share {
flex: 0 0 auto;
padding: 7px 16px;
@@ -58,6 +77,16 @@
border-radius: var(--radius-sm);
font-size: 0.95rem;
}
/* Secondary (outline) styling so it reads as a utility next to the primary Share action. */
.reset {
flex: 0 0 auto;
padding: 7px 16px;
border: 1px solid var(--accent);
background: transparent;
color: var(--accent);
border-radius: var(--radius-sm);
font-size: 0.95rem;
}
.body {
margin: 0;
width: 100%;