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
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:
@@ -35,7 +35,18 @@ import {
|
||||
import { onVKPath, insideVK, vkInit, vkLaunchParams, vkUserName, vkStartParam, vkOnScheme, vkOnInsets } from './vk';
|
||||
import { CLOUD_PREFS_KEY, decodeClientPrefs, encodeClientPrefs } from './cloudprefs';
|
||||
import { parseStartParam } from './deeplink';
|
||||
import { clearSession, loadPrefs, loadSession, saveSession, savePrefs } from './session';
|
||||
import {
|
||||
clearOnboarding,
|
||||
clearSession,
|
||||
loadOnboarding,
|
||||
loadPrefs,
|
||||
loadSession,
|
||||
type OnboardingState,
|
||||
saveOnboarding,
|
||||
saveSession,
|
||||
savePrefs,
|
||||
} from './session';
|
||||
import type { CoachSeries } from './coachmark';
|
||||
import { connection, reportOffline, reportOnline, resetConnection } from './connection.svelte';
|
||||
import { isConnectionCode } from './retry';
|
||||
import { clearGameCache, getCachedGame, setCachedGame } from './gamecache';
|
||||
@@ -86,6 +97,12 @@ export const app = $state<{
|
||||
locale: Locale;
|
||||
reduceMotion: boolean;
|
||||
boardLabels: BoardLabelMode;
|
||||
/** Completion flags for the two first-run coachmark series (components/Coachmark.svelte). */
|
||||
onboarding: OnboardingState;
|
||||
/** Whether a first-run coachmark overlay is currently on screen. While set, the scrolling promo
|
||||
* banner (components/PromoBanner) hides so it does not run above the dimmed onboarding layer,
|
||||
* and reappears (per its own show logic) once onboarding closes. */
|
||||
coachActive: boolean;
|
||||
/** Pending incoming friend requests, for the lobby ⚙️ badge and the Settings Friends tab. */
|
||||
notifications: number;
|
||||
/** Per-game flag: the player has at least one unread chat entry (message or nudge) in that
|
||||
@@ -128,6 +145,8 @@ export const app = $state<{
|
||||
locale: 'en',
|
||||
reduceMotion: false,
|
||||
boardLabels: 'beginner',
|
||||
onboarding: { lobbyDone: false, gameDone: false },
|
||||
coachActive: false,
|
||||
notifications: 0,
|
||||
chatUnread: {},
|
||||
messageUnread: {},
|
||||
@@ -609,6 +628,7 @@ export async function bootstrap(): Promise<void> {
|
||||
app.theme = prefs.theme ?? 'auto';
|
||||
app.reduceMotion = prefs.reduceMotion ?? false;
|
||||
app.boardLabels = prefs.boardLabels ?? 'beginner';
|
||||
app.onboarding = await loadOnboarding();
|
||||
applyTheme(app.theme);
|
||||
applyReduceMotion(app.reduceMotion);
|
||||
if (prefs.locale) {
|
||||
@@ -904,6 +924,26 @@ export async function logout(): Promise<void> {
|
||||
navigate('/login');
|
||||
}
|
||||
|
||||
/**
|
||||
* markOnboardingDone records that a first-run coachmark series has been completed (its last hint
|
||||
* was shown) and persists the flag, so the series never replays for this client. A series
|
||||
* interrupted before its last hint is left unmarked and replays from the start next launch.
|
||||
*/
|
||||
export function markOnboardingDone(series: CoachSeries): void {
|
||||
if (series === 'lobby') app.onboarding.lobbyDone = true;
|
||||
else app.onboarding.gameDone = true;
|
||||
void saveOnboarding({ ...app.onboarding });
|
||||
}
|
||||
|
||||
/**
|
||||
* resetOnboarding clears both coachmark completion flags (the hidden DebugPanel control), so the
|
||||
* onboarding replays on the next launch — a manual re-test aid.
|
||||
*/
|
||||
export function resetOnboarding(): void {
|
||||
app.onboarding = { lobbyDone: false, gameDone: false };
|
||||
void clearOnboarding();
|
||||
}
|
||||
|
||||
function persistPrefs(): void {
|
||||
void savePrefs({
|
||||
theme: app.theme,
|
||||
|
||||
Reference in New Issue
Block a user