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

@@ -1,7 +1,14 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter, Route, Routes } from 'react-router';
import { beforeEach, describe, expect, it } from 'vitest';
import { 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 { useAuthStore } from '@/stores/auth-store';
import { useVersusStore } from '@/stores/versus-store';
import VersusCompletionModal from './VersusCompletionModal';
@@ -57,9 +64,9 @@ describe('VersusCompletionModal', () => {
useVersusStore.setState({ myScore: score(1500), lobbyState: 'playing' });
renderModal();
expect(screen.getByRole('dialog', { name: 'You finished!' })).toBeInTheDocument();
expect(screen.getByText('1500')).toBeInTheDocument();
expect(screen.getByText('Waiting for opponent to finish...')).toBeInTheDocument();
expect(screen.getByRole('dialog', { name: 'You finished' })).toBeInTheDocument();
expect(screen.getByText('1,500')).toBeInTheDocument();
expect(screen.getByText('Waiting for opponent to finish…')).toBeInTheDocument();
});
it('defaults myTotal to 0 when the score object has no totalScore field', () => {
@@ -85,7 +92,7 @@ describe('VersusCompletionModal', () => {
useVersusStore.setState({ myScore: score(1500), lobbyState: 'finished', matchResult: null });
renderModal();
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.queryByText('Match Complete!')).toBeNull();
expect(screen.queryByText('Match complete')).toBeNull();
});
it('declares a tie when the match has no winner', () => {
@@ -103,8 +110,8 @@ describe('VersusCompletionModal', () => {
});
renderModal();
expect(screen.getByRole('heading', { name: /Match Complete!/ })).toBeInTheDocument();
expect(screen.getByText("It's a tie!")).toBeInTheDocument();
expect(screen.getByRole('heading', { name: /Match complete/ })).toBeInTheDocument();
expect(screen.getByText('A tie')).toBeInTheDocument();
expect(screen.getByText('Me')).toBeInTheDocument();
expect(screen.getByText('Rival')).toBeInTheDocument();
});
@@ -124,7 +131,7 @@ describe('VersusCompletionModal', () => {
});
renderModal();
expect(screen.getByText('You win!')).toBeInTheDocument();
expect(screen.getByText('You won')).toBeInTheDocument();
});
it('declares a loss when the winner is the other player', () => {
@@ -142,7 +149,7 @@ describe('VersusCompletionModal', () => {
});
renderModal();
expect(screen.getByText('You lose!')).toBeInTheDocument();
expect(screen.getByText('You lost')).toBeInTheDocument();
});
it('defaults a missing player score to 0', () => {