From d4a25746b215ec1aaa76d4a3fc1493b89c03f16f Mon Sep 17 00:00:00 2001
From: AlacrisDevs
Date: Fri, 12 Dec 2025 01:47:51 +0200
Subject: [PATCH] Started working on adding mobile buzzers, a lot of rewriting
---
messages/en.json | 4 +-
messages/et.json | 4 +-
src/lib/components/ColorPicker.svelte | 8 +-
src/lib/components/ConfirmDialog.svelte | 21 +-
src/lib/components/Settings.svelte | 58 +-
src/lib/components/ToastContainer.svelte | 28 +
src/lib/components/editor/EditorHeader.svelte | 153 +++
.../editor/QuestionEditModal.svelte | 284 ++++
src/lib/components/editor/RoundEditor.svelte | 118 ++
.../components/editor/SettingsPanel.svelte | 265 ++++
src/lib/components/editor/TeamEditor.svelte | 53 +
src/lib/components/editor/index.ts | 6 +
src/lib/components/index.ts | 4 +
.../components/kuldvillak/ui/KvButton.svelte | 101 ++
.../kuldvillak/ui/KvButtonPrimary.svelte | 48 -
.../kuldvillak/ui/KvButtonSecondary.svelte | 48 -
.../kuldvillak/ui/KvCheckbox.svelte | 38 +
.../kuldvillak/ui/KvEditCard.svelte | 112 +-
.../kuldvillak/ui/TutorialModal.svelte | 2 +
src/lib/components/kuldvillak/ui/index.ts | 4 +-
src/lib/index.ts | 6 +
src/lib/services/index.ts | 8 +
src/lib/services/localStorage.ts | 116 ++
src/lib/services/storage.ts | 56 +
src/lib/stores/editor.svelte.ts | 448 +++++++
src/lib/stores/editor.test.ts | 591 +++++++++
src/lib/stores/gameSession.svelte.ts | 66 +-
src/lib/stores/gameSession.test.ts | 26 +
src/lib/stores/persistence.test.ts | 11 +-
src/lib/stores/persistence.ts | 50 +-
src/lib/stores/theme.svelte.ts | 162 +--
src/lib/stores/theme.test.ts | 178 +++
src/lib/stores/toast.svelte.ts | 60 +
src/lib/types/buzzer.ts | 123 ++
src/lib/utils/color.ts | 104 ++
src/lib/utils/focusTrap.ts | 47 +
src/lib/utils/validation.ts | 155 +++
src/routes/+layout.svelte | 4 +-
src/routes/kuldvillak/+page.svelte | 24 +-
src/routes/kuldvillak/edit/+page.svelte | 1139 ++--------------
.../kuldvillak/edit/+page.svelte.backup | 1146 +++++++++++++++++
src/routes/kuldvillak/edit/+page.svelte.new | 0
src/routes/kuldvillak/play/+page.svelte | 13 +-
.../kuldvillak/play/ModeratorView.svelte | 334 +++--
src/routes/layout.css | 133 +-
src/test/mocks/app-environment.ts | 5 +
vitest.config.ts | 4 +-
47 files changed, 4712 insertions(+), 1656 deletions(-)
create mode 100644 src/lib/components/ToastContainer.svelte
create mode 100644 src/lib/components/editor/EditorHeader.svelte
create mode 100644 src/lib/components/editor/QuestionEditModal.svelte
create mode 100644 src/lib/components/editor/RoundEditor.svelte
create mode 100644 src/lib/components/editor/SettingsPanel.svelte
create mode 100644 src/lib/components/editor/TeamEditor.svelte
create mode 100644 src/lib/components/editor/index.ts
create mode 100644 src/lib/components/kuldvillak/ui/KvButton.svelte
delete mode 100644 src/lib/components/kuldvillak/ui/KvButtonPrimary.svelte
delete mode 100644 src/lib/components/kuldvillak/ui/KvButtonSecondary.svelte
create mode 100644 src/lib/components/kuldvillak/ui/KvCheckbox.svelte
create mode 100644 src/lib/services/index.ts
create mode 100644 src/lib/services/localStorage.ts
create mode 100644 src/lib/services/storage.ts
create mode 100644 src/lib/stores/editor.svelte.ts
create mode 100644 src/lib/stores/editor.test.ts
create mode 100644 src/lib/stores/gameSession.test.ts
create mode 100644 src/lib/stores/theme.test.ts
create mode 100644 src/lib/stores/toast.svelte.ts
create mode 100644 src/lib/types/buzzer.ts
create mode 100644 src/lib/utils/color.ts
create mode 100644 src/lib/utils/focusTrap.ts
create mode 100644 src/lib/utils/validation.ts
create mode 100644 src/routes/kuldvillak/edit/+page.svelte.backup
create mode 100644 src/routes/kuldvillak/edit/+page.svelte.new
create mode 100644 src/test/mocks/app-environment.ts
diff --git a/messages/en.json b/messages/en.json
index e21233f..4202626 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -51,7 +51,9 @@
"kv_toast_game_saved": "Game saved!",
"kv_toast_game_loaded": "Game loaded!",
"kv_toast_invalid_file": "Invalid game file",
- "kv_edit_reset_confirm": "Are you sure you want to reset all fields to default? This will clear all your work.",
+ "kv_edit_edit_round": "Edit Round",
+ "kv_edit_reset_confirm_title": "Reset Game?",
+ "kv_edit_reset_confirm_message": "Are you sure you want to reset all fields to default? This will clear all your work.",
"kv_edit_reset_success": "Game reset to defaults",
"kv_edit_final_round": "Final Round",
"kv_edit_add_team": "Add Team",
diff --git a/messages/et.json b/messages/et.json
index 8b0b0a9..c59351f 100644
--- a/messages/et.json
+++ b/messages/et.json
@@ -51,7 +51,9 @@
"kv_toast_game_saved": "Mäng salvestatud!",
"kv_toast_game_loaded": "Mäng laetud!",
"kv_toast_invalid_file": "Vigane mängufail",
- "kv_edit_reset_confirm": "Kas oled kindel, et soovid kõik väljad vaikeväärtustele lähtestada? See kustutab kogu sinu töö.",
+ "kv_edit_edit_round": "Muuda vooru",
+ "kv_edit_reset_confirm_title": "Lähtesta mäng?",
+ "kv_edit_reset_confirm_message": "Kas oled kindel, et soovid kõik väljad vaikeväärtustele lähtestada? See kustutab kogu sinu töö.",
"kv_edit_reset_success": "Mäng lähtestatud",
"kv_edit_final_round": "Finaalvoor",
"kv_edit_add_team": "Lisa tiim",
diff --git a/src/lib/components/ColorPicker.svelte b/src/lib/components/ColorPicker.svelte
index ec0851c..cd2f932 100644
--- a/src/lib/components/ColorPicker.svelte
+++ b/src/lib/components/ColorPicker.svelte
@@ -1,6 +1,7 @@
+
+{#each toastStore.notifications as notification (notification.id)}
+
+
+ {notification.message}
+
+
+
+{/each}
diff --git a/src/lib/components/editor/EditorHeader.svelte b/src/lib/components/editor/EditorHeader.svelte
new file mode 100644
index 0000000..90aec1c
--- /dev/null
+++ b/src/lib/components/editor/EditorHeader.svelte
@@ -0,0 +1,153 @@
+
+
+
diff --git a/src/lib/components/editor/QuestionEditModal.svelte b/src/lib/components/editor/QuestionEditModal.svelte
new file mode 100644
index 0000000..6d3ec8f
--- /dev/null
+++ b/src/lib/components/editor/QuestionEditModal.svelte
@@ -0,0 +1,284 @@
+
+
+{#if originalQuestion || finalRound}
+
+
+
+
+
+ {#if finalRound}
+ {m.kv_edit_final_round()}
+ {:else}
+ {category?.name || m.kv_edit_category()} - {originalQuestion?.points}€
+ {/if}
+
+
+
+
+
+ {#if finalRound}
+
+
+
+
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#if !finalRound}
+
+
+
+
+
+
+
+
+ (localIsDailyDouble = !localIsDailyDouble)}
+ />
+
+ {m.kv_edit_daily_double()}
+
+
+ ({currentDD}/{maxDD})
+
+
+ {/if}
+
+
+
+
+ {m.kv_settings_save_exit()}
+
+
+
+
+{/if}
+
+
+
diff --git a/src/lib/components/editor/RoundEditor.svelte b/src/lib/components/editor/RoundEditor.svelte
new file mode 100644
index 0000000..901708d
--- /dev/null
+++ b/src/lib/components/editor/RoundEditor.svelte
@@ -0,0 +1,118 @@
+
+
+
+
+ {#each editorStore.rounds as round, ri}
+ {#if ri === activeRoundIndex}
+
+
+
+
+ {getRoundName(ri)}
+
+ ({m.kv_edit_dd_count()}
+ {editorStore.countDailyDoubles(ri)}/{editorStore
+ .settings.dailyDoublesPerRound[ri] ?? 1})
+
+
+
+
+
+
+ {#each round.categories as cat, ci}
+
+
+ editorStore.updateCategoryName(
+ ri,
+ ci,
+ e.currentTarget.value,
+ )}
+ placeholder={m.kv_edit_category()}
+ class="w-full bg-transparent border-none outline-none font-kv-body text-sm text-kv-white text-center uppercase kv-shadow-text placeholder:text-kv-white/50"
+ />
+
+ {/each}
+
+
+
+
+ {#each { length: editorStore.settings.questionsPerCategory } as _, qi}
+ {#each round.categories as cat, ci}
+ {@const q = cat.questions[qi]}
+ {#if q}
+
+ {/if}
+ {/each}
+ {/each}
+
+
+ {/if}
+ {/each}
+
diff --git a/src/lib/components/editor/SettingsPanel.svelte b/src/lib/components/editor/SettingsPanel.svelte
new file mode 100644
index 0000000..5bb548b
--- /dev/null
+++ b/src/lib/components/editor/SettingsPanel.svelte
@@ -0,0 +1,265 @@
+
+
+
+
+
+
{m.kv_edit_settings_teams()}
+
+ {#if onShowRules}
+ {m.kv_edit_rules()}
+ {/if}
+ {#if onShowHowTo}
+ {m.kv_edit_how_to()}
+ {/if}
+
+
+
+
+
+
+
+
+ {m.kv_edit_rounds()}
+
+
+ {m.kv_play_timer()}
+
+
+ {m.kv_play_timer_reveal()}
+
+
+ {m.kv_edit_final_round()}
+
+ {#if editorStore.settings.numberOfRounds === 2 && onRoundSelect}
+
+ {m.kv_edit_edit_round()}
+
+ {/if}
+
+
+
+
+ setRoundCount(1)}>1
+ setRoundCount(2)}>2
+
+
+
+
+
+
+
{m.kv_play_seconds()}
+
+
+
+
+
+
+
{m.kv_play_seconds()}
+
+
+
+
+ (editorStore.settings.enableFinalRound =
+ !editorStore.settings.enableFinalRound)}
+ />
+ {#if editorStore.settings.enableFinalRound && onEditFinalRound}
+
+ {m.kv_edit_question()}
+
+ {/if}
+
+
+ {#if editorStore.settings.numberOfRounds === 2 && onRoundSelect}
+
+ onRoundSelect(0)}
+ >
+ {m.kv_edit_r1()}
+
+ onRoundSelect(1)}
+ >
+ {m.kv_edit_r2()}
+
+
+ {/if}
+
+
+
+
+
+
+
+ {m.kv_edit_points()}
+
+
+ {m.kv_edit_values()}
+
+
+ {m.kv_edit_negative_scores()}
+
+
+ {m.kv_edit_teams_label()}
+
+
+
+
+
+ editorStore.updatePreset("round1")}
+ >{m.kv_edit_preset_normal()}
+ editorStore.updatePreset("custom")}
+ >{m.kv_edit_custom()}
+
+
+
+ {#each editorStore.settings.pointValues as val, i}
+
+ {#if editorStore.settings.pointValuePreset === "custom"}
+
+ editorStore.updateQuestionPoints()}
+ min={1}
+ max={9999}
+ class="w-full h-full bg-transparent border-none outline-none font-kv-body text-lg text-kv-white text-center [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
+ />
+ {:else}
+ {val}
+ {/if}
+
+ {/each}
+
+
+
+
+ (editorStore.settings.allowNegativeScores =
+ !editorStore.settings.allowNegativeScores)}
+ />
+
+
+
+
+
+
+
diff --git a/src/lib/components/editor/TeamEditor.svelte b/src/lib/components/editor/TeamEditor.svelte
new file mode 100644
index 0000000..6340ab1
--- /dev/null
+++ b/src/lib/components/editor/TeamEditor.svelte
@@ -0,0 +1,53 @@
+
+
+
+ {#each editorStore.teams as team (team.id)}
+
+
+ editorStore.updateTeamName(team.id, e.currentTarget.value)}
+ class="bg-transparent border-none outline-none font-kv-body text-xl text-kv-text uppercase min-w-[80px] max-w-[120px] kv-shadow-text"
+ />
+ {#if editorStore.canRemoveTeam}
+ editorStore.removeTeam(team.id)}
+ ariaLabel={m.kv_edit_remove_team()}
+ class="w-8 h-8"
+ >
+
+
+ {/if}
+
+ {/each}
+
+ {#if editorStore.canAddTeam}
+
editorStore.addTeam()}
+ ariaLabel={m.kv_edit_add_team()}
+ >
+
+
+ {/if}
+
diff --git a/src/lib/components/editor/index.ts b/src/lib/components/editor/index.ts
new file mode 100644
index 0000000..b9b38ad
--- /dev/null
+++ b/src/lib/components/editor/index.ts
@@ -0,0 +1,6 @@
+// Editor Components
+export { default as TeamEditor } from './TeamEditor.svelte';
+export { default as RoundEditor } from './RoundEditor.svelte';
+export { default as EditorHeader } from './EditorHeader.svelte';
+export { default as QuestionEditModal } from './QuestionEditModal.svelte';
+export { default as SettingsPanel } from './SettingsPanel.svelte';
diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts
index 59cd386..4d54100 100644
--- a/src/lib/components/index.ts
+++ b/src/lib/components/index.ts
@@ -3,9 +3,13 @@ export { default as Slider } from './Slider.svelte';
export { default as Settings } from './Settings.svelte';
export { default as LanguageSwitcher } from './LanguageSwitcher.svelte';
export { default as Toast } from './Toast.svelte';
+export { default as ToastContainer } from './ToastContainer.svelte';
export { default as ConfirmDialog } from './ConfirmDialog.svelte';
export { default as ColorPicker } from './ColorPicker.svelte';
export { default as ErrorBoundary } from './ErrorBoundary.svelte';
// Kuldvillak Components
export * from './kuldvillak';
+
+// Editor Components
+export * from './editor';
diff --git a/src/lib/components/kuldvillak/ui/KvButton.svelte b/src/lib/components/kuldvillak/ui/KvButton.svelte
new file mode 100644
index 0000000..8fd55cd
--- /dev/null
+++ b/src/lib/components/kuldvillak/ui/KvButton.svelte
@@ -0,0 +1,101 @@
+
+
+{#if href}
+
+ {@render children()}
+
+{:else}
+
+{/if}
diff --git a/src/lib/components/kuldvillak/ui/KvButtonPrimary.svelte b/src/lib/components/kuldvillak/ui/KvButtonPrimary.svelte
deleted file mode 100644
index 999d7e8..0000000
--- a/src/lib/components/kuldvillak/ui/KvButtonPrimary.svelte
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-{#if href && !disabled}
-
- {@render children()}
-
-{:else}
-
-{/if}
diff --git a/src/lib/components/kuldvillak/ui/KvButtonSecondary.svelte b/src/lib/components/kuldvillak/ui/KvButtonSecondary.svelte
deleted file mode 100644
index 966b81f..0000000
--- a/src/lib/components/kuldvillak/ui/KvButtonSecondary.svelte
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-{#if href && !disabled}
-
- {@render children()}
-
-{:else}
-
-{/if}
diff --git a/src/lib/components/kuldvillak/ui/KvCheckbox.svelte b/src/lib/components/kuldvillak/ui/KvCheckbox.svelte
new file mode 100644
index 0000000..cab49d5
--- /dev/null
+++ b/src/lib/components/kuldvillak/ui/KvCheckbox.svelte
@@ -0,0 +1,38 @@
+
+
+
diff --git a/src/lib/components/kuldvillak/ui/KvEditCard.svelte b/src/lib/components/kuldvillak/ui/KvEditCard.svelte
index 373f196..ecaa6e4 100644
--- a/src/lib/components/kuldvillak/ui/KvEditCard.svelte
+++ b/src/lib/components/kuldvillak/ui/KvEditCard.svelte
@@ -1,6 +1,7 @@
@@ -473,699 +128,62 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
(showResetConfirm = true)}
+ onSettings={() => (showSettingsModal = true)}
+ onImportSuccess={() => showToast(m.kv_toast_game_loaded(), "success")}
+ onImportError={(err) => showToast(err, "error")}
+ />
-
-
-
-
- {m.kv_edit_settings_teams()}
-
-
- (showRulesModal = true)}>
- {m.kv_edit_rules()}
-
- (showHowToModal = true)}>
- {m.kv_edit_how_to()}
-
-
-
-
-
-
-
-
-
-
- {m.kv_edit_rounds()}
-
-
- {m.kv_play_timer()}
-
-
- {m.kv_play_timer_reveal()}
-
-
- {m.kv_edit_final_round()}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{m.kv_play_seconds()}
-
-
-
-
-
-
-
{m.kv_play_seconds()}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {m.kv_edit_points()}
-
-
- {m.kv_edit_values()}
-
-
- {m.kv_edit_negative_scores()}
-
-
- {m.kv_edit_teams_label()}
-
-
-
-
-
-
-
-
-
-
-
-
- {#each settings.pointValues as val, i}
-
- {#if settings.pointValuePreset === "custom"}
-
- {:else}
- {val}
- {/if}
-
- {/each}
-
-
-
-
-
-
-
- {#each teams as team (team.id)}
-
-
-
-
- {/each}
- {#if teams.length < 6}
-
- {/if}
-
-
-
-
-
-
-
-
- {#each rounds as round, ri}
-
-
-
-
- {ri === 0 ? m.kv_edit_r1() : m.kv_edit_r2()}
- ({m.kv_edit_dd_count()}
- {countDailyDoubles(ri)}/{settings
- .dailyDoublesPerRound[ri] ?? 1})
-
-
-
-
-
- {#each round.categories as cat, ci}
-
-
-
- {/each}
-
+ (showFinalRoundModal = true)}
+ onShowRules={() => (showRulesModal = true)}
+ onShowHowTo={() => (showHowToModal = true)}
+ {activeRoundIndex}
+ onRoundSelect={(index) => (activeRoundIndex = index)}
+ />
-
-
- {#each { length: settings.questionsPerCategory } as _, qi}
- {#each round.categories as cat, ci}
- {@const q = cat.questions[qi]}
-
- {/each}
- {/each}
-
-
- {/each}
-
+
+
-{#if editingQuestion}
- {@const q =
- rounds[editingQuestion.roundIndex].categories[editingQuestion.catIndex]
- .questions[editingQuestion.qIndex]}
- {@const cat =
- rounds[editingQuestion.roundIndex].categories[editingQuestion.catIndex]}
- {@const maxDD =
- settings.dailyDoublesPerRound[editingQuestion.roundIndex] ?? 1}
- {@const currentDD = countDailyDoubles(editingQuestion.roundIndex)}
-
- e.target === e.currentTarget && handleQuestionCloseClick()}
- onkeydown={(e) => e.key === "Escape" && handleQuestionCloseClick()}
- role="dialog"
- tabindex="-1"
- >
-
-
-
-
- {cat.name || m.kv_edit_category()} - {q.points}€
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {m.kv_edit_daily_double()} ({currentDD}/{maxDD})
-
-
-
-
-
-
-
-
-
-
- {
- showQuestionCloseConfirm = false;
- saveQuestion();
- }}
+{#if editingQ}
+ (editingQ = null)}
/>
{/if}
-
-{#if editingFinalQuestion}
- e.target === e.currentTarget && handleFinalCloseClick()}
- onkeydown={(e) => e.key === "Escape" && handleFinalCloseClick()}
- role="dialog"
- tabindex="-1"
- >
-
-
-
- {m.kv_edit_final_round()}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {m.kv_edit_final_enabled()}
-
-
-
-
-
-
-
+
+
-
- {
- showFinalCloseConfirm = false;
- saveFinalQuestion();
- }}
+
+{#if showFinalRoundModal}
+ (showFinalRoundModal = false)}
+ finalRound={true}
/>
{/if}
-
+
-
-
-
-{#if isStarting}
-
-
-
- {m.kv_edit_starting_game()}
-
-
-{/if}
-
-
-
+
-
+ import { goto } from "$app/navigation";
+ import { onMount } from "svelte";
+ import { browser } from "$app/environment";
+ import { Toast, Settings, ConfirmDialog } from "$lib/components";
+ import {
+ KvButtonSecondary,
+ KvCheckbox,
+ TutorialModal,
+ type TutorialSlide,
+ } from "$lib/components/kuldvillak/ui";
+ import * as m from "$lib/paraglide/messages";
+ import { getLocale } from "$lib/paraglide/runtime";
+ import { gameSession } from "$lib/stores/gameSession.svelte";
+ import type {
+ GameSettings,
+ Team,
+ Round,
+ Category,
+ Question,
+ FinalRound,
+ PointValuePreset,
+ } from "$lib/types/kuldvillak";
+ import { DEFAULT_SETTINGS } from "$lib/types/kuldvillak";
+
+ const AUTOSAVE_KEY = "kuldvillak-editor-autosave";
+
+ // State
+ let settings = $state({
+ ...DEFAULT_SETTINGS,
+ defaultTimerSeconds: 5,
+ answerRevealSeconds: 5,
+ });
+ let teams = $state([
+ { id: generateId(), name: "Mängija 1", score: 0 },
+ { id: generateId(), name: "Mängija 2", score: 0 },
+ ]);
+ let rounds = $state([
+ createRound(m.kv_edit_r1(), 1, DEFAULT_SETTINGS),
+ createRound(m.kv_edit_r2(), 2, DEFAULT_SETTINGS),
+ ]);
+ let finalRound = $state({
+ category: "",
+ question: "",
+ answer: "",
+ });
+ let gameName = $state("");
+ let editingQuestion = $state<{
+ roundIndex: number;
+ catIndex: number;
+ qIndex: number;
+ } | null>(null);
+ let editingFinalQuestion = $state(false);
+ let settingsOpen = $state(false);
+
+ // Toast state
+ let toastMessage = $state("");
+ let toastType = $state<"error" | "success">("error");
+ let toastVisible = $state(false);
+
+ // File input ref
+ let fileInput: HTMLInputElement;
+
+ // Confirm dialog states
+ let showResetConfirm = $state(false);
+ let showQuestionCloseConfirm = $state(false);
+ let showFinalCloseConfirm = $state(false);
+
+ // Tutorial modal states
+ let showRulesModal = $state(false);
+ let showHowToModal = $state(false);
+
+ // Tutorial slides - using paraglide translations with localized images
+ const rulesSlides: TutorialSlide[] = $derived([
+ {
+ image: `/tutorials/${getLocale()}/rules-1.png`,
+ text: m.kv_tutorial_rules_placeholder(),
+ },
+ ]);
+
+ const howToSlides: TutorialSlide[] = $derived([
+ {
+ image: `/tutorials/${getLocale()}/howto-1.png`,
+ text: m.kv_tutorial_howto_1(),
+ },
+ {
+ image: `/tutorials/${getLocale()}/howto-2.png`,
+ text: m.kv_tutorial_howto_2(),
+ },
+ {
+ image: `/tutorials/${getLocale()}/howto-3.png`,
+ text: m.kv_tutorial_howto_3(),
+ },
+ {
+ image: `/tutorials/${getLocale()}/howto-4.png`,
+ text: m.kv_tutorial_howto_4(),
+ },
+ {
+ image: `/tutorials/${getLocale()}/howto-5.png`,
+ text: m.kv_tutorial_howto_5(),
+ },
+ {
+ image: `/tutorials/${getLocale()}/howto-6.png`,
+ text: m.kv_tutorial_howto_6(),
+ },
+ {
+ image: `/tutorials/${getLocale()}/howto-7.png`,
+ text: m.kv_tutorial_howto_7(),
+ },
+ ]);
+
+ // Original values for reverting
+ let originalQuestion = $state<{
+ question: string;
+ answer: string;
+ imageUrl?: string;
+ isDailyDouble: boolean;
+ } | null>(null);
+ let originalFinal = $state<{
+ category: string;
+ question: string;
+ answer: string;
+ } | null>(null);
+
+ // Autosave to localStorage
+ function autoSave() {
+ if (!browser) return;
+ const data = { name: gameName, settings, teams, rounds, finalRound };
+ localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(data));
+ }
+
+ // Load from localStorage on mount
+ onMount(() => {
+ const saved = localStorage.getItem(AUTOSAVE_KEY);
+ if (saved) {
+ try {
+ const data = JSON.parse(saved);
+ if (data.settings && data.teams && data.rounds) {
+ gameName = data.name || "";
+ const { teamColors, ...cleanSettings } =
+ data.settings as Record;
+ settings = {
+ ...DEFAULT_SETTINGS,
+ ...cleanSettings,
+ } as GameSettings;
+ teams = (data.teams as Team[]).map((t) => ({
+ id: t.id,
+ name: t.name,
+ score: t.score ?? 0,
+ }));
+ rounds = data.rounds as Round[];
+ finalRound = (data.finalRound as FinalRound) || {
+ category: "",
+ question: "",
+ answer: "",
+ };
+ }
+ } catch {
+ /* ignore parse errors */
+ }
+ }
+ });
+
+ // Auto-save on any state change
+ $effect(() => {
+ const _ = [gameName, settings, teams, rounds, finalRound];
+ autoSave();
+ });
+
+ function showToast(message: string, type: "error" | "success" = "error") {
+ toastMessage = message;
+ toastType = type;
+ toastVisible = true;
+ }
+
+ function generateId(): string {
+ return crypto.randomUUID();
+ }
+
+ function createQuestion(points: number): Question {
+ return {
+ id: generateId(),
+ question: "",
+ answer: "",
+ points,
+ isDailyDouble: false,
+ isRevealed: false,
+ };
+ }
+
+ function createCategory(s: GameSettings, multiplier: number = 1): Category {
+ return {
+ id: generateId(),
+ name: "",
+ questions: s.pointValues.map((p) => createQuestion(p * multiplier)),
+ };
+ }
+
+ function createRound(
+ name: string,
+ multiplier: number,
+ s: GameSettings,
+ ): Round {
+ return {
+ id: generateId(),
+ name,
+ categories: Array.from({ length: s.categoriesPerRound }, () =>
+ createCategory(s, multiplier),
+ ),
+ pointMultiplier: multiplier,
+ };
+ }
+
+ function getPointsForRound(
+ preset: PointValuePreset,
+ roundIndex: number,
+ ): number[] {
+ const base = [10, 20, 30, 40, 50];
+ const multiplier = roundIndex + 1;
+ if (preset === "round1") return base.map((v) => v * multiplier);
+ // Custom preset uses user-defined point values
+ return settings.pointValues.map((v) => v * multiplier);
+ }
+
+ function updatePreset(preset: PointValuePreset) {
+ settings.pointValuePreset = preset;
+ if (preset === "round1") {
+ settings.pointValues = [10, 20, 30, 40, 50];
+ }
+ updateQuestionPoints();
+ }
+
+ function updateQuestionPoints() {
+ rounds.forEach((round, ri) => {
+ const points = getPointsForRound(settings.pointValuePreset, ri);
+ round.categories.forEach((cat) =>
+ cat.questions.forEach((q, i) => {
+ q.points = points[i] ?? q.points;
+ }),
+ );
+ });
+ rounds = [...rounds];
+ }
+
+ function setRoundCount(count: 1 | 2) {
+ settings.numberOfRounds = count;
+ if (count === 1 && rounds.length > 1) {
+ rounds = [rounds[0]];
+ settings.dailyDoublesPerRound = [settings.dailyDoublesPerRound[0]];
+ } else if (count === 2 && rounds.length === 1) {
+ rounds = [...rounds, createRound(m.kv_edit_r2(), 2, settings)];
+ settings.dailyDoublesPerRound = [
+ settings.dailyDoublesPerRound[0],
+ 2,
+ ];
+ }
+ updateQuestionPoints();
+ }
+
+ function addTeam() {
+ if (teams.length >= 6) return;
+ teams = [
+ ...teams,
+ { id: generateId(), name: `Mängija ${teams.length + 1}`, score: 0 },
+ ];
+ }
+
+ function removeTeam(id: string) {
+ if (teams.length <= 2) return;
+ teams = teams.filter((t) => t.id !== id);
+ }
+
+ function countDailyDoubles(roundIndex: number): number {
+ return rounds[roundIndex].categories.reduce(
+ (sum, cat) =>
+ sum + cat.questions.filter((q) => q.isDailyDouble).length,
+ 0,
+ );
+ }
+
+ function validateGame(): string | null {
+ if (teams.length < 2) return m.kv_error_min_players();
+ for (let i = 0; i < rounds.length; i++) {
+ const hasQuestions = rounds[i].categories.some((cat) =>
+ cat.questions.some((q) => q.question.trim()),
+ );
+ if (!hasQuestions)
+ return m.kv_error_no_questions({ round: String(i + 1) });
+ }
+ if (settings.enableFinalRound && !finalRound.question.trim())
+ return m.kv_error_no_final();
+ return null;
+ }
+
+ let isStarting = $state(false);
+
+ async function startGame() {
+ const error = validateGame();
+ if (error) {
+ showToast(error, "error");
+ return;
+ }
+ isStarting = true;
+ try {
+ gameSession.startGame({
+ name: gameName,
+ settings,
+ teams,
+ rounds,
+ finalRound: settings.enableFinalRound ? finalRound : null,
+ });
+ gameSession.openProjector();
+ await goto("/kuldvillak/play");
+ } catch (err) {
+ showToast("Failed to start game: " + err, "error");
+ isStarting = false;
+ }
+ }
+
+ function saveGame() {
+ const game = { name: gameName, settings, teams, rounds, finalRound };
+ const blob = new Blob([JSON.stringify(game, null, 2)], {
+ type: "application/json",
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `${gameName.replace(/\s+/g, "_") || "game"}.json`;
+ a.click();
+ URL.revokeObjectURL(url);
+ showToast(m.kv_toast_game_saved(), "success");
+ }
+
+ function resetGame() {
+ settings = {
+ ...DEFAULT_SETTINGS,
+ defaultTimerSeconds: 5,
+ answerRevealSeconds: 5,
+ };
+ teams = [
+ { id: generateId(), name: "Mängija 1", score: 0 },
+ { id: generateId(), name: "Mängija 2", score: 0 },
+ ];
+ rounds = [
+ createRound(m.kv_edit_r1(), 1, DEFAULT_SETTINGS),
+ createRound(m.kv_edit_r2(), 2, DEFAULT_SETTINGS),
+ ];
+ finalRound = { category: "", question: "", answer: "" };
+ gameName = "";
+ localStorage.removeItem(AUTOSAVE_KEY);
+ showToast(m.kv_edit_reset_success(), "success");
+ }
+
+ function loadGame(event: Event) {
+ const file = (event.target as HTMLInputElement).files?.[0];
+ if (!file) return;
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ try {
+ const data = JSON.parse(e.target?.result as string);
+ if (data.settings && data.teams && data.rounds) {
+ gameName = data.name || "Loaded Game";
+ const { teamColors, ...cleanSettings } =
+ data.settings as Record;
+ settings = {
+ ...DEFAULT_SETTINGS,
+ ...cleanSettings,
+ } as GameSettings;
+ teams = (data.teams as Team[]).map((t) => ({
+ id: t.id,
+ name: t.name,
+ score: t.score ?? 0,
+ }));
+ rounds = data.rounds as Round[];
+ finalRound = (data.finalRound as FinalRound) || {
+ category: "",
+ question: "",
+ answer: "",
+ };
+ showToast(m.kv_toast_game_loaded(), "success");
+ } else {
+ showToast(m.kv_toast_invalid_file(), "error");
+ }
+ } catch {
+ showToast(m.kv_toast_invalid_file(), "error");
+ }
+ };
+ reader.readAsText(file);
+ (event.target as HTMLInputElement).value = "";
+ }
+
+ function toggleDailyDouble() {
+ if (!editingQuestion) return;
+ const { roundIndex, catIndex, qIndex } = editingQuestion;
+ const q = rounds[roundIndex].categories[catIndex].questions[qIndex];
+ const maxDD = settings.dailyDoublesPerRound[roundIndex] ?? 1;
+ const currentCount = countDailyDoubles(roundIndex);
+
+ if (q.isDailyDouble) {
+ q.isDailyDouble = false;
+ } else if (currentCount < maxDD) {
+ q.isDailyDouble = true;
+ }
+ rounds = [...rounds];
+ }
+
+ function openQuestion(
+ roundIndex: number,
+ catIndex: number,
+ qIndex: number,
+ ) {
+ const q = rounds[roundIndex].categories[catIndex].questions[qIndex];
+ originalQuestion = {
+ question: q.question,
+ answer: q.answer,
+ imageUrl: q.imageUrl,
+ isDailyDouble: q.isDailyDouble,
+ };
+ editingQuestion = { roundIndex, catIndex, qIndex };
+ }
+
+ function saveQuestion() {
+ originalQuestion = null;
+ editingQuestion = null;
+ }
+
+ function handleQuestionCloseClick() {
+ showQuestionCloseConfirm = true;
+ }
+
+ function discardQuestionChanges() {
+ if (editingQuestion && originalQuestion) {
+ const q =
+ rounds[editingQuestion.roundIndex].categories[
+ editingQuestion.catIndex
+ ].questions[editingQuestion.qIndex];
+ q.question = originalQuestion.question;
+ q.answer = originalQuestion.answer;
+ q.imageUrl = originalQuestion.imageUrl;
+ q.isDailyDouble = originalQuestion.isDailyDouble;
+ rounds = [...rounds];
+ }
+ showQuestionCloseConfirm = false;
+ originalQuestion = null;
+ editingQuestion = null;
+ }
+
+ function openFinalQuestion() {
+ originalFinal = { ...finalRound };
+ editingFinalQuestion = true;
+ }
+
+ function saveFinalQuestion() {
+ originalFinal = null;
+ editingFinalQuestion = false;
+ }
+
+ function handleFinalCloseClick() {
+ showFinalCloseConfirm = true;
+ }
+
+ function discardFinalChanges() {
+ if (originalFinal) {
+ finalRound = { ...originalFinal };
+ }
+ showFinalCloseConfirm = false;
+ originalFinal = null;
+ editingFinalQuestion = false;
+ }
+
+
+
+ {m.kv_edit_title()} - {m.game_kuldvillak()}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {isStarting ? "⏳" : "▶"}
+ {m.kv_edit_start()}
+
+
+
+
+
+
+
+
+ {m.kv_edit_settings_teams()}
+
+
+ (showRulesModal = true)}
+ size="md"
+ >
+ {m.kv_edit_rules()}
+
+ (showHowToModal = true)}
+ size="md"
+ >
+ {m.kv_edit_how_to()}
+
+
+
+
+
+
+
+
+
+
+ {m.kv_edit_rounds()}
+
+
+ {m.kv_play_timer()}
+
+
+ {m.kv_play_timer_reveal()}
+
+
+ {m.kv_edit_final_round()}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{m.kv_play_seconds()}
+
+
+
+
+
+
+
{m.kv_play_seconds()}
+
+
+
+
+ (settings.enableFinalRound =
+ !settings.enableFinalRound)}
+ />
+ {#if settings.enableFinalRound}
+
+ {m.kv_edit_question()}
+
+ {/if}
+
+
+
+
+
+
+
+
+
+ {m.kv_edit_points()}
+
+
+ {m.kv_edit_values()}
+
+
+ {m.kv_edit_negative_scores()}
+
+
+ {m.kv_edit_teams_label()}
+
+
+
+
+
+
+
+
+
+
+
+
+ {#each settings.pointValues as val, i}
+
+ {#if settings.pointValuePreset === "custom"}
+
+ {:else}
+ {val}
+ {/if}
+
+ {/each}
+
+
+
+
+ (settings.allowNegativeScores =
+ !settings.allowNegativeScores)}
+ />
+
+
+
+ {#each teams as team (team.id)}
+
+
+
+
+ {/each}
+ {#if teams.length < 6}
+
+ {/if}
+
+
+
+
+
+
+
+
+ {#each rounds as round, ri}
+
+
+
+
+ {ri === 0 ? m.kv_edit_r1() : m.kv_edit_r2()}
+ ({m.kv_edit_dd_count()}
+ {countDailyDoubles(ri)}/{settings
+ .dailyDoublesPerRound[ri] ?? 1})
+
+
+
+
+
+ {#each round.categories as cat, ci}
+
+
+
+ {/each}
+
+
+
+
+ {#each { length: settings.questionsPerCategory } as _, qi}
+ {#each round.categories as cat, ci}
+ {@const q = cat.questions[qi]}
+
+ {/each}
+ {/each}
+
+
+ {/each}
+
+
+
+
+{#if editingQuestion}
+ {@const q =
+ rounds[editingQuestion.roundIndex].categories[editingQuestion.catIndex]
+ .questions[editingQuestion.qIndex]}
+ {@const cat =
+ rounds[editingQuestion.roundIndex].categories[editingQuestion.catIndex]}
+ {@const maxDD =
+ settings.dailyDoublesPerRound[editingQuestion.roundIndex] ?? 1}
+ {@const currentDD = countDailyDoubles(editingQuestion.roundIndex)}
+
+ e.target === e.currentTarget && handleQuestionCloseClick()}
+ onkeydown={(e) => e.key === "Escape" && handleQuestionCloseClick()}
+ role="dialog"
+ tabindex="-1"
+ >
+
+
+
+
+ {cat.name || m.kv_edit_category()} - {q.points}€
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = maxDD}
+ />
+
+ {m.kv_edit_daily_double()} ({currentDD}/{maxDD})
+
+
+
+
+
+
+ {m.kv_edit_save_exit()}
+
+
+
+
+
+ {
+ showQuestionCloseConfirm = false;
+ saveQuestion();
+ }}
+ />
+{/if}
+
+
+{#if editingFinalQuestion}
+ e.target === e.currentTarget && handleFinalCloseClick()}
+ onkeydown={(e) => e.key === "Escape" && handleFinalCloseClick()}
+ role="dialog"
+ tabindex="-1"
+ >
+
+
+
+ {m.kv_edit_final_round()}
+
+
+
+
+
+
+
+ {m.kv_edit_save_exit()}
+
+
+
+
+
+ {
+ showFinalCloseConfirm = false;
+ saveFinalQuestion();
+ }}
+ />
+{/if}
+
+
+
+
+
+
+
+
+{#if isStarting}
+
+
+
+ {m.kv_edit_starting_game()}
+
+
+{/if}
+
+
+
+
+
+
+
+
diff --git a/src/routes/kuldvillak/edit/+page.svelte.new b/src/routes/kuldvillak/edit/+page.svelte.new
new file mode 100644
index 0000000..e69de29
diff --git a/src/routes/kuldvillak/play/+page.svelte b/src/routes/kuldvillak/play/+page.svelte
index 18c0e03..36df8ac 100644
--- a/src/routes/kuldvillak/play/+page.svelte
+++ b/src/routes/kuldvillak/play/+page.svelte
@@ -3,10 +3,7 @@
import { gameSession } from "$lib/stores/gameSession.svelte";
import ProjectorView from "./ProjectorView.svelte";
import ModeratorView from "./ModeratorView.svelte";
- import {
- KvButtonSecondary,
- KvSpinner,
- } from "$lib/components/kuldvillak/ui";
+ import { KvButton, KvSpinner } from "$lib/components/kuldvillak/ui";
import * as m from "$lib/paraglide/messages";
import faviconKuldvillak from "$lib/assets/kuldvillak_favicon.svg";
@@ -40,9 +37,9 @@
{m.kv_play_loading_hint()}
-
+
{m.kv_play_go_to_editor()}
-
+
@@ -64,9 +61,9 @@
{m.kv_play_loading_hint()}
-
+
{m.kv_play_go_to_editor()}
-
+
diff --git a/src/routes/kuldvillak/play/ModeratorView.svelte b/src/routes/kuldvillak/play/ModeratorView.svelte
index ca0fcbe..98e2ad1 100644
--- a/src/routes/kuldvillak/play/ModeratorView.svelte
+++ b/src/routes/kuldvillak/play/ModeratorView.svelte
@@ -4,8 +4,7 @@
import { onMount } from "svelte";
import * as m from "$lib/paraglide/messages";
import {
- KvButtonPrimary,
- KvButtonSecondary,
+ KvButton,
KvNumberInput,
KvEditCard,
} from "$lib/components/kuldvillak/ui";
@@ -14,6 +13,31 @@
// Only moderator controls the timer
onMount(() => {
gameSession.enableTimerControl();
+
+ // Add spacebar event listener for timer control
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.code === "Space") {
+ e.preventDefault(); // Prevent scrolling
+ if (
+ ["question", "final-question"].includes(
+ session?.phase || "",
+ ) &&
+ !session?.showAnswer
+ ) {
+ if (session?.timerRunning) {
+ gameSession.stopTimer();
+ } else {
+ gameSession.startTimer();
+ }
+ }
+ }
+ };
+
+ window.addEventListener("keydown", handleKeyDown);
+
+ return () => {
+ window.removeEventListener("keydown", handleKeyDown);
+ };
});
// Settings modal state
@@ -162,24 +186,22 @@
class="bg-kv-blue flex flex-col md:flex-row items-start md:items-center justify-between p-4 gap-4"
>
-
-
+
+
{session.name}
{#if session.phase.startsWith("final")}
-
+
{m.kv_final_round()}
{:else}
-
+
{session.currentRoundIndex === 0
? m.kv_edit_r1()
: m.kv_edit_r2()}
-
+
({m.kv_edit_dd_short()}
{countRemainingDailyDoubles(
session.currentRoundIndex,
@@ -194,31 +216,34 @@
-
+
{m.kv_play_open_projector()}
-
+
{#if session.phase === "board"}
{#if session.rounds.length > 1 && session.currentRoundIndex === 0}
- gameSession.nextRound()}
>
{m.kv_play_next_round()}
-
+
{:else if session.settings.enableFinalRound && session.finalRound}
- gameSession.goToFinalRound()}
>
{m.kv_play_go_to_final()}
-
+
{/if}
{/if}
- (showEndGameConfirm = true)}
>
{m.kv_play_end_game()}
-
+
@@ -227,9 +252,7 @@
{@const lastCorrectTeam = session.teams.find(
(t) => t.id === session.lastCorrectTeamId,
)}
-
+
{m.kv_play_last_answer()}:
{lastCorrectTeam?.name}
@@ -240,9 +263,7 @@
{/if}
-
+
{m.kv_play_adjust_score()}:
{#if session.phase === "intro"}
-
+
{session.currentRoundIndex === 0
? m.kv_edit_r1()
: m.kv_edit_r2()}
{#if !session.categoriesIntroduced}
-
gameSession.startCategoryIntro()}
>
{m.kv_play_introduce_categories()}
-
+
{/if}
- gameSession.startBoard()}
>
{session.categoriesIntroduced
? m.kv_play_start_game()
: m.kv_play_skip_to_game()}
-
+
{:else if session.phase === "intro-categories"}
{#if !introDelayComplete && session.introCategoryIndex === 0}
-
+
{session.currentRoundIndex === 0
? m.kv_edit_r1()
: m.kv_edit_r2()}
-
+
{m.kv_play_introducing_categories({
seconds: introCountdown,
})}
{:else if currentRound?.categories[session.introCategoryIndex]}
-
+
{currentRound.categories[
session.introCategoryIndex
].name}
-
+
{session.introCategoryIndex + 1} / {currentRound
.categories.length}
{:else}
- gameSession.startBoard()}
>
{m.kv_play_start_game()}
-
+
{/if}
{/if}
@@ -397,9 +411,7 @@
-
+
{cat.name || "???"}
@@ -439,24 +451,22 @@
-
+
{m.kv_play_daily_double()}!
{#each session.teams as team}
-
+
{/each}
@@ -471,32 +481,29 @@
)}
{@const isValidWager =
wagerInput >= 5 && wagerInput <= maxWager}
-
+
{m.kv_play_wager_range({ min: 5, max: maxWager })}
-
+
{m.kv_play_wager()}:
-
{m.kv_play_confirm()}
-
+
{/if}
@@ -512,9 +519,7 @@
(t) => t.id === session.lastAnsweredTeamId,
)?.name ?? ""}
-
+
{m.kv_play_correct_return({
name: lastTeamName,
seconds: session.revealCountdown,
@@ -522,18 +527,14 @@
{:else if session.revealCountdown !== null && session.revealCountdown > 0}
-
+
{m.kv_play_answer_revealed({
seconds: session.revealCountdown,
})}
{:else if session.skippingQuestion && session.timeoutCountdown !== null && session.timeoutCountdown > 0}
-
+
{m.kv_play_skip_reveal({
seconds: session.timeoutCountdown,
})}
@@ -544,9 +545,7 @@
(t) => t.id === session.lastAnsweredTeamId,
)?.name ?? ""}
-
+
{m.kv_play_wrong_reveal({
name: lastTeamName,
seconds: session.timeoutCountdown,
@@ -554,9 +553,7 @@
{:else if session.timeoutCountdown !== null && session.timeoutCountdown > 0}
-
+
{m.kv_play_timeout_reveal({
seconds: session.timeoutCountdown,
})}
@@ -567,9 +564,7 @@
(t) => t.id === session.lastAnsweredTeamId,
)?.name ?? ""}
-
+
{m.kv_play_wrong_waiting({ name: lastTeamName })}
{:else if session.activeTeamId && !session.timerRunning && !session.showAnswer}
@@ -577,16 +572,12 @@
session.teams.find((t) => t.id === session.activeTeamId)
?.name ?? ""}
-
+
{m.kv_play_timer_paused({ name: activeTeamName })}
{:else if !session.activeTeamId && !session.showAnswer && (session.timerRunning || session.timerSeconds > 0)}
-
+
{m.kv_play_click_team_to_answer()}
{/if}
@@ -600,65 +591,67 @@
-
+
{m.kv_play_answer_short()}: {questionData.question.answer}
-
+
{m.kv_play_timer()}
{session.timerSeconds}
-
+
{m.kv_play_seconds()}
{#if session.timerRunning}
-
+
{:else}
-
+
{/if}
-
-
+
@@ -668,34 +661,33 @@
class="bg-kv-blue flex-1 flex flex-col items-center justify-center gap-8 p-8"
>
{#if session.finalCategoryRevealed || session.phase === "final-category"}
-
-
+
+
{session.finalRound?.category}
-
+
{m.kv_play_final_round()}
- gameSession.showFinalQuestion()}
- >
- {m.kv_play_question_short()}
-
+ {#if session.finalCategoryRevealed}
+
+ gameSession.showFinalQuestion()}
+ >
+ {m.kv_play_question_short()}
+
+ {/if}
{:else}
-
+
{m.kv_play_final_round()}
- gameSession.startFinalCategoryReveal()}
>
{m.kv_play_reveal_category()}
-
+
{/if}
{:else if session.phase === "final-question"}
@@ -708,15 +700,11 @@
>
{#if activeTeam}
-
+
{m.kv_play_judging()}: {activeTeam.name}
{:else}
-
+
{m.kv_play_click_team_to_judge()}
{/if}
@@ -730,68 +718,63 @@
-
+
{m.kv_play_answer_short()}: {session.finalRound?.answer}
-
+
{m.kv_play_timer()}
{session.timerSeconds}
-
+
{m.kv_play_seconds()}
{#if session.timerRunning}
-
+
{:else}
-
+
{/if}
-
+
{#if session.finalRevealed.length === session.teams.length}
-
gameSession.showFinalScores()}
>
{m.kv_play_show_scores()}
-
+
{/if}
{:else if session.phase === "final-scores"}
@@ -813,21 +796,16 @@
class="bg-kv-blue flex flex-col items-center justify-center gap-2 p-2"
>
{team.score}€
-
+
{team.name}
-
+
#{i + 1}
@@ -844,21 +822,16 @@
class="bg-kv-blue flex flex-col items-center justify-center gap-2 p-2"
>
{team.score}€
-
+
{team.name}
-
+
#{i + topRowCount + 1}
@@ -866,11 +839,12 @@
{/if}
- (showEndGameConfirm = true)}
>
{m.kv_play_end_game()}
-
+
{:else if session.phase === "finished"}
@@ -878,14 +852,16 @@
-
+
{m.kv_play_game_over()}!
-
+
{m.kv_play_finish()}
-
+
{/if}
diff --git a/src/routes/layout.css b/src/routes/layout.css
index 3649c1a..7425ace 100644
--- a/src/routes/layout.css
+++ b/src/routes/layout.css
@@ -9,7 +9,7 @@
--color-kv-blue: var(--kv-blue);
--color-kv-yellow: var(--kv-yellow);
--color-kv-green: #009900;
- --color-kv-red: #FF3333;
+ --color-kv-red: #CC0000;
--color-kv-black: var(--kv-background);
--color-kv-white: var(--kv-text);
/* Additional theme-aware colors */
@@ -87,7 +87,7 @@
--kv-text: #FFFFFF;
--kv-background: #000000;
--kv-green: #009900;
- --kv-red: #FF3333;
+ --kv-red: #CC0000;
--kv-black: #000000;
--kv-white: #FFFFFF;
@@ -195,6 +195,135 @@
text-shadow: var(--kv-shadow-text);
}
+/* ============================================
+ Kuldvillak Semantic Typography Scale
+ 6-tier system: 3 headings + 3 body sizes
+ All text includes shadow by default
+ ============================================ */
+
+/* --- HEADINGS (with shadow) --- */
+
+/* H1: Main Page Titles - 2.5rem/40px */
+.kv-h1 {
+ font-family: var(--kv-font-body);
+ font-size: 2rem;
+ line-height: 1.1;
+ text-transform: uppercase;
+ text-shadow: var(--kv-shadow-text);
+}
+
+@media (min-width: 768px) {
+ .kv-h1 {
+ font-size: 2.5rem;
+ }
+}
+
+/* H2: Section Headers / Big Prompts - 1.875rem/30px */
+.kv-h2 {
+ font-family: var(--kv-font-body);
+ font-size: 1.5rem;
+ line-height: 1.2;
+ text-transform: uppercase;
+ text-shadow: var(--kv-shadow-text);
+}
+
+@media (min-width: 768px) {
+ .kv-h2 {
+ font-size: 1.875rem;
+ }
+}
+
+/* H3: Card/Modal Titles - 1.5rem/24px */
+.kv-h3 {
+ font-family: var(--kv-font-body);
+ font-size: 1.25rem;
+ line-height: 1.3;
+ text-transform: uppercase;
+ text-shadow: var(--kv-shadow-text);
+}
+
+@media (min-width: 768px) {
+ .kv-h3 {
+ font-size: 1.5rem;
+ }
+}
+
+/* --- BODY TEXT (with shadow) --- */
+
+/* Body Large: Timer, Important Labels - 1.125rem/18px */
+.kv-body-lg {
+ font-family: var(--kv-font-body);
+ font-size: 1rem;
+ line-height: 1.4;
+ text-transform: uppercase;
+ text-shadow: var(--kv-shadow-text);
+}
+
+@media (min-width: 768px) {
+ .kv-body-lg {
+ font-size: 1.125rem;
+ }
+}
+
+/* Body: Standard text - 1rem/16px */
+.kv-body {
+ font-family: var(--kv-font-body);
+ font-size: 0.875rem;
+ line-height: 1.4;
+ text-transform: uppercase;
+ text-shadow: var(--kv-shadow-text);
+}
+
+@media (min-width: 768px) {
+ .kv-body {
+ font-size: 1rem;
+ }
+}
+
+/* Body Small: Captions, footers - 0.875rem/14px */
+.kv-body-sm {
+ font-family: var(--kv-font-body);
+ font-size: 0.75rem;
+ line-height: 1.4;
+ text-transform: uppercase;
+ text-shadow: var(--kv-shadow-text);
+}
+
+@media (min-width: 768px) {
+ .kv-body-sm {
+ font-size: 0.875rem;
+ }
+}
+
+/* --- NO-SHADOW VARIANTS (for buttons, inputs) --- */
+
+.kv-h1-plain,
+.kv-h2-plain,
+.kv-h3-plain,
+.kv-body-lg-plain,
+.kv-body-plain,
+.kv-body-sm-plain {
+ font-family: var(--kv-font-body);
+ text-transform: uppercase;
+ text-shadow: none;
+}
+
+.kv-h1-plain { font-size: 2rem; line-height: 1.1; }
+.kv-h2-plain { font-size: 1.5rem; line-height: 1.2; }
+.kv-h3-plain { font-size: 1.25rem; line-height: 1.3; }
+.kv-body-lg-plain { font-size: 1rem; line-height: 1.4; }
+.kv-body-plain { font-size: 0.875rem; line-height: 1.4; }
+.kv-body-sm-plain { font-size: 0.75rem; line-height: 1.4; }
+
+@media (min-width: 768px) {
+ .kv-h1-plain { font-size: 2.5rem; }
+ .kv-h2-plain { font-size: 1.875rem; }
+ .kv-h3-plain { font-size: 1.5rem; }
+ .kv-body-lg-plain { font-size: 1.125rem; }
+ .kv-body-plain { font-size: 1rem; }
+ .kv-body-sm-plain { font-size: 0.875rem; }
+}
+
/* ============================================
Global Styles
============================================ */
diff --git a/src/test/mocks/app-environment.ts b/src/test/mocks/app-environment.ts
new file mode 100644
index 0000000..801f497
--- /dev/null
+++ b/src/test/mocks/app-environment.ts
@@ -0,0 +1,5 @@
+// Mock for $app/environment used in tests
+export const browser = true;
+export const building = false;
+export const dev = true;
+export const version = 'test';
diff --git a/vitest.config.ts b/vitest.config.ts
index 5430562..6ecab4b 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -6,8 +6,8 @@ export default defineConfig({
environment: 'jsdom',
globals: true,
alias: {
- '$lib': '/src/lib',
- '$app': '/src/app',
+ '$lib': new URL('./src/lib', import.meta.url).pathname,
+ '$app/environment': new URL('./src/test/mocks/app-environment.ts', import.meta.url).pathname,
},
},
});