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

+16 -9
View File
@@ -2,6 +2,13 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Link, MemoryRouter, Route, Routes } from 'react-router';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// The completion modal waits for the loop-closing animation; there is none
// to wait for in jsdom, and these tests are about the content, not the delay.
vi.mock('@/lib/motion', async (importOriginal) => ({
...(await importOriginal<typeof import('@/lib/motion')>()),
prefersReducedMotion: () => true,
}));
import type {
ActorChainLink,
MovieChainLink,
@@ -231,7 +238,7 @@ function renderPage(initialEntry = '/versus/async-game/match-1') {
* target card + target badge), so wait on the chain-length badge instead -
* it renders exactly once, as soon as the game is ready to play. */
async function waitForReady() {
await screen.findByText('Chain: 0 links');
await screen.findByText('0 links');
}
beforeEach(() => {
@@ -364,7 +371,7 @@ describe('abort on unmount (WP cleanup)', () => {
// Switch to a different match before match-1's slow session finishes.
await userEvent.click(screen.getByText('go to match 2'));
await flush();
expect(await screen.findByText('Chain: 0 links')).toBeInTheDocument();
expect(await screen.findByText('0 links')).toBeInTheDocument();
expect(useGameStore.getState().movieA?.title).toBe('Third');
// match-1's delayed movie details now arrive - after being superseded.
@@ -391,8 +398,8 @@ describe('creator mode', () => {
expect(submitCreatorScore).toHaveBeenCalledWith('match-1', { sessionId: 'session-1' });
expect(submitCreatorScore).toHaveBeenCalledTimes(1);
expect(await screen.findByText('Score Submitted!')).toBeInTheDocument();
expect(screen.getByText('2400')).toBeInTheDocument();
expect(await screen.findByText('Score submitted')).toBeInTheDocument();
expect(screen.getByText('2,400')).toBeInTheDocument();
});
it('navigates from the completion modal to the leaderboard', async () => {
@@ -400,9 +407,9 @@ describe('creator mode', () => {
renderPage();
await waitForReady();
closeTheLoop();
await screen.findByText('Score Submitted!');
await screen.findByText('Score submitted');
await userEvent.click(screen.getByRole('button', { name: 'View Leaderboard' }));
await userEvent.click(screen.getByRole('button', { name: 'View leaderboard' }));
expect(await screen.findByText('leaderboard route')).toBeInTheDocument();
});
@@ -440,8 +447,8 @@ describe('challenger mode', () => {
expect(submitAsyncAttempt).toHaveBeenCalledWith('match-1', { sessionId: 'session-1' });
expect(submitAsyncAttempt).toHaveBeenCalledTimes(1);
expect(await screen.findByText('Challenge Complete!')).toBeInTheDocument();
expect(screen.getByText('You beat the creator!')).toBeInTheDocument();
expect(await screen.findByText('Challenge complete')).toBeInTheDocument();
expect(screen.getByText('You beat the creator')).toBeInTheDocument();
expect(screen.getByText('Your rank:')).toBeInTheDocument();
expect(screen.getByText('#1')).toBeInTheDocument();
});
@@ -464,7 +471,7 @@ describe('challenger mode', () => {
closeTheLoop();
await flush();
expect(await screen.findByText('Challenge Complete!')).toBeInTheDocument();
expect(await screen.findByText('Challenge complete')).toBeInTheDocument();
expect(screen.queryByText('Your rank:')).toBeNull();
});