diff --git a/frontend/index.html b/frontend/index.html
index 16bafb6..e1d859d 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -60,18 +60,38 @@
-
+
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
index 77bc860..61cac04 100644
--- a/frontend/src/main.ts
+++ b/frontend/src/main.ts
@@ -12,14 +12,17 @@ function requireEl
(id: string): T {
return el as T;
}
-const introModal = requireEl('intro-modal');
+const menuModal = requireEl('intro-modal');
+const resultModal = requireEl('result-modal');
const themeGridEl = requireEl('theme-grid');
const startBtn = requireEl('start-btn');
+const enterCodeBtn = requireEl('enter-code-btn');
+const createRoomBtn = requireEl('create-room-btn');
+const findMatchBtn = requireEl('find-match-btn');
const helpBtn = requireEl('help-btn');
const muteBtn = requireEl('mute-btn');
const themeNameBadge = requireEl('theme-name-badge');
const changeThemeBtn = requireEl('change-theme-btn');
-const resultModal = requireEl('result-modal');
const previewElements = {
overlay: requireEl('preview-overlay'),
@@ -28,13 +31,32 @@ const previewElements = {
canvas: requireEl('preview-canvas'),
};
+/** Top-level overlay screens the composition root switches between. At most
+ * one is visible at a time; the game board underneath is always present and
+ * simply gets obscured while a screen is shown. */
+const screens = {
+ menu: menuModal,
+ result: resultModal,
+} as const;
+
+type ScreenName = keyof typeof screens;
+
+function showView(name: ScreenName | null): void {
+ for (const key of Object.keys(screens) as ScreenName[]) {
+ screens[key].classList.toggle('hidden', key !== name);
+ }
+}
+
let game: ThemeGuessGame | null = null;
const themeGrid = createThemeGrid(themeGridEl, 'gruvbox', () => sound.playOpen());
-function startWithSelectedTheme(): void {
+// Solo is the only flow this ticket wires end-to-end. Ticket #17 will
+// replace this direct "pick a theme, hit play" shortcut with a fuller solo
+// config flow inside #menu-solo; keep it working in the meantime.
+function playSolo(): void {
const themeId: ThemeId = themeGrid.selected;
- introModal.classList.add('hidden');
+ showView(null);
runThemePreview(previewElements, themeId, () => {
if (game) game.setTheme(themeId);
else game = new ThemeGuessGame(themeId);
@@ -43,14 +65,21 @@ function startWithSelectedTheme(): void {
}, () => sound.playOpen());
}
-startBtn.addEventListener('click', startWithSelectedTheme);
-helpBtn.addEventListener('click', () => introModal.classList.remove('hidden'));
-changeThemeBtn.addEventListener('click', () => {
- resultModal.classList.add('hidden');
- introModal.classList.remove('hidden');
-});
+startBtn.addEventListener('click', playSolo);
+
+// TODO(#17/#15/#14): wire real flow
+enterCodeBtn.addEventListener('click', () => console.log('[menu] enter code clicked (not implemented yet)'));
+// TODO(#17/#15/#14): wire real flow
+createRoomBtn.addEventListener('click', () => console.log('[menu] create room clicked (not implemented yet)'));
+// TODO(#17/#15/#14): wire real flow
+findMatchBtn.addEventListener('click', () => console.log('[menu] find match clicked (not implemented yet)'));
+
+helpBtn.addEventListener('click', () => showView('menu'));
+changeThemeBtn.addEventListener('click', () => showView('menu'));
muteBtn.addEventListener('click', () => {
const muted = sound.toggleMute();
muteBtn.textContent = muted ? 'đ' : 'đ';
});
+
+showView('menu');
diff --git a/frontend/src/style.css b/frontend/src/style.css
index c122211..622ba30 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -380,6 +380,49 @@ body {
text-align: center;
}
+/* ---------- main menu (Solo / Private / Matchmaking) ---------- */
+
+.menu-modal { max-width: 760px; }
+
+.menu-sections {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 14px;
+ margin: 18px 0 4px;
+}
+
+.menu-section {
+ background: var(--panel-2);
+ border: 3px solid var(--ink);
+ box-shadow: 4px 4px 0 var(--ink);
+ padding: 14px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.menu-section h3 {
+ margin: 0;
+ font-family: var(--pixel);
+ font-size: 11px;
+ text-shadow: 1px 1px 0 var(--ink);
+}
+
+#menu-solo h3 { color: var(--cyan); }
+#menu-private h3 { color: var(--pink); }
+#menu-matchmaking h3 { color: var(--green); }
+
+.menu-section-desc { margin: 0; font-size: 11px; line-height: 1.5; color: var(--fg-dim); }
+
+.menu-section-actions { display: flex; flex-direction: column; gap: 8px; margin-top: auto; }
+.menu-section-actions .btn { width: 100%; }
+
+#menu-solo .theme-grid { margin-bottom: 0; }
+
+@media (max-width: 640px) {
+ .menu-sections { grid-template-columns: 1fr; }
+}
+
/* ---------- theme preview countdown ---------- */
.preview-box {