Apply velvet-table redesign, fix game lifecycle and history bugs

Frontend:
- Dark green/gold "velvet table" visual redesign across the whole app
  (Auth, Lobby, GameList, GameTable, History, GameOver, modals), with
  Playfair Display/DM Sans typography and a centralized Tailwind palette.
- Desktop game table fit-scales to fill the window; mobile gets
  overlapping hand/trick layouts and larger touch-friendly cards.
- Standings sidebar now groups completed rounds by series with a
  per-series subtotal row, struck-through tips on missed bids.
- History page rewritten into a scoreboard-style detail view (player
  totals beside names, series grouped 2-up on desktop / stacked on
  mobile) and gained game names, completed/abandoned status, and a
  button to reopen a prematurely-ended game back into the lobby.

Backend:
- Fix started games being deleted from memory (and vanishing from
  everyone's lobby) when all players disconnect; only `end_game` tears
  down a started game now.
- Fix a crash writing a timezone-aware datetime into the naive
  `ended_at` Postgres column.
- Add `reopen_game`/`restore_game` to un-end a prematurely-ended game
  from history and resume it from the lobby.
- Let any seated player end an abandoned game once the host is
  offline, not just the host, so the game isn't stuck forever.
- Expose SERIES_PER_GAME/ROUNDS_PER_SERIES as named constants on the
  engine so the persistence layer derives game-completion rules from
  bridzik.py instead of re-encoding them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Tim
2026-07-01 00:11:42 +02:00
parent 30c32b7714
commit 2c2f07c2ec
28 changed files with 1472 additions and 395 deletions
+58 -1
View File
@@ -1,6 +1,63 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{ts,tsx}'],
theme: { extend: {} },
theme: {
extend: {
colors: {
// Velvet table palette (design handoff: "01 — Sametový stôl")
table: '#090e0b',
header: '#070c09',
circle: '#0c1a0f',
'circle-active': '#0e2015',
gold: '#c9a84c',
'gold-bright': '#f0d060',
'gold-dim': '#e8c14a',
// Secondary text is warm cream (not green) for legibility on the dark
// table — the green is reserved for structure (felt, opponent cards).
'green-dim': '#9c906c',
'green-score': '#d8cba6',
'green-circle': '#c2b58c',
},
fontFamily: {
serif: ['"Playfair Display"', 'Georgia', 'serif'],
sans: ['"DM Sans"', 'system-ui', 'sans-serif'],
},
keyframes: {
// turn-pulse — blinking dot / placeholder slot
tp: { '0%,100%': { opacity: '1' }, '50%': { opacity: '.5' } },
// card-in — card lands on the table
ci: {
from: { opacity: '0', transform: 'translateY(-6px) scale(.9)' },
to: { opacity: '1', transform: 'none' },
},
// active-ring — glowing gold ring around the active player circle
ar: {
'0%,100%': {
boxShadow:
'0 0 0 3px rgba(201,168,76,.18),0 0 18px rgba(201,168,76,.5),0 0 42px rgba(201,168,76,.2)',
},
'50%': {
boxShadow:
'0 0 0 5px rgba(201,168,76,.34),0 0 32px rgba(201,168,76,.85),0 0 56px rgba(201,168,76,.3)',
},
},
// glow-1 — gold glow border on a playable card
g1: {
'0%,100%': {
boxShadow: '0 0 18px rgba(201,168,76,.55),0 6px 18px rgba(0,0,0,.55)',
},
'50%': {
boxShadow: '0 0 34px rgba(201,168,76,.85),0 6px 18px rgba(0,0,0,.55)',
},
},
},
animation: {
tp: 'tp 1.8s ease-in-out infinite',
ci: 'ci .3s ease both',
ar: 'ar 2.2s ease-in-out infinite',
g1: 'g1 2.2s ease-in-out infinite',
},
},
},
plugins: [],
};