mirror of
https://github.com/dsec-hub/dsec-notebook.git
synced 2026-09-22 07:24:27 +00:00
added theme toggle
This commit is contained in:
parent
f807ae9500
commit
be5f9e7536
6 changed files with 150 additions and 14 deletions
|
|
@ -78,7 +78,7 @@ define(['./workbox-7e5eb42b'], (function (workbox) { 'use strict';
|
||||||
*/
|
*/
|
||||||
workbox.precacheAndRoute([{
|
workbox.precacheAndRoute([{
|
||||||
"url": "/",
|
"url": "/",
|
||||||
"revision": "0.4n2fvpih6f8"
|
"revision": "0.5d3f7lf01og"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/"), {
|
||||||
|
|
|
||||||
13
src/app.html
13
src/app.html
|
|
@ -12,6 +12,19 @@
|
||||||
<meta name="apple-mobile-web-app-title" content="Notebook" />
|
<meta name="apple-mobile-web-app-title" content="Notebook" />
|
||||||
<meta name="format-detection" content="telephone=no" />
|
<meta name="format-detection" content="telephone=no" />
|
||||||
<meta name="theme-color" content="#ffffff" />
|
<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="manifest" href="/manifest.webmanifest" />
|
||||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png" />
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { isAuthenticated, initAuth, logout, currentUser } from "$lib/stores/auth";
|
import { isAuthenticated, initAuth, logout, currentUser } from "$lib/stores/auth";
|
||||||
|
import { theme, toggleTheme } from "$lib/stores/theme";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { page } from "$app/state";
|
import { page } from "$app/state";
|
||||||
|
|
@ -8,6 +9,7 @@
|
||||||
let auth = $state(false);
|
let auth = $state(false);
|
||||||
let admin = $state(false);
|
let admin = $state(false);
|
||||||
let searchQuery = $state("");
|
let searchQuery = $state("");
|
||||||
|
let isDark = $state(false);
|
||||||
|
|
||||||
function submitSearch(e: SubmitEvent) {
|
function submitSearch(e: SubmitEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -22,9 +24,11 @@
|
||||||
initAuth();
|
initAuth();
|
||||||
const unsubAuth = isAuthenticated.subscribe((v) => (auth = v));
|
const unsubAuth = isAuthenticated.subscribe((v) => (auth = v));
|
||||||
const unsubUser = currentUser.subscribe((u) => (admin = u?.role === "admin"));
|
const unsubUser = currentUser.subscribe((u) => (admin = u?.role === "admin"));
|
||||||
|
const unsubTheme = theme.subscribe((t) => (isDark = t === "dark"));
|
||||||
return () => {
|
return () => {
|
||||||
unsubAuth();
|
unsubAuth();
|
||||||
unsubUser();
|
unsubUser();
|
||||||
|
unsubTheme();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -34,7 +38,7 @@
|
||||||
const onAdmin = $derived(path === "/admin" || path.startsWith("/admin/"));
|
const onAdmin = $derived(path === "/admin" || path.startsWith("/admin/"));
|
||||||
</script>
|
</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">
|
<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>
|
<a href="/" class="text-ink font-serif text-[1.65rem] leading-none">Notebook</a>
|
||||||
|
|
||||||
|
|
@ -60,13 +64,50 @@
|
||||||
{:else}
|
{:else}
|
||||||
<a href="/auth/login" class="nav-link">Sign in</a>
|
<a href="/auth/login" class="nav-link">Sign in</a>
|
||||||
{/if}
|
{/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}>
|
<form onsubmit={submitSearch}>
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
aria-label="Search notes and questions"
|
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>
|
</form>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
@ -96,7 +137,7 @@
|
||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
aria-label="Search notes and questions"
|
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>
|
</form>
|
||||||
<a href="/notes" class="nav-link block" onclick={() => (mobileMenuOpen = false)}
|
<a href="/notes" class="nav-link block" onclick={() => (mobileMenuOpen = false)}
|
||||||
|
|
@ -129,6 +170,43 @@
|
||||||
onclick={() => (mobileMenuOpen = false)}>Sign in</a
|
onclick={() => (mobileMenuOpen = false)}>Sign in</a
|
||||||
>
|
>
|
||||||
{/if}
|
{/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>
|
</nav>
|
||||||
{/if}
|
{/if}
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
29
src/lib/stores/theme.ts
Normal file
29
src/lib/stores/theme.ts
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -26,14 +26,14 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="flex min-h-screen flex-col bg-white">
|
<div class="bg-surface flex min-h-screen flex-col">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<main class="flex-1">
|
<main class="flex-1">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</main>
|
</main>
|
||||||
<footer class="border-rule mt-16 border-t">
|
<footer class="border-rule mt-16 border-t">
|
||||||
<div class="mx-auto max-w-4xl px-4 py-8 sm:px-6">
|
<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">
|
<p class="text-faint mt-2 text-xs">
|
||||||
DSEC Notebook is a community resource for Deakin University students. Not affiliated
|
DSEC Notebook is a community resource for Deakin University students. Not affiliated
|
||||||
with Deakin University.
|
with Deakin University.
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,33 @@
|
||||||
--color-muted: #737373;
|
--color-muted: #737373;
|
||||||
--color-faint: #a3a3a3;
|
--color-faint: #a3a3a3;
|
||||||
--color-rule: #e5e5e5;
|
--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 {
|
html {
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
color: var(--color-ink);
|
color: var(--color-ink);
|
||||||
background: #fff;
|
background: var(--color-surface);
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
a, button {
|
a,
|
||||||
cursor: pointer;
|
button {
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
|
|
@ -33,11 +49,11 @@ a, button {
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.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 {
|
.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 {
|
.btn-ghost {
|
||||||
|
|
@ -45,15 +61,15 @@ a, button {
|
||||||
}
|
}
|
||||||
|
|
||||||
.field {
|
.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 {
|
.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 {
|
.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 {
|
.nav-link {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue