feat(#48): Add dark mode

This commit is contained in:
Josh Creek
2026-01-10 21:12:25 +00:00
parent ad66b874e9
commit d50417ffef
7 changed files with 158 additions and 57 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en" data-theme="pokeball">
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
+62
View File
@@ -0,0 +1,62 @@
<script lang="ts">
import { theme, applyTheme } from '$lib/stores/theme.js';
import { onMount } from 'svelte';
let currentTheme: 'pokeball' | 'dark';
// Subscribe to theme changes
$: if ($theme) {
currentTheme = $theme;
applyTheme($theme);
}
onMount(() => {
// Initialize theme from localStorage
theme.init();
});
function toggleTheme() {
theme.toggle();
}
</script>
<button
class="btn btn-ghost btn-circle"
on:click={toggleTheme}
aria-label="Toggle dark mode"
title="Toggle dark mode"
>
{#if currentTheme === 'pokeball'}
<!-- Sun icon for light mode (clicking will switch to dark) -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
{:else}
<!-- Moon icon for dark mode (clicking will switch to light) -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
</svg>
{/if}
</button>
@@ -59,7 +59,7 @@
>
<div class="dex-column pokedex-entry-container">
<div class="flex mb-2">
<div class="sprite-container flex justify-center items-center bg-white rounded-lg p-2">
<div class="sprite-container flex justify-center items-center bg-base-100 rounded-lg p-2">
<PokemonSprite
pokemonName={pokedexEntry.pokemon}
pokedexNumber={pokedexEntry.pokedexNumber}
@@ -68,18 +68,20 @@
/>
</div>
<div class="pl-2">
<h3 class="text-xl font-bold pt-1 text-secondary">{pokedexEntry.pokemon}</h3>
<sub class="text-gray-200">#{pokedexEntry.pokedexNumber.toString().padStart(3, '0')}</sub>
<h3 class="text-xl font-bold pt-1">{pokedexEntry.pokemon}</h3>
<sub class="text-primary-content/80"
>#{pokedexEntry.pokedexNumber.toString().padStart(3, '0')}</sub
>
</div>
</div>
{#if showForms}
<div class="bg-white text-black rounded-lg p-4 mb-2">
<div class="bg-base-100 text-base-content rounded-lg p-4 mb-2">
<p><strong>Form:</strong> {pokedexEntry.form ? pokedexEntry.form : '-'}</p>
</div>
{/if}
<div class="bg-white text-black rounded-lg p-4">
<div class="bg-base-100 text-base-content rounded-lg p-4">
{#if pokedexEntry.evolutionInformation}
<p><strong>How to evolve: </strong>{pokedexEntry.evolutionInformation}</p>
{:else}
@@ -95,7 +97,9 @@
</div>
{#if catchRecord}
<div class="dex-column catch-record-container bg-white text-black rounded-lg p-4 mb-4 md:mb-0">
<div
class="dex-column catch-record-container bg-base-100 text-base-content rounded-lg p-4 mb-4 md:mb-0"
>
<div class="flex items-center">
<div class="form-control">
<label class="cursor-pointer label">
@@ -103,7 +107,7 @@
<input
type="checkbox"
bind:checked={catchRecord.caught}
class="checkbox checkbox-primary border-black"
class="checkbox checkbox-primary"
on:change={onCaughtChange}
/>
</label>
@@ -116,7 +120,7 @@
<input
type="checkbox"
bind:checked={catchRecord.haveToEvolve}
class="checkbox checkbox-primary border-black"
class="checkbox checkbox-primary"
on:change={onNeedsToEvolveChange}
/>
</label>
@@ -126,30 +130,30 @@
<div class="form-control">
<label class="cursor-pointer label">
<span class="block font-bold mr-2">In Home:</span>
<input
type="checkbox"
bind:checked={catchRecord.inHome}
class="checkbox checkbox-primary border-black"
on:change={() => updateCatchRecord('toggle')}
/>
</label>
</div>
</div>
{#if pokedexEntry.canGigantamax && showForms}
<div class="flex items-center">
<div class="form-control">
<label class="cursor-pointer label">
<span class="block font-bold mr-2">Has Gigantamaxed:</span>
<input
type="checkbox"
bind:checked={catchRecord.hasGigantamaxed}
class="checkbox checkbox-primary border-black"
bind:checked={catchRecord.inHome}
class="checkbox checkbox-primary"
on:change={() => updateCatchRecord('toggle')}
/>
</label>
</div>
</div>
{/if}
{#if pokedexEntry.canGigantamax && showForms}
<div class="flex items-center">
<div class="form-control">
<label class="cursor-pointer label">
<span class="block font-bold mr-2">Has Gigantamaxed:</span>
<input
type="checkbox"
bind:checked={catchRecord.hasGigantamaxed}
class="checkbox checkbox-primary"
on:change={() => updateCatchRecord('toggle')}
/>
</label>
</div>
</div>
{/if}
<p>
<label
class="block font-bold mb-1"
@@ -158,7 +162,7 @@
<textarea
bind:value={catchRecord.personalNotes}
id={`personalNotesInput-${catchRecord._id || pokedexEntry._id}`}
class="form-textarea w-full p-2 border rounded"
class="textarea textarea-bordered w-full"
style="min-height: 120px;"
on:input={() => updateCatchRecord('notes')}
on:change={() => updateCatchRecord('notes-blur')}
@@ -169,7 +173,7 @@
<div class="dex-column additional-details-container">
{#if showOrigins}
<div class="bg-white text-black rounded-lg p-4 mb-2">
<div class="bg-base-100 text-base-content rounded-lg p-4 mb-2">
<h3 class="text-xl font-semibold mb-4">Origin Dex Requirements</h3>
<p><strong>Region to Catch In:</strong> {pokedexEntry.regionToCatchIn}</p>
<p><strong>Games to catch in:</strong></p>
@@ -184,7 +188,7 @@
</div>
{/if}
{#if pokedexEntry.catchInformation.length > 0}
<div class="bg-white text-black rounded-lg p-4 mb-2">
<div class="bg-base-100 text-base-content rounded-lg p-4 mb-2">
<p><strong>Where to catch: </strong></p>
<ul class="list-disc list-inside">
{#each pokedexEntry.catchInformation as info}
@@ -206,7 +210,7 @@
</p>
</div>
{:else}
<div class="bg-white text-black rounded-lg p-4 mb-2">
<div class="bg-base-100 text-base-content rounded-lg p-4 mb-2">
<p>
<strong>Where to catch: </strong>Currently missing - can you
<a
@@ -229,27 +233,4 @@
width: 68px;
height: 68px;
}
.form-checkbox {
appearance: none;
background-color: #fff;
border: 2px solid #000;
border-radius: 0.25rem;
width: 1.5rem;
height: 1.5rem;
display: inline-block;
position: relative;
}
.form-checkbox:checked {
background-color: #00bfff;
background-size: 100% 100%;
background-position: center center;
background-repeat: no-repeat;
}
.form-textarea {
min-height: 3rem;
background-color: #fff;
}
</style>
@@ -25,7 +25,7 @@
{#if isOpen}
<div class="modal modal-open" role="dialog" aria-modal="true">
<div class="modal-box-custom">
<div class="modal-box-custom bg-primary text-primary-content">
<button class="close-button" on:click={onClose} aria-label="Close"></button>
<div class="modal-content">
<slot />
@@ -52,7 +52,6 @@
max-width: 72rem;
width: 100%;
max-height: 90vh;
background-color: rgba(220, 38, 38, 0.95);
border: none;
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.5);
outline: none;
@@ -175,9 +175,9 @@
} else {
const placement = calculateBoxPlacement(index);
if (placement.column % 2 === 0) {
return 'background-color: #ffffff';
return 'background-color: var(--ld-box-bg-even);';
} else {
return 'background-color: #f9f9f9;';
return 'background-color: var(--ld-box-bg-odd);';
}
}
}
@@ -596,6 +596,26 @@
</main>
<style>
/*
Theme-aware backgrounds for non-caught box slots.
- Light mode (`pokeball`) keeps the original exact colors.
- Dark mode maps to DaisyUI theme base tokens so it stays consistent with the active theme.
*/
:global(:root) {
--ld-box-bg-even: var(--fallback-b1, oklch(var(--b1) / 1));
--ld-box-bg-odd: var(--fallback-b2, oklch(var(--b3) / 1));
}
:global([data-theme='pokeball']) {
--ld-box-bg-even: #ffffff;
--ld-box-bg-odd: #f9f9f9;
}
:global([data-theme='dark']) {
--ld-box-bg-even: var(--fallback-b1, oklch(var(--b1) / 1));
--ld-box-bg-odd: var(--fallback-b2, oklch(var(--b3) / 1));
}
.boxes-grid {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
+37
View File
@@ -0,0 +1,37 @@
import { writable } from 'svelte/store';
type Theme = 'pokeball' | 'dark';
const STORAGE_KEY = 'theme';
function createThemeStore() {
// Get stored theme from localStorage or default to 'pokeball'
const storedTheme = (typeof window !== 'undefined' &&
localStorage.getItem(STORAGE_KEY)) as Theme | null;
const initialTheme: Theme = storedTheme || 'pokeball';
const { subscribe, set, update } = writable<Theme>(initialTheme);
return {
subscribe,
set,
toggle: () => update((theme) => (theme === 'pokeball' ? 'dark' : 'pokeball')),
init: () => {
if (typeof window !== 'undefined') {
const stored = localStorage.getItem(STORAGE_KEY) as Theme | null;
if (stored) {
set(stored);
}
}
}
};
}
export const theme = createThemeStore();
// Helper function to apply theme to HTML element
export function applyTheme(themeValue: Theme) {
if (typeof document !== 'undefined') {
document.documentElement.setAttribute('data-theme', themeValue);
}
}
+2
View File
@@ -5,6 +5,7 @@
import { type User } from '@supabase/auth-js';
import SignIn from '$lib/components/SignIn.svelte';
import SignOut from '$lib/components/SignOut.svelte';
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
import { pwaInfo } from 'virtual:pwa-info';
import { pwaAssetsHead } from 'virtual:pwa-assets/head';
@@ -137,6 +138,7 @@
<a class="btn btn-ghost text-xl" href="/">Living Dex Tracker</a>
</div>
<div class="navbar-end">
<ThemeToggle />
<div class="dropdown dropdown-end">
{#if localUser}
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar">