Files
jackboxpartypack-gamepicker/docs/superpowers/plans/2026-08-23-sticker-visibility-toggle.md

11 KiB

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:

  const [stickerMode, setStickerMode] = useState('auto');
  const stickerSendRef = useRef(null);
  • Step 2: Pass new props to SessionInfo

In frontend/src/pages/Picker.jsx, find the <SessionInfo JSX (around line 1282). Add two new props after setGameSource={setGameSource}:

          <SessionInfo 
            sessionId={activeSession.id} 
            onGamesUpdate={gamesUpdateTrigger}
            playingGame={playingGame}
            setPlayingGame={setPlayingGame}
            setHasPlayedGames={setHasPlayedGames}
            setLeadingGame={setLeadingGame}
            setPollActive={setPollActive}
            pollActiveRef={pollActiveRef}
            setPollResult={setPollResult}
            setPollEndingAt={setPollEndingAt}
            setShowEndPollOptions={setShowEndPollOptions}
            pollStartedAtRef={pollStartedAtRef}
            setSelectedGame={setSelectedGame}
            setGameSource={setGameSource}
            setStickerMode={setStickerMode}
            stickerSendRef={stickerSendRef}
          />
  • 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:

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:

          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:

          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:

      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:

  }, [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
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:

  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:

  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:

          {/* Sticker Visibility Toggle */}
          <div
            className={`flex items-center justify-between border rounded-lg p-3 mb-4 transition-all duration-300 ${
              stickerPulse
                ? 'border-indigo-400 dark:border-indigo-500 bg-indigo-50 dark:bg-indigo-900/20 animate-pulse'
                : 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800'
            }`}
          >
            <span className="text-sm font-semibold text-gray-700 dark:text-gray-300">Stickers</span>
            <div className="flex border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden">
              {[
                { 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) => (
                <button
                  key={opt.value}
                  onClick={() => {
                    setStickerMode(opt.value);
                    stickerSendRef.current?.(opt.value);
                  }}
                  disabled={!stickerSendRef.current}
                  className={`px-3 py-1.5 text-xs font-medium transition ${
                    i === 1 ? 'border-x border-gray-300 dark:border-gray-600 ' : ''
                  }${
                    stickerMode === opt.value
                      ? opt.activeClass
                      : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300'
                  }${
                    !stickerSendRef.current ? ' opacity-50 cursor-not-allowed' : ''
                  }`}
                >
                  {opt.label}
                </button>
              ))}
            </div>
          </div>
  • 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
git add frontend/src/pages/Picker.jsx
git commit -m "feat: add sticker visibility toggle UI with pulse animation"