8881214213
Mechanical, behaviour-preserving removal of Stage N / TODO-N / phase (RN) references from comments, doc-comments, service READMEs, the current-state docs (ARCHITECTURE, FUNCTIONAL+_ru, TESTING, UI_DESIGN), config-file comments, and the .fbs/.proto schema comments. PLAN.md / PRERELEASE.md / CLAUDE.md keep the stage history. - Rename the only stage-named identifiers: registerStage8 -> registerSocialOps, registerStage11 -> registerLinkOps (gateway transcode). - Split stage6_test.go: TestEmailLoginFlow -> email_test.go, TestGuestAutoMatchLeavesNoStats (+ provisionGuest) -> account_test.go. - Regenerated proto bindings (push.pb.go, telegram_grpc.pb.go) from the de-staged .proto comments; FB Go/TS bindings unchanged (flatc strips schema comments). go build/vet/gofmt clean across modules; integration typecheck and pnpm check green.
178 lines
12 KiB
Markdown
178 lines
12 KiB
Markdown
# Scrabble Game — UI design system
|
||
|
||
Visual and interaction conventions for the `ui` client. Behaviour lives in
|
||
[`FUNCTIONAL.md`](FUNCTIONAL.md); cross-service architecture (including the global
|
||
points this doc references) lives in [`ARCHITECTURE.md`](ARCHITECTURE.md). The client
|
||
is **pure HTML5/CSS + Unicode** — no image/font/SVG assets; icons are CSS shapes or
|
||
emoji glyphs. Tokens are CSS custom properties (`ui/src/app.css`), light/dark via
|
||
`prefers-color-scheme` or an explicit Settings choice, and **Telegram-themed**:
|
||
on a Telegram Mini App launch — the app is served under `/telegram/` and detects the
|
||
launch by `Telegram.WebApp.initData` — the SDK's `themeParams` override the tokens at
|
||
runtime; opened outside Telegram, the `/telegram/` path redirects to the site root.
|
||
|
||
## Layout shell (`components/Screen.svelte`)
|
||
|
||
A full-height flex column: the nav bar, the announcement strip, the content, and an
|
||
optional bottom tab bar (the tab bar always sits at the screen bottom). On most screens
|
||
the nav is minimal and the **content fills** between nav and tab bar. **Only in the
|
||
game** (`growNav`) does the nav bar grow to absorb spare height (buttons top-aligned),
|
||
pinning the board and controls to the **bottom** for thumb reach. Every screen except
|
||
Login uses `Screen`.
|
||
|
||
## Navigation
|
||
|
||
- **Back**: a thin, compact `<` drawn from two rotated CSS borders (`Header.svelte`
|
||
`.chev`) — lighter than a glyph.
|
||
- **Hamburger**: a CSS three-bar (`Menu.svelte`), deliberately larger; opens a dropdown
|
||
of items (lobby: Friends/Profile/Settings/About; game: History/Chat/Check word, plus
|
||
*Export GCG* on a finished game and *Add to friends* per opponent, then Drop game). A
|
||
red count **badge** rides the hamburger (and the lobby *Friends* item) for pending
|
||
incoming friend requests + invitations; the same dot style serves any future
|
||
notification count.
|
||
- **Tab bar** (`TabBar.svelte`): square, borderless, evenly distributed buttons — a large
|
||
emoji icon over a tiny truncated label. A press highlights a rounded **square** behind
|
||
the icon (slightly larger than it) until release; spacing keeps adjacent labels from
|
||
touching. No text selection on nav / tab-bar / buttons (`user-select: none`).
|
||
- **Screen transitions** (`App.svelte`): navigation slides directionally — a
|
||
screen entered from the lobby flies in from the right; returning to the lobby reveals it
|
||
from the left (back). Transitions are local (so they do not play on first load) and
|
||
collapse to nothing under reduce-motion. Per-game and lobby in-memory caches
|
||
(`lib/gamecache.ts`, `lib/lobbycache.ts`) render a re-opened game or the lobby instantly
|
||
and refresh in the background, removing the blank-loading flash and the lobby's "draw-in"
|
||
on lobby ↔ game navigation.
|
||
- **Telegram integration** (`lib/telegram.ts`): inside the Mini App the colour
|
||
scheme is forced from `Telegram.WebApp.colorScheme` (over the OS `prefers-color-scheme`,
|
||
which leaks into the Telegram Desktop webview and otherwise fights it) and the Settings
|
||
theme switcher is hidden; the nav bar takes Telegram's background and `setHeaderColor` /
|
||
`setBackgroundColor` / `setBottomBarColor` paint Telegram's own chrome to match; the
|
||
native header **BackButton** drives back-navigation (the app's chevron is hidden in
|
||
Telegram); **HapticFeedback** fires on tile placement / commit / error; **closing
|
||
confirmation** is enabled while a game is open; **vertical swipes** (swipe-to-minimise)
|
||
are disabled so they don't fight tile drag or the board scroll; and a live stream dropped
|
||
by a background suspend reconnects silently on return — the connection banner is
|
||
suppressed while hidden and for a short grace after resume (visibilitychange +
|
||
pageshow/pagehide + Telegram `activated`/`deactivated`).
|
||
|
||
## Tiles & board
|
||
|
||
- **Tiles**: the letter sits in the **top-left** corner (offset a touch more than the
|
||
value), the point value bottom-right; blanks show no value.
|
||
- **Board zoom** (`Board.svelte`): a two-state zoom (full 15×15 ↔ ~9 cells) by **growing
|
||
the board's width** inside a fixed-size viewport (a real layout change → native scroll
|
||
that works consistently across browsers; no `transform`, which broke scrolling
|
||
differently in Safari/Chrome). Labels are sized in `cqw` against the fixed viewport, so
|
||
they stay a constant size as the cells grow (relatively smaller at higher zoom).
|
||
**Double-tap** an empty/filled cell toggles zoom centred on it; double-tap a **pending**
|
||
tile recalls it. **Pinch** zooms toward the pinch midpoint (a two-finger gesture;
|
||
preventDefault fires only for two touches, so one-finger scroll stays native, and a second
|
||
finger aborts an in-progress drag). The pan is **interpolated toward a pre-clamped target**
|
||
as the real width grows/shrinks, so it magnifies evenly A→B instead of lurching and snapping
|
||
back near the edges. It **recentres only on a zoom-in** — placing a 2nd+ tile or
|
||
hovering a dragged tile never jumps the board. On touch the first tile placement auto-zooms
|
||
in centred on the target, and **holding a dragged tile over a cell ~1 s** auto-zooms there
|
||
the first time. The swipe-to-open-history gesture stays dropped (it fought native scroll);
|
||
history opens from the menu or a tap on the players plaque (below). A **hint** auto-zooms
|
||
centred on the hint's placement, not the top-left.
|
||
- **Placing & recall** (`Game.svelte`): a rack tile is placed by tap-then-tap or by
|
||
dragging it onto a cell; while a dragged tile is carried over the board, the aimed-at empty
|
||
cell is **highlighted as a drop target** (an accent ring). A pending tile is taken back by a
|
||
**double-tap** or by **dragging it back onto the rack** (unzoomed board only — when zoomed
|
||
the one-finger gesture scrolls). A single tap no longer recalls (too easy to trigger); a
|
||
recalled tile returns to its original rack slot.
|
||
- **Players plaque & history** (`Game.svelte`): the seats above the board share
|
||
the width evenly; the seat whose turn it is is **raised** (a drop shadow on its sides)
|
||
while the others read **sunk in** (an inset shadow). A tap anywhere on the plaque toggles
|
||
the **move history** — a fixed-height slide-down drawer whose bottom border (and its
|
||
shadow) pins to the board as the board slides down, instead of tracking the table as
|
||
moves accumulate; its scrollbar gutter is reserved so the centred word column does not
|
||
jitter. A move's row lists every word it formed (the main word first).
|
||
- **Vertical fit & keyboard**: when the game does not fit the viewport, only the
|
||
board area scrolls vertically (`Screen` `column` mode; the score bar, status, rack and tab
|
||
bar stay fixed), while zoom keeps its own scroll. The check-word dialog opens in
|
||
`Modal` keyboard-overlay mode — the small sheet is top-anchored and the soft keyboard
|
||
overlays the empty area below, so the layout doesn't resize/jank; other modals stay
|
||
keyboard-aware (they size to the area above the keyboard).
|
||
- **Highlights**: pending tiles use a slightly darker tile background (no outline). The
|
||
last completed word gets a dark tile background — static while it is the opponent's
|
||
turn (our word), and a 1 s flash when it is our turn (their word). While placing, only
|
||
the pending tiles are highlighted.
|
||
- **Bonus-square labels** — a Settings choice (`boardlabels.ts`): `beginner` shows a
|
||
split `3×` / `word` (localized слово/буква), `classic` a single `3W` / `3С`, `none`
|
||
nothing. Default **beginner**.
|
||
- **Grid lines** — a Settings toggle, **default off**. Off: a **gapless
|
||
checkerboard** — plain cells alternate two shades, and tiles get rounded corners with a
|
||
soft right-side shadow so adjacent gapless tiles still read apart, reclaiming ~14px of
|
||
board width. On: the classic lined grid, where the inter-cell gap shows a contrasting
|
||
`--cell-line` (darker in light, lighter in dark) to avoid a wavy-line optical illusion.
|
||
|
||
## Controls
|
||
|
||
- **HoldConfirm** (`components/HoldConfirm.svelte`): the shared press-and-hold control. A
|
||
short tap opens a small popover above the button; a ~0.7 s hold runs the primary action
|
||
immediately. Used by the **Skip** and **Hint** tabs (each confirmed by an **Ok ✅** popover).
|
||
- **MakeMove / Reset**: when ≥1 tile is pending the rack collapses its used slots
|
||
and shifts left, a **borderless ✅ icon button** (styled like a tab, not a filled accent
|
||
button) beside the rack commits the move — no popover, and disabled while the pending word
|
||
is known illegal; the 🔀 Shuffle tab is replaced by a **↩️ Reset** tab.
|
||
- **Game tab bar**: 🔄 Draw (disabled when the bag is empty), 🥺 Skip, 🛟 Hint (with a
|
||
remaining-count badge, disabled at zero); 🔀 Shuffle (no label, no confirm), which
|
||
**animates** — tiles hop along a low parabola to their new slots (duration scaled by the
|
||
distance, the longest ≤ 0.3 s; off under reduce-motion) with a short haptic shake. The
|
||
under-board slot shows the **Scores: N** preview. The screen **title** is the variant's
|
||
display name (Scrabble / Скрэббл / Erudite / Эрудит), not a constant "Scrabble".
|
||
|
||
## Announcement banner (`components/AdBanner.svelte`, `lib/banner.ts`)
|
||
|
||
A one-line inset strip under the nav bar, drawn on a dedicated `--ad-bg` token —
|
||
a subtle accent, a touch darker than the surroundings in the light theme and a touch lighter
|
||
in the dark theme, mapped to Telegram's `secondary_bg_color` inside the Mini App. Content is
|
||
minimal markdown (text + links, escaped + linkified). A parameterised **rotator** drives messages: a fitting message
|
||
holds `holdMs` (default 60 s) then cross-fades to the next; a message wider than the strip
|
||
pauses (`edgePauseMs`), scrolls to its right edge at `scrollPxPerSec`, pauses, and repeats
|
||
until the cycle exceeds `holdMs`. Today a **mock** provider rotates a long and a short
|
||
message; the source becomes a server-driven channel later (see ARCHITECTURE).
|
||
|
||
## Result / status iconography (`lib/result.ts`)
|
||
|
||
Lobby rows show two lines (opponents, then result + score) with a large place-based emoji
|
||
on the right: Victory 🏆 / Defeat 🥈 / Draw 🏅, and for 3–4-player games II 🥈 / III 🥉 /
|
||
IV 🏅; active games show Your move 🟢 / Opponent's move ⏳; invitations use 💌.
|
||
|
||
## Social, account & history surfaces
|
||
|
||
- **Friends** (`screens/Friends.svelte`, from the lobby menu): an "add a friend" block
|
||
pairing a code **input** with a **Show my code** action that reveals a large 6-digit
|
||
code + its expiry; then the incoming **requests** (Accept / Decline), the **friends**
|
||
list (Remove / Block), and a **blocked** list (Unblock). Durable accounts only — a
|
||
guest sees a sign-in prompt.
|
||
- **Invitations**: a lobby **section** (a 💌 row per open invitation) with Accept /
|
||
Decline for an invitee and a waiting/Cancel state for the inviter; creating one is the
|
||
**"Play with friends"** mode in `NewGame.svelte` (pick invitees, then variant / move
|
||
time / hints).
|
||
- **Statistics** (`screens/Stats.svelte`, the lobby 📊 tab): a 2-column grid of stat
|
||
cards (wins / losses / draws / games / win-rate / best game / best move) — pure
|
||
numbers, no charts.
|
||
- **Profile editing** (`screens/Profile.svelte`): an inline form — display name, a
|
||
**UTC-offset** timezone dropdown (defaulting to the browser's offset), the away
|
||
window as hour + 10-minute dropdowns (24-hour, ≤ 12 h), and block toggles — plus an
|
||
email-binding sub-flow (enter email → enter the confirm code on a numeric field).
|
||
Invalid fields show a **red border** (no message) and **Save stays disabled** until
|
||
every field is valid. Interface language stays in **Settings** (it writes through to
|
||
the account for durable users).
|
||
- **Friend code**: the issued code sits next to a 📋 copy control; tapping the code or
|
||
the icon copies it. Flex text inputs carry `min-width:0` so they shrink instead of
|
||
overflowing in Safari.
|
||
- **History / GCG**: the in-game slide-down history gains the running total per move;
|
||
*Export GCG* shares or downloads the `.gcg` file and appears only once the game is
|
||
finished.
|
||
- **Finished game**: the board keeps no last-word highlight and no zoom; the menu drops
|
||
*Check word* and *Drop game*; and the footer (rack + tab bar) is **drawn but inert**
|
||
(greyed, non-interactive) rather than hidden, so the layout does not jump. Chat
|
||
send / nudge are the ⬆️ / 🛎️ icons.
|
||
|
||
## Caveat
|
||
|
||
Emoji are rendered by the platform's system emoji font, so their exact look varies across
|
||
OSes — acceptable for the MVP, and consistent with the no-asset rule (no glyphs are
|
||
downloaded).
|