diff --git a/web/frontend/app/features/home/components/ThemeTryOut.stories.tsx b/web/frontend/app/features/home/components/ThemeTryOut.stories.tsx index d8275d3a..c39aca54 100644 --- a/web/frontend/app/features/home/components/ThemeTryOut.stories.tsx +++ b/web/frontend/app/features/home/components/ThemeTryOut.stories.tsx @@ -5,7 +5,7 @@ import { expect, within } from 'storybook/test'; import { biergartenThemes, defaultThemeName } from '../../theme/themes'; import ThemeTryOut from './ThemeTryOut'; -const themeTryOutDescription = `Landing page theme band. Hosts the shared \`ThemeSegmentedControl\` with short labels, so a visitor can switch the whole document theme from the marketing page. The control seeds its selection from the root route's loader data, so this story runs behind a memory data router whose \`root\` route supplies a theme.`; +const themeTryOutDescription = `Landing page theme band. Frames the shared \`ThemeSegmentedControl\` in an elevated card with a "Set the mood" eyebrow and a pour count, and renders it with short labels so the four options fit the marketing page's narrower column. Each option carries its \`shortVibe\` mood line, and picking one switches the whole document theme. The control seeds its selection from the root route's loader data, so this story runs behind a memory data router whose \`root\` route supplies a theme. \`Themes/ThemeSegmentedControl\` covers the control itself.`; const ThemeTryOutHarness = () => { const [router] = useState(() => @@ -48,9 +48,15 @@ export const Default: Story = { canvas.getByRole('heading', { name: 'Pick a pour that suits you' }), ).toBeInTheDocument(); + await expect(canvas.getByText('Set the mood')).toBeVisible(); + await expect(canvas.getByText(`${biergartenThemes.length} pours`)).toBeVisible(); + const group = canvas.getByRole('radiogroup', { name: 'Theme selector' }); + await expect(within(group).getAllByRole('radio')).toHaveLength(biergartenThemes.length); + for (const theme of biergartenThemes) { - await expect(within(group).getByText(theme.shortLabel)).toBeInTheDocument(); + await expect(within(group).getByText(theme.shortLabel)).toBeVisible(); + await expect(within(group).getByText(theme.shortVibe)).toBeVisible(); } }, }; diff --git a/web/frontend/app/features/theme/components/ThemeSegmentedControl.stories.tsx b/web/frontend/app/features/theme/components/ThemeSegmentedControl.stories.tsx new file mode 100644 index 00000000..25ae5bcb --- /dev/null +++ b/web/frontend/app/features/theme/components/ThemeSegmentedControl.stories.tsx @@ -0,0 +1,167 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { useMemo } from 'react'; +import { createMemoryRouter, RouterProvider } from 'react-router'; +import { expect, fn, userEvent, within } from 'storybook/test'; +import { biergartenThemes, defaultThemeName, themeCookieName, type ThemeName } from '../themes'; +import ThemeSegmentedControl from './ThemeSegmentedControl'; + +const themeSegmentedControlDescription = `The shared theme picker, used by the theme guide and by the landing page's try-out band. Each option pairs a gradient swatch of the theme's palette with its name and \`shortVibe\` mood line, and selection is carried by both color and elevation. Picking an option sets \`data-theme\` on \`\` and writes the \`${themeCookieName}\` cookie, so the choice survives the next request and the server renders the same theme. The initial selection comes from the root route's loader, so these stories run behind a memory data router whose \`root\` route supplies the theme the cookie would have provided.`; + +interface HarnessProps { + labels?: 'full' | 'short'; + className?: string; + onChange?: (theme: ThemeName) => void; + /** Theme the root loader hands the control, standing in for the theme cookie. */ + seedTheme?: ThemeName; +} + +const ThemeSegmentedControlHarness = ({ + labels, + className, + onChange, + seedTheme = defaultThemeName, +}: HarnessProps) => { + // Rebuilt per arg change so the docs controls remount the control rather + // than leaving it seeded from the previous render. + const router = useMemo( + () => + createMemoryRouter([ + { + id: 'root', + path: '/', + loader: () => ({ theme: seedTheme }), + element: ( +