Files
scrabble-game/docs/UI_DESIGN.md
T
Ilia Denisov 09fec2b83c
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 26s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m4s
Stage 17: bake decisions into PLAN, ARCHITECTURE, FUNCTIONAL(+ru), UI_DESIGN, READMEs; mark stage done
- PLAN: Stage 17 Refinements entry + caveats resolved summary + tracker done
- ARCHITECTURE §7 (move-number robot timing, composed variant-aware names), §10 (move event to the actor too), §11 (game_move_duration metric + offline admin per-user analytics), §14 (current branch model, path-conditional CI + gate, connector liveness)
- FUNCTIONAL(+ru): robot draws language-appropriate names
- UI_DESIGN: screen transitions, Telegram theme/nav, ad-banner accent, players plaque + history drawer
- backend README: robot timing/names refinements
2026-06-06 10:31:22 +02:00

9.4 KiB
Raw Blame History

Scrabble Game — UI design system

Visual and interaction conventions for the ui client. Behaviour lives in FUNCTIONAL.md; cross-service architecture (including the global points this doc references) lives in 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 (Stage 9): 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 (Stage 17, 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. A per-game in-memory cache (lib/gamecache.ts) renders a re-opened game instantly and refreshes it in the background, removing the blank-loading flash on lobby ↔ game navigation.
  • Telegram theme (Stage 17): 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), the Settings theme switcher is hidden, the nav bar takes Telegram's background (header_bg_color), and a live stream dropped by a background suspend silently reconnects on return to the foreground (the connection banner is suppressed while hidden).

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 toggles zoom and, on touch, placing a tile auto-zooms in centred on the target; the custom pinch and swipe-to-open-history gestures were dropped because they fight 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 (Stage 17).
  • Players plaque & history (Game.svelte, Stage 17): 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).
  • 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: 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. Reused by:
    • MakeMove (appears when ≥1 tile is pending; the rack collapses its used slots and shifts left to free room): a 🏁 button whose popover offers Make move / Reset .
    • Game tab bar: 🔄 Draw (disabled when the bag is empty), 🥺 Skip, 🛟 Hint (with a remaining-count badge) — each confirmed by an Ok popover; 🔀 Shuffle has no label and no confirm. The under-board slot shows the Scores: N preview.

Announcement banner (components/AdBanner.svelte, lib/banner.ts)

A one-line inset strip under the nav bar, drawn on a dedicated --ad-bg token (Stage 17) — 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 34-player games II 🥈 / III 🥉 / IV 🏅; active games show Your move 🟢 / Opponent's move ; invitations use 💌.

Social, account & history surfaces (Stage 8)

  • 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).