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)} + +{/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 @@ + + +
+ + + + +
+ fileInput.click()} + ariaLabel={m.kv_edit_load()} + > + + {@html LoadIcon} + + + + + {@html SaveIcon} + + + + + {@html ResetIcon} + + + {#if onSettings} + + + {@html SettingsIcon} + + + {/if} +
+ + + + + ▶ {m.kv_edit_start()} + +
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} + + + 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" + > + + {@html RemoveIcon} + + + {/if} +
+ {/each} + + {#if editorStore.canAddTeam} + editorStore.addTeam()} + ariaLabel={m.kv_edit_add_team()} + > + + {@html AddIcon} + + + {/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, }, }, });