docs(launcher): add Call to Play specification and design reference
Extend the launcher design specification and prototype reference with the "Call to Play" multiplayer coordination feature and clean up top-bar chrome. Key additions and changes: - Spec & Roadmap: Document Call to Play mechanics in SPEC.md, including Play Now / Scheduled call flavors, 15-minute check-in windows, top-bar button with active count badge, quick-bar ticker stack above grid, overlay modal, and per-call group chat. Update design/README.md. - Components & Logic: Add calltoplay.jsx and ctp-chat.jsx components for call creation, status progression, and chat. Extend data.jsx with a mock peer roster (PEERS) and helper functions for peer install count tracking. - Top-bar Cleanup: Move game-folder configuration from top bar into Settings -> Library, freeing top-bar space for the Call to Play action button. - Styling & Layout: Add CTP quick-bar, ticker, badge, card, and chat styles to styles.css, and integrate CTP components into launcher.jsx, components.jsx, and SoftLAN Launcher.html. Test Plan: - `git diff --cached --check` -- passed (no trailing whitespace or conflict markers) - `cargo check --workspace` -- passed cleanly - Manual review of staged diff across 9 design/launcher files -- confirmed clean staging
This commit is contained in:
+163
-4
@@ -32,6 +32,20 @@ The HTML mock includes two chrome variants — **A (single-row)** and **B (two-r
|
||||
|
||||
---
|
||||
|
||||
## Changes since v4
|
||||
|
||||
- **Call to Play added.** A whole new coordination layer on top of the library:
|
||||
rally the LAN around a game and a time. Two flavors — **Play now** (give
|
||||
people N minutes to ready up) and **Scheduled** ("Among Us at 20:00", collect
|
||||
"I'm in" RSVPs, then a **check-in window** opens 15 min before start). Every
|
||||
call carries a small group **chat**. Surfaced in three places: a **Call to
|
||||
Play button** in the top bar (with an active-call badge), a persistent stack
|
||||
of **quick bars** above the grid, and a full **overlay** with per-call cards
|
||||
and a create form. Full spec in the new **"Call to Play"** section below.
|
||||
New source files: `calltoplay.jsx`, `ctp-chat.jsx`. New data: a mock LAN
|
||||
**peer roster** (`PEERS`) in `data.jsx`. New icons: `flag`, `chat`, `send`,
|
||||
`clock`, `caretUp`, `caretDown`.
|
||||
|
||||
## Changes since v3
|
||||
|
||||
- **Game-folder button removed from the top bar.** Setting the games directory is a one-time action — it doesn't deserve permanent real estate in the chrome. The button is gone from both top-bar variants, freeing the right zone for the kebab menu alone (variant A) / the storage meter + kebab pair (variant B).
|
||||
@@ -84,6 +98,7 @@ The default screen. A grid of game cards over a dark, gradient-tinted background
|
||||
- **Right zone (col 3, flex space-between with two sub-groups):**
|
||||
- **Sort menu** (pinned left, hugging search) — 36 px button, same surface style as search. Label `Sort: <bold value>` plus 13 px sort-bars icon and 11 px chevron. Click reveals dropdown menu below. Options: `Name (A–Z)`, `Size (largest)`, `Recently Played`, `Status`. This is the only thing on the *left* side of the right zone — it's part of the search cluster, so it hugs the search.
|
||||
- **Kebab menu** (`⋮`, pinned far-right) — 36×36 button with same surface as search. Menu items: `Settings` (opens Settings dialog), `Refresh library`, separator, `Unpack logs`, `About SoftLAN`. This is the only "app-level" control left in the top bar; the game-folder picker has moved into Settings.
|
||||
- **Call to Play button** (`.ctp-btn`, sits just left of the kebab in the far-right sub-group) — 36 px pill, flag icon + `Call to Play` label. Carries an accent-filled **badge** with the count of active (non-started) calls. Opens the Call to Play overlay. See the **"Call to Play"** section for the full feature. In variant B it lives in row 1's right group, between the storage meter and the kebab.
|
||||
|
||||
**Narrow-window fallback** (container width < 1100 px): the grid is replaced by a single `display: flex; flex-wrap: nowrap; gap: 16px` row. All items align left-to-right in source order (brand → filter → search → sort → kebab). The search field becomes `flex: 1 1 auto` so it absorbs remaining slack. The geometric centering is abandoned at narrow widths because there isn't enough horizontal slack for it to read cleanly. Implement via container query (`@container launcher (max-width: 1100px)`) on the launcher root; a viewport media query is an acceptable fallback if you're not using container queries yet.
|
||||
|
||||
@@ -416,6 +431,144 @@ Dot uses the live accent so it visually ties to the progress fill.
|
||||
|
||||
---
|
||||
|
||||
## Call to Play
|
||||
|
||||
A coordination layer that sits on top of the library: **rally the people on the
|
||||
LAN around a specific game at a specific time.** It answers the actual social
|
||||
question at a LAN party ("what are we all playing, and when?") that a plain
|
||||
library grid can't.
|
||||
|
||||
Source: `calltoplay.jsx` (the feature) + `ctp-chat.jsx` (per-call chat + shared
|
||||
`ctpAvatarColor` / `ctpFmtClock` helpers). Styles are the `ctp-*` classes in
|
||||
`styles.css`. Backed by the mock LAN **peer roster** `PEERS` in `data.jsx`.
|
||||
|
||||
### Two flavors of call
|
||||
|
||||
| | **Play now** | **Scheduled** |
|
||||
|---|---|---|
|
||||
| Set up with | game + max players + **duration** (5/10/15/30/60 min) | game + max players + **clock time** (24h, + which day) |
|
||||
| Others respond | `Ready now` or `+N minutes` | `I'm in` (RSVP), then check in later |
|
||||
| Resolves | when the roster fills **or** the timer runs out | at the scheduled time, after a check-in window |
|
||||
| Who starts it | the **caller** decides the actual launch | the **caller**, once people have checked in |
|
||||
|
||||
**Check-in window.** `CHECKIN_LEAD_MS = 15 min`. A scheduled call sits in the
|
||||
`scheduled` phase collecting RSVPs until 15 minutes before its start time, then
|
||||
flips into the `checkin` phase: it lights up everywhere and everyone who said
|
||||
"I'm in" is nudged to answer with the same `Ready now` / `+N minutes` states as
|
||||
a play-now call. A play-now call is effectively "always in its check-in window."
|
||||
|
||||
**Every call carries a small group chat** (see "Per-call chat" below).
|
||||
|
||||
### Three surfaces
|
||||
|
||||
1. **Top-bar button** (`CallToPlayButton` / `.ctp-btn`) — flag icon + label +
|
||||
an accent badge counting active (non-`started`) calls. Opens the overlay.
|
||||
2. **Quick bars** (`CallToPlayTicker` / `.ctp-ticker-stack`) — a persistent
|
||||
stack rendered at the top of the grid area, **one row per active call**.
|
||||
Sorted **ready → starting-soon → the rest**, ties broken by whichever
|
||||
resolves/starts soonest (`TICKER_RANK` = ready 0, soon 1, call/scheduled 2;
|
||||
then by `deadline`). Clicking a row opens the overlay focused on that call.
|
||||
3. **Overlay** (`CallToPlayOverlay`) — a modal (same scrim/panel treatment as
|
||||
the other dialogs) with a header, a **Call a new match** button, the create
|
||||
form, and a list of **nomination cards** (started calls sink to the bottom).
|
||||
|
||||
### Status model
|
||||
|
||||
Two derived values drive everything (`calltoplay.jsx`):
|
||||
|
||||
- `phaseOf(call)` → `'now'` (no `scheduledFor`) · `'scheduled'` (>15 min out) ·
|
||||
`'checkin'` (within the 15-min lead).
|
||||
- `statusOf(call)` (quick-bar/label status) → `'started'` · `'ready'`
|
||||
(`readyCount >= maxPlayers`, or state `done`) · `'soon'` (`deadline - now ≤
|
||||
15 min`) · `'scheduled'` (has a clock time) · `'call'` (a plain play-now call).
|
||||
|
||||
Quick-bar labels + LED colors: **SCHEDULED**, **CALL TO PLAY**, **STARTING
|
||||
SOON**, **READY** (`TICKER_LABEL`), each with its own dot color via
|
||||
`.ctp-ticker-dot[data-status]`.
|
||||
|
||||
**Per-participant ready state:** `ready` (explicitly readied, or their `readyAt`
|
||||
countdown has elapsed) · `in` (RSVP'd to a scheduled call but not checked in
|
||||
yet) · `pending` (checked in with a `+N minutes` buffer, counting down). Shown
|
||||
as compact **ready-bubbles** in the quick bars (`MiniBubbles`: initials +
|
||||
green ring/check when ready, `+Nm` tag when pending, dimmed when just "in") and
|
||||
as larger `AvatarChip`s in the nomination card roster.
|
||||
|
||||
### Nomination card (`NominationCard`)
|
||||
|
||||
Top to bottom:
|
||||
|
||||
1. **Header** — square game cover + title + a sub-line: `Called by <creator> · N/M peers have it installed` (or `Scheduled by <creator> · starts at HH:MM · …`), and a **timer** on the right: a live `M:SS` countdown for play-now/check-in, the **clock time + "in N min"** for a scheduled call, `Ready`, or `Launching…`. Countdown urgency (`data-urgency` high/mid/low) tints it as time runs low.
|
||||
2. **Check-in note** — only in the `checkin` phase: a clock icon + "Starting soon — check-in is open" (or a personalized nudge if you RSVP'd).
|
||||
3. **Progress bar** — time remaining as a fill (accent, → green when done); hidden while a call is still in the far-out `scheduled` phase.
|
||||
4. **Roster** — `readyCount/maxPlayers ready` (scheduled shows `N in · up to M players`; check-in adds `· K not checked in yet`), then avatar chips for each participant plus empty slots up to `maxPlayers`.
|
||||
5. **Actions** — context-dependent on your role (**creator** / **participant** / **outsider**) and phase: `Ready now` + `+5/10/15/30m` buffer buttons, `I'm in` (RSVP), `Start now` / `Add 5 more minutes` (creator once resolved), `Leave` / `Can't make it`, or a status note.
|
||||
6. **Chat** — the collapsible per-call chat panel.
|
||||
7. **Cancel** — creators get a `Cancel this call` link with an inline confirm.
|
||||
|
||||
### Create form (`CreateNominationForm`)
|
||||
|
||||
Game search (typeahead over the catalog) → on pick, max-players defaults to the
|
||||
game's parsed player cap (`parseMaxPlayers`). **When** toggles `Now` vs
|
||||
`Schedule`. `Now` reveals the **Give people** duration chips. `Schedule` reveals
|
||||
a **24-hour time picker** (hour/minute steppers **and** a "Type a time" free-text
|
||||
field accepting `20:00` / `2000` / `9:30`) plus **day chips** (Today + next two
|
||||
days). The confirm button reads `Call it — <game>` or `Schedule it — <game> ·
|
||||
<day> <time>`.
|
||||
|
||||
### Per-call chat (`CtpChat`, `ctp-chat.jsx`)
|
||||
|
||||
Collapsed to a one-line toggle showing an unread badge + a preview of the last
|
||||
message; expands to a compact IRC-style list (colored usernames via
|
||||
`ctpAvatarColor`, `HH:MM` timestamps) + an input. All chat state is local to the
|
||||
card. Usernames are colored deterministically by a hash of the name.
|
||||
|
||||
### Data shape
|
||||
|
||||
```ts
|
||||
type Nomination = {
|
||||
id: string;
|
||||
gameId: string;
|
||||
creator: string; // username of the caller
|
||||
maxPlayers: number;
|
||||
createdAt: number; // ms epoch
|
||||
scheduledFor: number | null; // ms epoch clock time; null = play-now
|
||||
deadline: number; // play-now: createdAt + durationMin; scheduled: === scheduledFor
|
||||
participants: Record<string, { // keyed by username
|
||||
status: 'ready' | 'in' | 'pending';
|
||||
joinedAt: number;
|
||||
readyAt?: number; // ms epoch a 'pending' buffer elapses
|
||||
}>;
|
||||
messages: { id: string; from: string; text: string; at: number }[];
|
||||
state: 'open' | 'done' | 'started';
|
||||
startedAt?: number;
|
||||
};
|
||||
```
|
||||
|
||||
The `useNominations({ username, seed })` hook owns the list and exposes
|
||||
`createNomination`, `respond`, `rsvp`, `sendMessage`, `leave`, `cancel`,
|
||||
`startNow`, `addTime`.
|
||||
|
||||
### Mock vs production
|
||||
|
||||
The mock **simulates other people** with a 1-second `setInterval`
|
||||
(`tickNomination`): bots RSVP to scheduled calls, ready-up during check-in, walk
|
||||
in late, and occasionally post a chat line — purely so the demo resolves
|
||||
visibly (bots use second-scale buffers; real people use the minute-scale ones).
|
||||
The mock also seeds a few representative calls on mount (a live call with chat,
|
||||
a fresh call you started, a scheduled call whose check-in window just opened,
|
||||
and one scheduled for later collecting RSVPs).
|
||||
|
||||
**In production, replace the simulation with a real-time LAN transport:** the
|
||||
launcher instances broadcast nominations, responses, RSVPs, and chat messages to
|
||||
each other (whatever the app uses — a small pub/sub over the LAN, a lightweight
|
||||
signaling server, or Tauri-side networking). Swap the guts of `useNominations`
|
||||
(the `setInterval` + local `setNoms`) for your networking layer that pushes the
|
||||
same state shape; the rendering components need nothing else. The `username` that
|
||||
identifies "you" comes from `settings.username` (the Profile setting), not a
|
||||
prop default.
|
||||
|
||||
---
|
||||
|
||||
## Filter controls — variant B (not used, kept for reference)
|
||||
|
||||
The two-row chrome has a different filter style — **underlined tabs with counts**, like browser tabs:
|
||||
@@ -570,7 +723,7 @@ Cover art in the design files is **stylized placeholder art** — generated enti
|
||||
|
||||
In the production app, the launcher should ideally use real cover-art when available (fetch from IGDB / Steam / local game folder) and fall back to the placeholder generator for games without art. The placeholder generator is in `design_reference/components.jsx → GameCover`.
|
||||
|
||||
The icon set (search, play, **server**, install, download, folder, kebab, sort, users, close, check, chevron, trash) is in `design_reference/components.jsx → Icon`. They are 12-14px inline SVGs using `currentColor`. Reuse as-is or substitute with the codebase's existing icon library at the same visual weight. The `server` glyph is new in this round — two stacked rounded rectangles with LED dots, used only on the Start Server button.
|
||||
The icon set (search, play, **server**, install, download, folder, kebab, sort, users, close, check, chevron, trash, **flag**, **clock**, **chat**, **send**, **caretUp**, **caretDown**) is in `design_reference/components.jsx → Icon`. They are 12-14px inline SVGs using `currentColor`. Reuse as-is or substitute with the codebase's existing icon library at the same visual weight. The `server` glyph drives the Start Server button; `flag` / `clock` / `chat` / `send` / `caretUp` / `caretDown` are used by Call to Play.
|
||||
|
||||
Fonts to load:
|
||||
```html
|
||||
@@ -587,17 +740,22 @@ Fonts to load:
|
||||
design_reference/
|
||||
├── SoftLAN Launcher.html ← entry; wires React + Babel, mounts <App>
|
||||
├── styles.css ← all visual styles (CSS custom props + components)
|
||||
├── data.jsx ← mock GAMES array + filter/sort helpers + STORAGE mock
|
||||
├── data.jsx ← mock GAMES array + PEERS roster + filter/sort helpers + STORAGE mock
|
||||
├── components.jsx ← Icon, GameCover, StateChip, ActionButton, GameCard,
|
||||
│ SegmentedFilters, UnderlineFilters, SearchField,
|
||||
│ SortMenu, StorageMeter, KebabMenu,
|
||||
│ GameDetailModal, SettingsDialog (incl. GameFolderField)
|
||||
└── launcher.jsx ← <Launcher> component composing chrome + grid + modals
|
||||
├── ctp-chat.jsx ← CtpChat panel + ctpAvatarColor / ctpFmtClock helpers
|
||||
├── calltoplay.jsx ← Call to Play: useNominations hook, quick bars,
|
||||
│ overlay, nomination cards, create form
|
||||
└── launcher.jsx ← <Launcher> component composing chrome + grid +
|
||||
modals + Call to Play
|
||||
```
|
||||
|
||||
To preview the design in a browser:
|
||||
1. Open `SoftLAN Launcher.html` in a static-server (e.g. `python -m http.server` from the folder).
|
||||
2. You'll see a design canvas with all variants (A, B, C, D, E, F) side-by-side. Click an artboard's expand button to view it full-screen.
|
||||
2. You'll see a design canvas with all variants side-by-side. Click an artboard's expand button to view it full-screen.
|
||||
- **Call to Play** (top section) — the quick-bar main view, and the same with the overlay open (live call + chat, your call, a check-in nudge, and a scheduled RSVP). Click anything to interact live — create a call, ready up, RSVP, chat.
|
||||
- **A / B** — chrome variants (A is the chosen direction)
|
||||
- **C** — detail overlay for an installed, server-capable game (Counter-Strike 1.6) → shows **Play + Start Server + Uninstall**
|
||||
- **D** — detail overlay for a downloaded-but-not-installed game (CoD 4) → shows **Install + Delete from disk**
|
||||
@@ -615,4 +773,5 @@ To preview the design in a browser:
|
||||
- **Progress state** — designed. See "Download progress" section above. The action-button slot is swapped for a live `DownloadProgress` component (card + modal variants with container-query fallback for narrow tiles). Wire it to your real progress events; the rendering layer is dev-ready.
|
||||
- **Keyboard arrow nav** — arrow keys should move focus between cards in the grid; not implemented in the mock but mentioned as a goal.
|
||||
- **"Server running" state** — once Start Server actually spawns a process, the button should switch to a *running* state (live indicator dot + "Server running" label + click-to-stop). Not designed this round — flag for follow-up alongside whatever server-status panel the app grows.
|
||||
- **Call to Play real-time transport** — the feature is fully designed and interactive, but the mock fakes other players with a local `setInterval` simulation. Production needs a real LAN transport that broadcasts nominations / responses / RSVPs / chat between launcher instances and pushes the same state shape into `useNominations`. Notifications when a call you're in enters its check-in window (OS notification / tray) are also a follow-up.
|
||||
- **German translations** — the language toggle is wired in Settings, but the catalog of translated UI strings hasn't been compiled. Stand up `react-i18next` (or equivalent) and seed `en.json` from the existing copy; `de.json` is a translation task for whoever owns localization.
|
||||
|
||||
Reference in New Issue
Block a user