From f01e8ffe32253c6f08b528fc05a1d86e3b9847aa Mon Sep 17 00:00:00 2001 From: Nils Briggen Date: Mon, 8 Jun 2026 15:53:33 +0200 Subject: [PATCH] Phase 9: onboarding, mobile & accessibility - First-run welcome on the Campaigns page: what-the-app-is blurb, three feature highlights, and one-click "Try the sample campaign" (seedSampleCampaign) plus "Start your first campaign". - Map token palette starts collapsed on small screens so the map gets the width. - (Reduced-motion is already honoured globally in styles/globals.css.) Co-Authored-By: Claude Opus 4.8 (1M context) --- e2e/onboarding.spec.ts | 16 +++++++ src/features/campaigns/CampaignsPage.tsx | 54 +++++++++++++++++++----- src/features/world/map/MapEditor.tsx | 3 +- 3 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 e2e/onboarding.spec.ts diff --git a/e2e/onboarding.spec.ts b/e2e/onboarding.spec.ts new file mode 100644 index 0000000..1450de8 --- /dev/null +++ b/e2e/onboarding.spec.ts @@ -0,0 +1,16 @@ +import { test, expect } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.evaluate(async () => { + indexedDB.deleteDatabase('ttrpg-manager'); + localStorage.clear(); + }); + await page.reload(); +}); + +test('onboarding: first-run welcome loads the sample campaign', async ({ page }) => { + await expect(page.getByRole('heading', { name: 'Welcome to TTRPG Manager' })).toBeVisible(); + await page.getByRole('button', { name: 'Try the sample campaign' }).click(); + await expect(page.getByRole('heading', { name: 'Sample: Lost Mine' })).toBeVisible(); +}); diff --git a/src/features/campaigns/CampaignsPage.tsx b/src/features/campaigns/CampaignsPage.tsx index 04667b9..b950681 100644 --- a/src/features/campaigns/CampaignsPage.tsx +++ b/src/features/campaigns/CampaignsPage.tsx @@ -1,11 +1,12 @@ import { useState } from 'react'; import { useNavigate } from '@tanstack/react-router'; import { campaignsRepo } from '@/lib/db/repositories'; +import { seedSampleCampaign } from '@/lib/sample'; import { SYSTEM_OPTIONS } from '@/lib/rules'; import { useUiStore } from '@/stores/uiStore'; import { useCampaigns } from './hooks'; import type { Campaign } from '@/lib/schemas'; -import { Page, PageHeader, EmptyState } from '@/components/ui/Page'; +import { Page, PageHeader } from '@/components/ui/Page'; import { Button } from '@/components/ui/Button'; import { Modal } from '@/components/ui/Modal'; import { Field, Input, Select, Textarea } from '@/components/ui/Input'; @@ -28,15 +29,7 @@ export function CampaignsPage() { /> {campaigns.length === 0 ? ( - setCreating(true)}> - + New campaign - - } - /> + setCreating(true)} /> ) : (
{campaigns.map((c) => ( @@ -51,6 +44,47 @@ export function CampaignsPage() { ); } +function Welcome({ onCreate }: { onCreate: () => void }) { + const navigate = useNavigate(); + const setActive = useUiStore((s) => s.setActiveCampaign); + const [loading, setLoading] = useState(false); + const loadSample = async () => { + setLoading(true); + const id = await seedSampleCampaign(); + setActive(id); + void navigate({ to: '/dashboard' }); + }; + return ( +
+

Welcome to TTRPG Manager

+

+ A local-first companion for D&D 5e & Pathfinder 2e — guided character building, combat + tracking, a full battle-map VTT with fog & dynamic vision, a searchable compendium, and live + play your players join from their own devices. Everything is stored on this device. +

+
+ + + +
+
+ + +
+
+ ); +} + +function FeatCard({ icon, title, desc }: { icon: string; title: string; desc: string }) { + return ( +
+
{icon}
+
{title}
+
{desc}
+
+ ); +} + function CampaignCard({ campaign, onEdit }: { campaign: Campaign; onEdit: () => void }) { const navigate = useNavigate(); const setActive = useUiStore((s) => s.setActiveCampaign); diff --git a/src/features/world/map/MapEditor.tsx b/src/features/world/map/MapEditor.tsx index dd16181..1f4f765 100644 --- a/src/features/world/map/MapEditor.tsx +++ b/src/features/world/map/MapEditor.tsx @@ -53,7 +53,8 @@ export function MapEditor({ map, campaign }: { map: BattleMap; campaign: Campaig const [gmDraw, setGmDraw] = useState(true); const [dims, setDims] = useState({ cols: 0, rows: 0 }); const [editToken, setEditToken] = useState(null); - const [showPalette, setShowPalette] = useState(true); + // Collapsed by default on small screens so the map gets the width. + const [showPalette, setShowPalette] = useState(() => typeof window === 'undefined' || window.innerWidth >= 880); // transient interaction state const [overlay, setOverlay] = useState({});