Files
scrabble-game/docs/UI_DESIGN.md
T
Ilia Denisov fc1261e078
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 39s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 59s
UI: tab-bar navigation — drop the hamburger
Replace Menu.svelte (hamburger) everywhere with tab-bar navigation:
- Settings hub (SettingsHub) from the lobby ⚙️ tab: Settings/Profile/
  Friends/About as in-place tabs, back → lobby; the lobby ⚙️ badge counts
  incoming friend requests (invitations keep their own lobby section).
- Comms hub (CommsHub) from the move-history 💬: Chat/Dictionary tabs,
  back → game; Dictionary only while the game is active.
- Game menu items relocate into the open history: 🏁 leave / 📤 export in
  the header, 🤝 add-friend per opponent card, 💬 comms; unread chat is
  badged on the score bar + the 💬.
- TapConfirm (tap → fading  → tap) replaces the Skip/Hint press-and-hold
  popovers and drives the add-friend confirm.
- Fix the move-history "jump": the slid board is inert and the stage can't
  scroll, so a swipe up genuinely closes the history.

Remove Menu.svelte + HoldConfirm.svelte. Docs: UI_DESIGN, FUNCTIONAL(+ru),
PRERELEASE. UI check/unit/build/bundle/e2e (Chromium+WebKit) all green.
2026-06-11 14:13:54 +02:00

204 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
- **No hamburger**: navigation is entirely bottom tab bars (the former `Menu.svelte`
dropdown is gone — it fought the Telegram-fullscreen layout, where it had to be re-centred).
Destinations beyond the lobby live behind **hub screens** reached from a tab: a ⚙️
**Settings hub** (`screens/SettingsHub.svelte`, the lobby's ⚙️ tab) and an in-game
**comms hub** (`game/CommsHub.svelte`, the history's 💬). A hub owns one nav bar + one
bottom tab bar whose tabs switch its body **in place** (state, not navigation), so the
back control always returns to the hub's parent (Settings → lobby, comms → game). Settings
hub tabs: ⚙️ Settings / 👤 Profile / 🤝 Friends / ️ About (Friends hidden for guests);
comms hub tabs: 💬 Chat / 🔎 Dictionary (Dictionary only while the game is active). The
routes `/settings|/profile|/friends|/about` and `/game/:id/{chat,check}` survive as hub
entry points (so a Telegram friend-code deep-link still lands on the Friends tab).
- **Tab bar** (`TabBar.svelte`): square, borderless, evenly distributed buttons — a large
emoji icon over a tiny truncated label (hub tabs are **icon-only**). A press highlights a
rounded **square** behind the icon; a hub's **selected** tab stays highlighted (a filled
square with an accent underline). A red count **badge** rides the icon's corner — on the
lobby ⚙️ tab and the hub's 🤝 Friends tab for pending incoming friend requests (invitations
keep their own lobby section), and on the Hint tab for the remaining count. 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 on a **tap of the score bar** and closes on a tap or an **upward swipe** of
the then-inert board (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 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). While the history
is open the **slid board is inert** and the stage cannot scroll, so the whole board reads
as a **tap-or-swipe-up-to-close** surface — closing genuinely clears the open state (it no
longer merely scrolls the board out of view, which used to leave a stale-open state that
made a follow-up plaque tap "jump" the board). The drawer carries its own **header**: a 🏁
**Drop game** (or 📤 **Export GCG** on a finished game) at the left and the comms 💬
(badged with unread chat) at the right, icon-only. Each **opponent**'s card also gains a
🤝 **add-friend** control (non-guests; hidden once a friend, disabled once requested) that
confirms via the fading-✅ tap, swapping the card's score for "Add friend?" while armed
(see Controls); the name and score stay centred — the 🤝 is pinned to the card's edge.
Unread chat is also badged on the score bar itself, so it shows with the history closed.
- **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
- **TapConfirm** (`components/TapConfirm.svelte`, logic in `lib/tapconfirm.ts`): the shared
tap-to-confirm control. A first tap arms a ~2 s window showing a **fading ✅** (no fade
under reduce-motion, but the window still holds); a tap on the ✅ within it confirms,
otherwise it reverts. Used by the **Skip** and **Hint** tabs (the icon morphs to ✅, no
label — replacing the old press-and-hold popover) and the in-game **add-friend 🤝**. The
**Drop game** action keeps its `Modal` confirmation (a destructive, less-frequent action).
- **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 34-player games II 🥈 / III 🥉 /
IV 🏅; active games show Your move 🟢 / Opponent's move ⏳; invitations use 💌.
## Social, account & history surfaces
- **Settings hub** (`screens/SettingsHub.svelte`, the lobby ⚙️ tab): one nav bar + a bottom
tab bar over four bodies — ⚙️ Settings, 👤 Profile, 🤝 Friends, ️ About — switched in
place; back always returns to the lobby. Guests see all but Friends. The lobby ⚙️ badge and
the 🤝 tab badge both show the pending incoming-friend-request count.
- **Friends** (`screens/Friends.svelte`, the Settings hub's 🤝 tab): 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* (the 📤 in the history header) 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 history header
offers *Export GCG* (not *Drop game*) and the comms hub hides the 🔎 *Dictionary* tab; 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).