Phase 8: session tooling — handouts to players

- GM "📣 Handout" composer in the header (title + body + optional image) sets
  uiStore.activeHandout; a clear button hides it.
- snapshot gains an optional handout {title, body, imageId}; buildSnapshot streams
  the handout image over the existing image channel (id "handout").
- Player view (local + networked) shows the handout as a prominent card above the
  boards. resizeToMax() keeps handout images compact.
- (Live scene/map switching already works via "Show to players".)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 15:48:23 +02:00
parent f923375ebb
commit 7f1ab8f094
10 changed files with 125 additions and 6 deletions
+16
View File
@@ -39,3 +39,19 @@ test('player view shows the party and hides enemy HP numbers', async ({ page })
await expect(page.getByText('Bandit')).toBeVisible();
await expect(page.getByText(/Healthy|Bloodied|Down/)).toBeVisible();
});
test('handout: GM pushes a handout to the player view', async ({ page }) => {
await page.getByRole('button', { name: '+ New campaign' }).first().click();
await page.locator('input[data-autofocus]').fill('Handout Camp');
await page.getByRole('button', { name: 'Create' }).click();
await page.getByRole('button', { name: /Handout/ }).click();
await page.getByLabel('Handout title').fill('A Mysterious Letter');
await page.getByLabel('Handout text').fill('Meet me at midnight.');
await page.getByRole('button', { name: 'Show to players' }).click();
await page.getByRole('link', { name: 'Dashboard' }).click();
await page.getByRole('link', { name: 'Player View' }).click();
await expect(page.getByRole('heading', { name: 'A Mysterious Letter' })).toBeVisible();
await expect(page.getByText('Meet me at midnight.')).toBeVisible();
});