mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-07-12 18:43:45 +00:00
feat(#48): Add dark mode
This commit is contained in:
+1
-1
@@ -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" />
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user