feat(ui): give the app one visual identity as Movie Loop

Gold is the primary in both themes, Bricolage Grotesque carries the
wordmark, titles and the score, and Movie A / Movie B wear the blue and
magenta of three-strip Technicolor on their posters instead of colouring
whole rows. The loop closes on screen - connectors light, a gold line
draws back to Movie A, the row glows, then the result arrives - in place
of confetti, on every play mode, scaled to the chain's length. Every
page shares one header and hairline lists; copy is sentence case with
one verb per flow, and the Home hero shows a real finished loop.

Also clamps link counts at zero: an opponent who had not moved yet
showed "-1 links".
This commit is contained in:
TehRiehlDeal committed 2026-09-12 22:42:55 -07:00
1 parent da32e49b0b
commit 8d0b7f39ee
122 files changed
+3129 -2089

No files matched your search

+14 -14
View File
@@ -91,14 +91,14 @@ afterEach(() => {
});
describe('filters panel', () => {
it('is hidden until "Show Filters" is toggled', async () => {
it('is hidden until "Show filters" is toggled', async () => {
renderPage();
expect(screen.queryByText('Genre')).toBeNull();
await userEvent.click(screen.getByRole('button', { name: 'Show Filters' }));
await userEvent.click(screen.getByRole('button', { name: 'Show filters' }));
expect(screen.getByText('Genre')).toBeInTheDocument();
expect(screen.getByText('Decade')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: 'Hide Filters' }));
await userEvent.click(screen.getByRole('button', { name: 'Hide filters' }));
expect(screen.queryByText('Genre')).toBeNull();
});
});
@@ -107,10 +107,10 @@ describe('starting a game', () => {
it('uses a preset pair for the chosen decade', async () => {
vi.spyOn(Math, 'random').mockReturnValue(0);
renderPage();
await userEvent.click(screen.getByRole('button', { name: 'Show Filters' }));
await userEvent.click(screen.getByRole('button', { name: 'Show filters' }));
await userEvent.click(screen.getByText('1970s'));
await userEvent.click(screen.getByRole('button', { name: 'Start Practice Game' }));
await userEvent.click(screen.getByRole('button', { name: 'Play' }));
expect(await screen.findByText('Game Page')).toBeInTheDocument();
expect(discoverSpy).not.toHaveBeenCalled();
@@ -122,14 +122,14 @@ describe('starting a game', () => {
it('falls back to a TMDB discover pair once a filter is reset to "Any"', async () => {
renderPage();
await userEvent.click(screen.getByRole('button', { name: 'Show Filters' }));
await userEvent.click(screen.getByRole('button', { name: 'Show filters' }));
await userEvent.click(screen.getByText('Comedy'));
await userEvent.click(screen.getByText('1970s'));
// Reset both filters back to unfiltered before starting.
await userEvent.click(screen.getAllByText('Any')[0] as HTMLElement);
await userEvent.click(screen.getAllByText('Any')[1] as HTMLElement);
await userEvent.click(screen.getByRole('button', { name: 'Start Practice Game' }));
await userEvent.click(screen.getByRole('button', { name: 'Play' }));
expect(await screen.findByText('Game Page')).toBeInTheDocument();
expect(discoverSpy).toHaveBeenCalledTimes(1);
@@ -144,11 +144,11 @@ describe('starting a game', () => {
);
vi.spyOn(Math, 'random').mockReturnValue(0);
renderPage();
await userEvent.click(screen.getByRole('button', { name: 'Show Filters' }));
await userEvent.click(screen.getByRole('button', { name: 'Show filters' }));
await userEvent.click(screen.getByText('1970s'));
await userEvent.click(screen.getByRole('button', { name: 'Start Practice Game' }));
const loadingButton = await screen.findByRole('button', { name: 'Loading...' });
await userEvent.click(screen.getByRole('button', { name: 'Play' }));
const loadingButton = await screen.findByRole('button', { name: 'Finding a pair…' });
expect(loadingButton).toBeDisabled();
release();
@@ -162,11 +162,11 @@ describe('starting a game', () => {
discoverSpy.mockRejectedValue(new Error('Network Error'));
renderPage();
await userEvent.click(screen.getByRole('button', { name: 'Start Practice Game' }));
await userEvent.click(screen.getByRole('button', { name: 'Play' }));
expect(await screen.findByRole('alert')).toHaveTextContent(/Network error/);
expect(screen.queryByText('Game Page')).toBeNull();
expect(screen.getByRole('button', { name: 'Start Practice Game' })).toBeEnabled();
expect(screen.getByRole('button', { name: 'Play' })).toBeEnabled();
});
it('explains a page with too few usable movies rather than failing silently', async () => {
@@ -178,7 +178,7 @@ describe('starting a game', () => {
});
renderPage();
await userEvent.click(screen.getByRole('button', { name: 'Start Practice Game' }));
await userEvent.click(screen.getByRole('button', { name: 'Play' }));
expect(await screen.findByRole('alert')).toHaveTextContent(/not return enough movies/);
});
@@ -187,7 +187,7 @@ describe('starting a game', () => {
movieDetailsSpy.mockRejectedValue(new Error('network'));
renderPage();
await userEvent.click(screen.getByRole('button', { name: 'Start Practice Game' }));
await userEvent.click(screen.getByRole('button', { name: 'Play' }));
expect(await screen.findByRole('alert')).toHaveTextContent(
'Failed to load movie details. Please try again.',