From 8a37b674fe27accccfdf3706f0c73230450762f5 Mon Sep 17 00:00:00 2001 From: cottongin Date: Sun, 23 Aug 2026 17:17:52 -0400 Subject: [PATCH] docs: add sticker visibility toggle spec, plan, and version bump Co-authored-by: Cursor --- docs/external-downtream-stickers.md | 162 +++++++++++ .../2026-08-23-sticker-visibility-toggle.md | 266 ++++++++++++++++++ ...-08-23-sticker-visibility-toggle-design.md | 122 ++++++++ frontend/src/config/branding.js | 2 +- 4 files changed, 551 insertions(+), 1 deletion(-) create mode 100644 docs/external-downtream-stickers.md create mode 100644 docs/superpowers/plans/2026-08-23-sticker-visibility-toggle.md create mode 100644 docs/superpowers/specs/2026-08-23-sticker-visibility-toggle-design.md diff --git a/docs/external-downtream-stickers.md b/docs/external-downtream-stickers.md new file mode 100644 index 0000000..7b5a6a0 --- /dev/null +++ b/docs/external-downtream-stickers.md @@ -0,0 +1,162 @@ +# Sticker Layer Visibility Control + +Integration guide for the Jackbox Game Picker to toggle sticker layer visibility on the vote-app overlay. + +## Overview + +The vote-app overlay shows stickers that voters purchase throughout a game session. By default, stickers are visible whenever a poll is displayed and hidden between polls. This API allows the Game Picker host to override that behavior — showing accumulated stickers between rounds (a "sticker wall showcase") or hiding them during a poll for a cleaner view. + +## Modes + +| Mode | Behavior | +|--------|-------------------------------------------------| +| `auto` | Default — stickers visible during polls, hidden between polls | +| `show` | Force stickers visible regardless of poll state | +| `hide` | Force stickers hidden regardless of poll state | + +**Auto-reset:** When a new poll is generated, mode automatically resets to `auto`. The Game Picker receives a notification so its UI can update. + +## WebSocket Integration (Primary) + +### Sending: Set Mode + +Send on the existing upstream WebSocket connection (`/api/sessions/live`): + +```json +{ + "type": "stickers.setMode", + "data": { + "mode": "show" + } +} +``` + +Valid values for `mode`: `"auto"`, `"show"`, `"hide"`. + +### Receiving: Mode Changed + +The vote-app sends this whenever the mode changes (including auto-resets): + +```json +{ + "type": "stickers.mode", + "mode": "auto" +} +``` + +**When you'll receive this:** +- After you send `stickers.setMode` (confirmation) +- When a new poll starts (mode resets to `auto`) +- When a session starts/ends (mode resets to `auto`) +- When the debug panel changes the mode + +**Use this to keep your UI in sync.** If you show a toggle button, update its state whenever you receive this message. + +## HTTP API (Fallback) + +### Set Mode + +``` +PUT /api/stickers/visibility +``` + +**Headers:** +- `Content-Type: application/json` +- `X-API-Key: ` + +**Request body:** +```json +{ "mode": "show" } +``` + +**Response (200):** +```json +{ "mode": "show", "previousMode": "auto" } +``` + +**Error responses:** +- `403` — Invalid or missing API key +- `400` — Invalid mode value (not `auto`/`show`/`hide`) + +### Get Current Mode + +``` +GET /api/stickers/visibility +``` + +No auth required. + +**Response (200):** +```json +{ "mode": "auto" } +``` + +Use this on startup to sync your UI state. + +## Suggested UX + +### Simple Toggle Button + +A two-state toggle that alternates between `show` and `auto`: + +``` +[Show Stickers] ← when mode is "auto" or "hide" +[Hide Stickers] ← when mode is "show" +``` + +When receiving `stickers.mode` with `"auto"`, reset the button to "Show Stickers" state. + +### Three-State Control (Advanced) + +If you prefer explicit control: + +``` +( ) Auto — stickers follow poll visibility +(•) Show — always visible +( ) Hide — always hidden +``` + +Highlight the current selection. Update when receiving `stickers.mode`. + +### Handling Auto-Reset + +When a poll starts, mode resets and you'll receive: +```json +{ "type": "stickers.mode", "mode": "auto" } +``` + +Your UI should: +1. Update the button/toggle state to reflect `auto` +2. Optionally show a brief indicator ("Sticker mode reset") + +## Example Flows + +### Show stickers between games + +1. Game ends, poll results shown, winner selected +2. Host wants to show off sticker wall before next game +3. Host clicks "Show Stickers" → sends `stickers.setMode` with `mode: "show"` +4. Sticker layer becomes visible on the overlay +5. Next poll starts → mode auto-resets to `auto` +6. Game Picker receives `stickers.mode: "auto"` → updates button + +### Hide stickers during a busy poll + +1. Poll is active, stickers are distracting +2. Host clicks "Hide Stickers" → sends `stickers.setMode` with `mode: "hide"` +3. Sticker layer hides on the overlay +4. Poll ends, new poll starts → mode auto-resets to `auto` +5. Stickers show normally with the new poll + +### Sync on reconnect + +1. Game Picker reconnects to vote-app +2. Call `GET /api/stickers/visibility` to get current mode +3. Update UI to match + +## Notes + +- Stickers accumulate in the DOM regardless of visibility — toggling just shows/hides the layer +- The sticker layer is at z-index 1, behind the poll overlay (z-index 9997) +- Sticker data persists across server restarts (stored in SQLite) +- Stickers are cleared on `session.started` and `session.ended` events diff --git a/docs/superpowers/plans/2026-08-23-sticker-visibility-toggle.md b/docs/superpowers/plans/2026-08-23-sticker-visibility-toggle.md new file mode 100644 index 0000000..2165b22 --- /dev/null +++ b/docs/superpowers/plans/2026-08-23-sticker-visibility-toggle.md @@ -0,0 +1,266 @@ +# Sticker Visibility Toggle Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a 3-way segmented toggle (Auto/Show/Hide) to the Picker screen that lets admins control sticker layer visibility on the downstream vote-app overlay via WebSocket. + +**Architecture:** State (`stickerMode`) and UI live in the `Picker` component. `SessionInfo` (which owns the WebSocket connection) handles inbound `stickers.mode` events via a setter prop and exposes a send function to `Picker` via a ref. This follows the existing pattern used for poll state, leading game, etc. + +**Tech Stack:** React 18, Tailwind CSS, WebSocket (existing `/api/sessions/live` connection) + +## Global Constraints + +- All changes are in `frontend/src/pages/Picker.jsx` (contains both `Picker` and `SessionInfo` components) +- Mode values are exactly `"auto"`, `"show"`, `"hide"` (matching the downstream API) +- Default mode is `"auto"` +- Follow existing code patterns — local `useState`, props passed to `SessionInfo`, Tailwind utility classes +- No new dependencies + +--- + +## File Structure + +| File | Action | Responsibility | +|------|--------|----------------| +| `frontend/src/pages/Picker.jsx` | Modify | Add `stickerMode` state + `stickerSendRef` ref to `Picker`, render sticker toggle card in right column, add `setStickerMode` + `stickerSendRef` props to `SessionInfo`, handle `stickers.mode` WS events, populate send ref | + +Single-file change. Both `Picker` and `SessionInfo` are defined in this file. + +--- + +### Task 1: Wire sticker state and WebSocket plumbing + +**Files:** +- Modify: `frontend/src/pages/Picker.jsx:20-34` (Picker state declarations) +- Modify: `frontend/src/pages/Picker.jsx:1282-1297` (SessionInfo props) +- Modify: `frontend/src/pages/Picker.jsx:1304` (SessionInfo function signature) +- Modify: `frontend/src/pages/Picker.jsx:1385-1396` (WS auth_success handler, populate stickerSendRef) +- Modify: `frontend/src/pages/Picker.jsx:1470-1477` (WS message handler, add stickers.mode) +- Modify: `frontend/src/pages/Picker.jsx:1487-1491` (WS onclose, clear stickerSendRef) +- Modify: `frontend/src/pages/Picker.jsx:1498` (connectWs dependency array) + +**Interfaces:** +- Consumes: nothing (first task) +- Produces: + - `stickerMode` state (`'auto' | 'show' | 'hide'`) in Picker + - `stickerSendRef` ref (`.current` is `(mode: string) => void` when WS connected, `null` otherwise) + - `setStickerMode` and `stickerSendRef` passed as props to SessionInfo + +- [ ] **Step 1: Add stickerMode state and stickerSendRef to Picker** + +In `frontend/src/pages/Picker.jsx`, after line 34 (`const [sessionEnded, setSessionEnded] = useState(false);`), add: + +```jsx + const [stickerMode, setStickerMode] = useState('auto'); + const stickerSendRef = useRef(null); +``` + +- [ ] **Step 2: Pass new props to SessionInfo** + +In `frontend/src/pages/Picker.jsx`, find the ` +``` + +- [ ] **Step 3: Accept new props in SessionInfo function signature** + +In `frontend/src/pages/Picker.jsx`, update the `SessionInfo` function signature (line 1304) to destructure the two new props: + +```jsx +function SessionInfo({ sessionId, onGamesUpdate, playingGame, setPlayingGame, setHasPlayedGames, setLeadingGame, setPollActive, pollActiveRef, setPollResult, setPollEndingAt, setShowEndPollOptions, pollStartedAtRef, setSelectedGame, setGameSource, setStickerMode, stickerSendRef }) { +``` + +- [ ] **Step 4: Populate stickerSendRef after WS authentication** + +In the `connectWs` function, inside the `if (message.type === 'auth_success')` block (around line 1385), after the ping interval setup and before the `return;`, add the ref assignment: + +```jsx + if (message.type === 'auth_success') { + console.log('[WebSocket] Authenticated, subscribing to session', sessionId); + ws.send(JSON.stringify({ type: 'subscribe', sessionId: parseInt(sessionId) })); + + clearInterval(pingIntervalRef.current); + pingIntervalRef.current = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'ping' })); + } + }, 30000); + + stickerSendRef.current = (mode) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'stickers.setMode', data: { mode } })); + } + }; + + return; + } +``` + +- [ ] **Step 5: Handle inbound stickers.mode WS events** + +In the `ws.onmessage` handler, after the `game.dismissed` block (around line 1468) and before the `reloadEvents.includes` check, add: + +```jsx + if (message.type === 'stickers.mode') { + setStickerMode(message.mode); + return; + } +``` + +- [ ] **Step 6: Clear stickerSendRef on WS disconnect** + +In the `ws.onclose` handler (around line 1487), add the ref cleanup alongside the existing cleanup: + +```jsx + ws.onclose = () => { + console.log('[WebSocket] Disconnected, reconnecting in 3s...'); + clearInterval(pingIntervalRef.current); + stickerSendRef.current = null; + reconnectTimeoutRef.current = setTimeout(connectWs, 3000); + }; +``` + +- [ ] **Step 7: Update connectWs dependency array** + +Update the `useCallback` dependency array for `connectWs` (line 1498) to include the new dependencies: + +```jsx + }, [sessionId, token, loadGames, setPollActive, setPollResult, setPollEndingAt, setShowEndPollOptions, setLeadingGame, pollActiveRef, pollStartedAtRef, setSelectedGame, setGameSource, setStickerMode, stickerSendRef]); +``` + +- [ ] **Step 8: Verify no lint errors** + +Run linter on `frontend/src/pages/Picker.jsx` and fix any issues introduced. + +- [ ] **Step 9: Commit** + +```bash +git add frontend/src/pages/Picker.jsx +git commit -m "feat: wire sticker mode state and WebSocket plumbing" +``` + +--- + +### Task 2: Render the sticker toggle UI with pulse animation + +**Files:** +- Modify: `frontend/src/pages/Picker.jsx:34-35` (add stickerPulse state, near other Picker state) +- Modify: `frontend/src/pages/Picker.jsx:868-875` (insert toggle card above poll leader indicator in right column) + +**Interfaces:** +- Consumes: + - `stickerMode` state (`'auto' | 'show' | 'hide'`) from Task 1 + - `stickerSendRef` ref from Task 1 + - `setStickerMode` setter from Task 1 +- Produces: Rendered sticker toggle card in Picker's right column + +- [ ] **Step 1: Add stickerPulse state for auto-reset animation** + +In `frontend/src/pages/Picker.jsx`, right after the `stickerSendRef` line added in Task 1 (after `const stickerSendRef = useRef(null);`), add: + +```jsx + const [stickerPulse, setStickerPulse] = useState(false); +``` + +- [ ] **Step 2: Add pulse trigger logic** + +After the `stickerPulse` state declaration, add a `useEffect` that watches `stickerMode` for auto-reset events: + +```jsx + const prevStickerModeRef = useRef(stickerMode); + useEffect(() => { + if (stickerMode === 'auto' && prevStickerModeRef.current !== 'auto') { + setStickerPulse(true); + const timer = setTimeout(() => setStickerPulse(false), 2000); + return () => clearTimeout(timer); + } + prevStickerModeRef.current = stickerMode; + }, [stickerMode]); +``` + +- [ ] **Step 3: Render the sticker toggle card** + +In the right column's `{/* Results Panel */}` div, after the error display and **before** the `{/* Poll Leader Indicator */}` comment (line 875), insert the sticker toggle card: + +```jsx + {/* Sticker Visibility Toggle */} +
+ Stickers +
+ {[ + { value: 'auto', label: 'Auto', activeClass: 'bg-gray-500 text-white' }, + { value: 'show', label: 'Show', activeClass: 'bg-green-500 text-white' }, + { value: 'hide', label: 'Hide', activeClass: 'bg-red-500 text-white' }, + ].map((opt, i) => ( + + ))} +
+
+``` + +- [ ] **Step 4: Verify no lint errors** + +Run linter on `frontend/src/pages/Picker.jsx` and fix any issues introduced. + +- [ ] **Step 5: Manual smoke test** + +Start the dev server and verify: + +1. The sticker toggle card appears above the poll leader / poll control cards in the right column +2. All three segments (Auto/Show/Hide) are clickable and highlight correctly +3. Auto shows gray, Show shows green, Hide shows red when active +4. Inactive segments show the muted/white style +5. The card is compact and single-row on desktop + +Run: `cd frontend && npm run dev` + +Open the Picker page in a browser while logged in with an active session. Click each segment and confirm visual feedback. + +- [ ] **Step 6: Commit** + +```bash +git add frontend/src/pages/Picker.jsx +git commit -m "feat: add sticker visibility toggle UI with pulse animation" +``` diff --git a/docs/superpowers/specs/2026-08-23-sticker-visibility-toggle-design.md b/docs/superpowers/specs/2026-08-23-sticker-visibility-toggle-design.md new file mode 100644 index 0000000..6f427c6 --- /dev/null +++ b/docs/superpowers/specs/2026-08-23-sticker-visibility-toggle-design.md @@ -0,0 +1,122 @@ +# Sticker Visibility Toggle — Design Spec + +## Overview + +Add a 3-way segmented toggle to the Picker's Game Session screen that lets admins control sticker layer visibility on the downstream vote-app overlay. The three modes are **Auto** (default), **Show**, and **Hide**, matching the downstream API contract defined in `docs/external-downstream-stickers.md`. + +## Motivation + +The vote-app overlay shows stickers that voters purchase during a session. By default, stickers follow poll state (visible during polls, hidden between). Admins need the ability to override this — force-showing stickers between rounds for a "sticker wall showcase" or force-hiding them during a busy poll for a cleaner view. + +## Modes + +| Mode | Behavior | +|------|----------| +| `auto` | Default — stickers visible during polls, hidden between polls | +| `show` | Force stickers visible regardless of poll state | +| `hide` | Force stickers hidden regardless of poll state | + +When a new poll starts, the downstream vote-app auto-resets mode to `auto` and sends a `stickers.mode` event. + +## Architecture + +### State ownership + +- `stickerMode` state (`useState('auto')`) lives in the `Picker` component, where the toggle UI renders. +- A `stickerSendRef` ref is created in `Picker` and passed to `SessionInfo`, which populates it with a send function once the WebSocket connection is authenticated. + +### Props + +`SessionInfo` receives two new props (following the existing pattern for `setPollActive`, `setLeadingGame`, etc.): + +- `setStickerMode` — setter called when inbound `stickers.mode` WS events arrive. +- `stickerSendRef` — ref that SessionInfo populates with `(mode) => ws.send(...)`. + +### WebSocket integration + +All communication uses the existing `/api/sessions/live` WebSocket connection managed by `SessionInfo.connectWs`. + +**Receiving (`stickers.mode` events):** + +```javascript +if (message.type === 'stickers.mode') { + setStickerMode(message.mode); + return; +} +``` + +Added alongside the existing `poll.*` and `game.*` handlers in the `ws.onmessage` callback. + +**Sending (`stickers.setMode` commands):** + +After successful auth and subscribe, SessionInfo populates the ref: + +```javascript +stickerSendRef.current = (mode) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'stickers.setMode', data: { mode } })); + } +}; +``` + +The ref is cleared on disconnect/cleanup. + +**No HTTP fallback.** The default `auto` state is correct on fresh load, and the WS connection handles all sync from that point. + +## UI Design + +### Placement + +The sticker toggle renders in Picker's right column (`md:col-span-2`), **above** the poll leader indicator and poll control cards. It is always visible when an active session exists, regardless of poll state. + +``` +┌─────────────────────────────────────────────┐ +│ Stickers [Auto] [Show] [Hide] │ +└─────────────────────────────────────────────┘ + ↕ poll leader indicator (conditional) + ↕ currently playing card (conditional) + ↕ poll control card (conditional) + ↕ SessionInfo games list +``` + +### Control style + +A compact single-row card with: +- **Label** ("Stickers") on the left. +- **3-way segmented control** on the right, matching the Drawing/Family Friendly filter pattern (three adjacent buttons, active one highlighted). + +### Segment colors + +| Segment | Active background | +|---------|-------------------| +| Auto | Neutral gray (`bg-gray-500`) | +| Show | Green (`bg-green-500`) | +| Hide | Red (`bg-red-500`) | + +Inactive segments use a muted/transparent style with border, same as the existing filter toggles. + +### Auto-reset pulse + +When a `stickers.mode` event arrives with `mode: "auto"` and the current local state is not already `auto`, a brief CSS pulse animation plays on the toggle card: + +- A gentle border/background highlight flash that fades over ~2 seconds. +- Implemented via a CSS `@keyframes` animation toggled by a transient state flag. +- The flag is set on receiving an auto-reset event and cleared after the animation completes. + +### Interaction behavior + +- Clicking a segment calls `stickerSendRef.current(mode)` and optimistically updates local state. +- If the subsequent `stickers.mode` event returns a different value, state corrects to match. +- The toggle is visually disabled (reduced opacity, no pointer events) if the WebSocket is not connected. + +## Files changed + +| File | Change | +|------|--------| +| `frontend/src/pages/Picker.jsx` | Add `stickerMode` state, `stickerSendRef` ref, render sticker toggle card, pass new props to `SessionInfo` | +| `frontend/src/pages/Picker.jsx` (`SessionInfo`) | Accept `setStickerMode` and `stickerSendRef` props, handle `stickers.mode` WS events, populate send ref | + +## Scope boundaries + +- **In scope:** Frontend toggle UI + WS message send/receive in the game picker. +- **Out of scope:** Backend relay of `stickers.setMode` messages to the vote-app (assumed handled by existing infrastructure or a separate task). Vote-app sticker rendering (already implemented downstream). diff --git a/frontend/src/config/branding.js b/frontend/src/config/branding.js index 73867c4..4229d48 100644 --- a/frontend/src/config/branding.js +++ b/frontend/src/config/branding.js @@ -2,7 +2,7 @@ export const branding = { app: { name: 'HSO Jackbox Game Picker', shortName: 'Jackbox Game Picker', - version: '0.7.13 - Pokémon-Go-To-The-Polls Edition', + version: '0.8.0 - Stickers That Stick Edition', description: 'Spicing up Hyper Spaceout game nights!', }, meta: {