Frontend
The frontend is a SvelteKit application under frontend/, built with Vite and TypeScript and deployed as static files. It uses raw CSS rather than a component or utility framework.
Main files
| Path | Responsibility |
|---|---|
src/routes/+page.svelte | Prerendered home page and room creation |
src/routes/room/[code]/ | Client-rendered join and room lifecycle |
src/routes/+layout.svelte | Global styles and theme initialization |
src/views/ | Home, join, full-table room, and focused phone view composition |
src/components/ | Shared brand and theme controls |
src/components/room/ | Room session view, header, active table, shared vote cards, estimate deck, and session summary |
src/lib/room/ | Room domain types, session state machine, and pure table layout calculations |
src/lib/transport/ | Shared transport contract and hardened SSE/WebSocket implementations |
src/lib/api.ts | Typed room API operations and status-aware errors |
src/app.css | Global design tokens, resets, and shared control styles |
vite.config.ts | Development proxy for API and real-time routes |
../backend/internal/adapters/webapp | Production static serving and dynamic-route fallback |
View lifecycle
The app has four views:
- Home: collect room and facilitator names, then create a room.
- Join: collect a display name for an invite URL such as
/room/A1B2C3. - Room: display participants, topic, voting cards, completion state, revealed results, and the final session summary on a shared or desktop screen.
- Mobile room: provide a focused card-selection interface for participants who can see the full table on a shared screen.
SvelteKit supplies filesystem routing. The home page is prerendered to index.html; /room/[code] is client-rendered because it depends on browser identity storage and realtime connections. The static adapter emits 200.html, which the Go web adapter serves for direct room URLs. The route keys RoomSessionView by room code so navigation between room URLs always closes the old transport and initializes fresh state.
Identity storage
After create or join, the participant token is stored under:
poker:{ROOM_CODE}in localStorage. Reloading a room recovers the token and opens a new event connection. This is intentionally lightweight MVP identity, not user authentication; see Security.
State updates
- Command responses may immediately replace the current room.
- Real-time events also carry complete room snapshots.
- The client accepts a snapshot only when its version is at least the currently rendered version.
- A locally selected card is retained for feedback because unrevealed server snapshots intentionally omit vote values.
- A snapshot that removes the participant's submitted vote clears the local selection, including resets initiated by another client.
- A failed vote restores the previously selected card, and pending commands disable controls that could submit duplicates.
- Room initialization has explicit loading, joining, connected, and unavailable states. Temporary failures retain stored identity; definitive authorization or not-found responses remove it.
- When the facilitator ends a session after a reveal, every client receives the persisted terminal snapshot and renders shared awards plus a per-participant statistics table.
Appearance
The appearance control cycles through system, light, and dark modes. System mode follows prefers-color-scheme and responds when the operating-system preference changes. A manual light or dark override is stored in localStorage under poker:theme; returning to system mode removes the override. A small script in app.html applies the effective theme before hydration to avoid a light/dark flash.
Table layout
During an active round, participants are distributed evenly around an elliptical table in server join order, rotated so the current participant sits at the bottom of their view. The keyed participant seats transition to new positions when the team changes. Submitted votes animate from each seat to a face-down card position on the nearby table edge. When the facilitator reveals the round, the cards flip and move into centered vertical piles for equal estimates; those piles follow the configured deck order from left to right, while equal estimates retain seating order within their pile. Participant and card sizes become progressively more compact for large teams and narrow screens; reduced-motion preferences disable these transitions without changing the final state.
Phone voting
RoomSessionView owns one room session and chooses its presentation using a media query. Narrow portrait viewports, and coarse-pointer devices with limited landscape height, receive the focused mobile view by default. Participants can temporarily open the full table and return to phone mode without reconnecting or creating a second session.
The phone view shows room identity, connection state, vote status, errors, and a large touch-friendly card grid. It intentionally omits the topic, participant table, results, summary statistics, transport switcher, and facilitator controls because those remain visible and operable on the shared screen. After reveal or session completion, the phone directs the participant back to that screen.
Transport abstraction
RoomTransport exposes three operations:
interface RoomTransport {
connect({ onRoom, onStatus, onError }): void
command(type, payload?): Promise<Room | undefined>
close(): void
}HttpSseTransport and WebSocketTransport both satisfy this contract. Components and room state do not need to know which network protocol is active. The connection badge switches transport at runtime for demonstration and parity testing. SSE uses the browser's native reconnection; WebSocket reconnects with bounded exponential backoff and rejects pending commands when a connection closes. Session generation checks prevent callbacks from replaced transports from mutating current state.
Styling
Global CSS defines light and dark color palettes, document defaults, and shared form and control styles. Views and components own their scoped layout, responsive, dark-mode, and animation styles. Interactive controls include visible keyboard focus styles, semantic button labels, pressed-state semantics for vote cards, and non-color vote indicators.