diff --git a/pony-frontend/src/App.css b/pony-frontend/src/App.css
index a6c2983..759b4d7 100644
--- a/pony-frontend/src/App.css
+++ b/pony-frontend/src/App.css
@@ -84,7 +84,8 @@ body {
.home,
.start-page,
.game,
-.game-end {
+.game-end,
+.pixel-configurator {
min-height: 100vh;
padding: var(--space-xl);
position: relative;
@@ -103,7 +104,8 @@ body {
}
.start-page,
-.game-end {
+.game-end,
+.pixel-configurator {
text-align: center;
}
@@ -162,6 +164,24 @@ body {
transform: scale(1.05);
}
+.btn-secondary {
+ background: rgba(255, 255, 255, 0.85);
+ color: var(--color-accent);
+ border: none;
+ padding: var(--space-sm) var(--space-lg);
+ border-radius: var(--radius-pill);
+ font-size: 1rem;
+ cursor: pointer;
+ box-shadow: var(--shadow-sm);
+ margin-top: var(--space-md);
+ transition: transform 0.2s;
+}
+
+.btn-secondary:hover {
+ transform: scale(1.05);
+ background: var(--color-white);
+}
+
.btn-back {
background: rgba(255, 255, 255, 0.2);
color: var(--color-white);
@@ -375,6 +395,99 @@ body {
margin-top: var(--space-xs);
}
+/* --- Pixel pony configurator --- */
+.pixel-configurator-preview {
+ display: flex;
+ justify-content: center;
+ margin: var(--space-lg) auto;
+ padding: var(--space-lg);
+ background: rgba(255, 255, 255, 0.15);
+ border-radius: var(--radius-lg);
+ max-width: 320px;
+}
+
+.pixel-configurator-section {
+ max-width: 600px;
+ margin: 0 auto var(--space-lg);
+}
+
+.pixel-configurator-section h2 {
+ color: var(--color-white);
+ font-size: 1.1rem;
+ margin-bottom: var(--space-sm);
+}
+
+.pixel-mane-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
+ gap: var(--space-sm);
+}
+
+.pixel-mane-swatch {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: var(--space-xs);
+ background: var(--color-white);
+ border: 3px solid transparent;
+ border-radius: var(--radius-md);
+ padding: var(--space-sm);
+ cursor: pointer;
+ font-size: var(--font-small);
+ color: var(--color-muted);
+ transition: transform 0.15s;
+}
+
+.pixel-mane-swatch:hover {
+ transform: scale(1.05);
+}
+
+.pixel-mane-swatch.is-selected {
+ border-color: var(--color-accent);
+}
+
+.pixel-color-row,
+.pixel-toggle-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-sm);
+ justify-content: center;
+}
+
+.pixel-color-swatch {
+ width: 40px;
+ height: 40px;
+ border-radius: var(--radius-round);
+ border: 3px solid rgba(255, 255, 255, 0.6);
+ cursor: pointer;
+ transition: transform 0.15s;
+}
+
+.pixel-color-swatch:hover {
+ transform: scale(1.1);
+}
+
+.pixel-color-swatch.is-selected {
+ border-color: var(--focus-color);
+ box-shadow: var(--shadow-md);
+}
+
+.pixel-toggle {
+ background: var(--color-white);
+ color: var(--color-muted);
+ border: 3px solid transparent;
+ border-radius: var(--radius-pill);
+ padding: var(--space-sm) var(--space-lg);
+ font-size: 1rem;
+ cursor: pointer;
+}
+
+.pixel-toggle.is-selected {
+ border-color: var(--color-accent);
+ color: var(--color-accent);
+ font-weight: bold;
+}
+
/* --- Pony mini card (in-game) --- */
.pony-card-mini {
display: flex;
@@ -1060,10 +1173,15 @@ input:focus-visible,
.home-content,
.start-page,
.game,
- .game-end {
+ .game-end,
+ .pixel-configurator {
padding: var(--space-md) !important;
}
+ .pixel-mane-grid {
+ grid-template-columns: repeat(3, 1fr) !important;
+ }
+
.pony-choices {
grid-template-columns: repeat(2, 1fr) !important;
}
@@ -1095,6 +1213,10 @@ input:focus-visible,
grid-template-columns: 1fr !important;
}
+ .pixel-mane-grid {
+ grid-template-columns: repeat(2, 1fr) !important;
+ }
+
.title {
font-size: 1.5rem !important;
}
diff --git a/pony-frontend/src/App.js b/pony-frontend/src/App.js
index 15ff4cc..e817b88 100644
--- a/pony-frontend/src/App.js
+++ b/pony-frontend/src/App.js
@@ -314,6 +314,14 @@ function App() {
/>
)}
+ {page === 'pixelConfigurator' && (
+
+ )}
+
{page === 'start' && (
🎮 Start Nyt Spil!
+ onNavigate('pixelConfigurator')}
+ aria-label="Design din pixel pony"
+ >
+ 🎨 Design din Pony
+
);
diff --git a/pony-frontend/src/pages/PixelPonyConfiguratorPage.test.js b/pony-frontend/src/pages/PixelPonyConfiguratorPage.test.js
new file mode 100644
index 0000000..66d5370
--- /dev/null
+++ b/pony-frontend/src/pages/PixelPonyConfiguratorPage.test.js
@@ -0,0 +1,61 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import PixelPonyConfiguratorPage from './PixelPonyConfiguratorPage';
+
+jest.mock('framer-motion', () => {
+ const React = require('react');
+ const motion = {};
+ ['div', 'button', 'h1', 'h2', 'span'].forEach(tag => {
+ motion[tag] = ({ children, ...props }) =>
+ React.createElement(tag, { 'data-motion': 'true', ...props }, children);
+ });
+ return { motion, AnimatePresence: ({ children }) => children };
+});
+
+jest.mock('../SceneMusic', () => function SceneMusic() { return null; });
+jest.mock('../services/tts', () => ({ speakDanish: jest.fn(), cancelSpeech: jest.fn() }));
+
+beforeEach(() => {
+ window.localStorage.clear();
+});
+
+test('renders mane options, color swatches, and toggles', () => {
+ render( {}} onNavigate={() => {}} />);
+ expect(screen.getByRole('button', { name: 'Vælg manke: Boglig' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: '🦄 Horn' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: '🪽 Vinger' })).toBeInTheDocument();
+});
+
+test('selecting a mane style marks it as pressed', async () => {
+ render( {}} onNavigate={() => {}} />);
+ const bubbly = screen.getByRole('button', { name: 'Vælg manke: Boblende' });
+ expect(bubbly).toHaveAttribute('aria-pressed', 'false');
+ await userEvent.click(bubbly);
+ expect(bubbly).toHaveAttribute('aria-pressed', 'true');
+});
+
+test('toggling wings flips its pressed state', async () => {
+ render( {}} onNavigate={() => {}} />);
+ const wings = screen.getByRole('button', { name: '🪽 Vinger' });
+ expect(wings).toHaveAttribute('aria-pressed', 'false');
+ await userEvent.click(wings);
+ expect(wings).toHaveAttribute('aria-pressed', 'true');
+});
+
+test('saving persists the appearance to localStorage', async () => {
+ render( {}} onNavigate={() => {}} />);
+ await userEvent.click(screen.getByRole('button', { name: 'Vælg manke: Boblende' }));
+ const saveButton = screen.getByRole('button', { name: 'Gem min pony' });
+ await userEvent.click(saveButton);
+ const saved = JSON.parse(window.localStorage.getItem('pony_appearance'));
+ expect(saved.mane).toBe('bubbly');
+ expect(saveButton).toHaveTextContent('✅ Gemt!');
+});
+
+test('back button navigates home', async () => {
+ const onNavigate = jest.fn();
+ render( {}} onNavigate={onNavigate} />);
+ await userEvent.click(screen.getByRole('button', { name: 'Tilbage til forsiden' }));
+ expect(onNavigate).toHaveBeenCalledWith('home');
+});