added theme toggle

This commit is contained in:
liyunze 2026-09-01 15:26:16 +08:00
parent f807ae9500
commit be5f9e7536
6 changed files with 150 additions and 14 deletions

View file

@ -78,7 +78,7 @@ define(['./workbox-7e5eb42b'], (function (workbox) { 'use strict';
*/
workbox.precacheAndRoute([{
"url": "/",
"revision": "0.4n2fvpih6f8"
"revision": "0.5d3f7lf01og"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/"), {

View file

@ -12,6 +12,19 @@
<meta name="apple-mobile-web-app-title" content="Notebook" />
<meta name="format-detection" content="telephone=no" />
<meta name="theme-color" content="#ffffff" />
<script>
(function () {
var stored = localStorage.getItem("dsec_theme");
var dark =
stored === "dark" ||
(!stored && window.matchMedia("(prefers-color-scheme: dark)").matches);
var root = document.documentElement;
root.classList.toggle("dark", dark);
root.style.colorScheme = dark ? "dark" : "light";
var meta = document.querySelector('meta[name="theme-color"]');
if (meta) meta.setAttribute("content", dark ? "#131014" : "#ffffff");
})();
</script>
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png" />

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { isAuthenticated, initAuth, logout, currentUser } from "$lib/stores/auth";
import { theme, toggleTheme } from "$lib/stores/theme";
import { onMount } from "svelte";
import { goto } from "$app/navigation";
import { page } from "$app/state";
@ -8,6 +9,7 @@
let auth = $state(false);
let admin = $state(false);
let searchQuery = $state("");
let isDark = $state(false);
function submitSearch(e: SubmitEvent) {
e.preventDefault();
@ -22,9 +24,11 @@
initAuth();
const unsubAuth = isAuthenticated.subscribe((v) => (auth = v));
const unsubUser = currentUser.subscribe((u) => (admin = u?.role === "admin"));
const unsubTheme = theme.subscribe((t) => (isDark = t === "dark"));
return () => {
unsubAuth();
unsubUser();
unsubTheme();
};
});
@ -34,7 +38,7 @@
const onAdmin = $derived(path === "/admin" || path.startsWith("/admin/"));
</script>
<header class="border-rule border-b bg-white">
<header class="border-rule bg-surface border-b">
<div class="mx-auto flex h-16 max-w-4xl items-center justify-between px-4 sm:px-6">
<a href="/" class="text-ink font-serif text-[1.65rem] leading-none">Notebook</a>
@ -60,13 +64,50 @@
{:else}
<a href="/auth/login" class="nav-link">Sign in</a>
{/if}
<button
type="button"
class="text-muted hover:text-ink p-1"
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
onclick={toggleTheme}
>
{#if isDark}
<svg
class="h-5 w-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
aria-hidden="true"
>
<circle cx="12" cy="12" r="4" />
<path
stroke-linecap="square"
d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"
/>
</svg>
{:else}
<svg
class="h-5 w-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
aria-hidden="true"
>
<path
stroke-linecap="square"
d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"
/>
</svg>
{/if}
</button>
<form onsubmit={submitSearch}>
<input
type="search"
bind:value={searchQuery}
placeholder="Search"
aria-label="Search notes and questions"
class="border-rule text-ink placeholder:text-faint focus:border-primary w-40 rounded-none border bg-white px-2.5 py-1.5 text-[11px] tracking-wide transition-colors outline-none"
class="border-rule text-ink placeholder:text-faint focus:border-primary bg-surface w-40 rounded-none border px-2.5 py-1.5 text-[11px] tracking-wide transition-colors outline-none"
/>
</form>
</nav>
@ -96,7 +137,7 @@
bind:value={searchQuery}
placeholder="Search"
aria-label="Search notes and questions"
class="border-rule text-ink placeholder:text-faint focus:border-primary w-full rounded-none border bg-white px-3 py-2 text-sm tracking-wide transition-colors outline-none"
class="border-rule text-ink placeholder:text-faint focus:border-primary bg-surface w-full rounded-none border px-3 py-2 text-sm tracking-wide transition-colors outline-none"
/>
</form>
<a href="/notes" class="nav-link block" onclick={() => (mobileMenuOpen = false)}
@ -129,6 +170,43 @@
onclick={() => (mobileMenuOpen = false)}>Sign in</a
>
{/if}
<button
type="button"
class="text-muted hover:text-ink flex items-center gap-2 p-1"
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
onclick={toggleTheme}
>
{#if isDark}
<svg
class="h-5 w-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
aria-hidden="true"
>
<circle cx="12" cy="12" r="4" />
<path
stroke-linecap="square"
d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"
/>
</svg>
{:else}
<svg
class="h-5 w-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
aria-hidden="true"
>
<path
stroke-linecap="square"
d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"
/>
</svg>
{/if}
</button>
</nav>
{/if}
</header>

29
src/lib/stores/theme.ts Normal file
View file

@ -0,0 +1,29 @@
import { get, writable } from "svelte/store";
export type Theme = "light" | "dark";
const STORAGE_KEY = "dsec_theme";
function getInitialTheme(): Theme {
if (typeof window === "undefined") return "light";
const stored = localStorage.getItem(STORAGE_KEY);
if (stored === "light" || stored === "dark") return stored;
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
export const theme = writable<Theme>(getInitialTheme());
function applyTheme(value: Theme) {
const root = document.documentElement;
root.classList.toggle("dark", value === "dark");
root.style.colorScheme = value;
const meta = document.querySelector('meta[name="theme-color"]');
if (meta) meta.setAttribute("content", value === "dark" ? "#131014" : "#ffffff");
}
export function toggleTheme() {
const next: Theme = get(theme) === "dark" ? "light" : "dark";
localStorage.setItem(STORAGE_KEY, next);
theme.set(next);
applyTheme(next);
}

View file

@ -26,14 +26,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
</svelte:head>
<div class="flex min-h-screen flex-col bg-white">
<div class="bg-surface flex min-h-screen flex-col">
<Navbar />
<main class="flex-1">
{@render children()}
</main>
<footer class="border-rule mt-16 border-t">
<div class="mx-auto max-w-4xl px-4 py-8 sm:px-6">
<p class="kicker">Notebook — written by students, for students.</p>
<p class="kicker">DSEC Notebook</p>
<p class="text-faint mt-2 text-xs">
DSEC Notebook is a community resource for Deakin University students. Not affiliated
with Deakin University.

View file

@ -11,17 +11,33 @@
--color-muted: #737373;
--color-faint: #a3a3a3;
--color-rule: #e5e5e5;
--color-surface: #ffffff;
--color-on-primary: #ffffff;
}
.dark {
--color-primary: #ea4c65;
--color-primary-dark: #ea4c65;
--color-secondary: #7fdfea;
--color-secondary-dark: #5fd4e2;
--color-ink: #f5f5f5;
--color-muted: #b3b3b3;
--color-faint: #7d7d7d;
--color-rule: #2a2a2a;
--color-surface: #131014;
--color-on-primary: #1a1a1a;
}
html {
font-family: var(--font-sans);
color: var(--color-ink);
background: #fff;
background: var(--color-surface);
-webkit-font-smoothing: antialiased;
}
a, button {
cursor: pointer;
a,
button {
cursor: pointer;
}
.page {
@ -33,11 +49,11 @@ a, button {
}
.btn-primary {
@apply border-primary bg-primary hover:bg-primary-dark hover:border-primary-dark inline-flex items-center justify-center rounded-none border px-5 py-2.5 text-[11px] font-medium tracking-[0.14em] text-white uppercase transition-colors disabled:cursor-not-allowed disabled:opacity-50;
@apply border-primary bg-primary hover:brightness-110 inline-flex items-center justify-center rounded-none border px-5 py-2.5 text-[11px] font-medium tracking-[0.14em] text-white uppercase transition-colors disabled:cursor-not-allowed disabled:opacity-50;
}
.btn-secondary {
@apply border-secondary text-secondary hover:bg-secondary inline-flex items-center justify-center rounded-none border bg-transparent px-5 py-2.5 text-[11px] font-medium tracking-[0.14em] uppercase transition-colors hover:text-white disabled:cursor-not-allowed disabled:opacity-50;
@apply border-secondary text-secondary hover:bg-secondary hover:text-on-primary inline-flex items-center justify-center rounded-none border bg-transparent px-5 py-2.5 text-[11px] font-medium tracking-[0.14em] uppercase transition-colors disabled:cursor-not-allowed disabled:opacity-50;
}
.btn-ghost {
@ -45,15 +61,15 @@ a, button {
}
.field {
@apply border-rule text-ink placeholder:text-faint focus:border-primary block w-full rounded-none border bg-white px-3 py-2.5 text-sm transition-colors outline-none;
@apply border-rule text-ink placeholder:text-faint focus:border-primary bg-surface block w-full rounded-none border px-3 py-2.5 text-sm transition-colors outline-none;
}
.chip {
@apply border-rule text-ink hover:border-ink inline-flex items-center rounded-none border bg-white px-3 py-1.5 font-sans text-[11px] font-medium tracking-[0.12em] uppercase transition-colors;
@apply border-rule text-ink hover:border-ink bg-surface inline-flex items-center rounded-none border px-3 py-1.5 font-sans text-[11px] font-medium tracking-[0.12em] uppercase transition-colors;
}
.chip-active {
@apply border-primary bg-primary hover:border-primary-dark hover:bg-primary-dark text-white;
@apply border-primary bg-primary hover:border-primary hover:bg-primary text-white;
}
.nav-link {