Add navbar and use localStorage for theme
This commit is contained in:
parent
972b7b983f
commit
18a41b3b3f
|
@ -2,3 +2,7 @@
|
|||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
**/node_modules
|
||||
**/.svelte-kit
|
||||
**/.vercel
|
||||
.prettierignore
|
17
src/app.html
17
src/app.html
|
@ -1,12 +1,27 @@
|
|||
<!doctype html>
|
||||
<html lang="en" data-theme="%THEME%">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<script>
|
||||
const themeValue = localStorage.getItem("theme")
|
||||
const systemPreferredTheme = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)"
|
||||
).matches
|
||||
? "dark"
|
||||
: "light"
|
||||
|
||||
document.documentElement.setAttribute(
|
||||
"data-theme",
|
||||
themeValue ?? systemPreferredTheme
|
||||
)
|
||||
</script>
|
||||
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import type { Handle } from "@sveltejs/kit"
|
||||
|
||||
export type Theme = "light" | "dark" | "auto"
|
||||
// export type Theme = "light" | "dark" | "auto"
|
||||
export type Theme = "light" | "dark"
|
||||
|
||||
export const isValidTheme = (
|
||||
theme: FormDataEntryValue | null
|
||||
): theme is Theme =>
|
||||
!!theme && (theme === "light" || theme === "dark" || theme === "auto")
|
||||
): theme is Theme => !!theme && (theme === "light" || theme === "dark")
|
||||
// !!theme && (theme === "light" || theme === "dark" || theme === "auto")
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
const theme = event.cookies.get("theme") ?? "auto"
|
||||
// const theme = event.cookies.get("theme") ?? "auto"
|
||||
const theme = event.cookies.get("theme") ?? "dark"
|
||||
if (isValidTheme(theme)) {
|
||||
event.locals.theme = theme
|
||||
}
|
||||
|
@ -18,7 +20,8 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||
})
|
||||
|
||||
const response = await resolve(event, {
|
||||
transformPageChunk: ({ html }) => html.replace("%THEME%", theme),
|
||||
// transformPageChunk: ({ html }) => html.replace("%THEME%", theme === "auto" ? "dark" : theme),
|
||||
// transformPageChunk: ({ html }) => html.replace("%THEME%", theme),
|
||||
})
|
||||
|
||||
return response
|
||||
|
|
|
@ -1,19 +1,240 @@
|
|||
<script lang="ts">
|
||||
import { applyAction, enhance } from "$app/forms"
|
||||
import ThemePicker from "./ThemePicker.svelte"
|
||||
import type { Theme } from "../../hooks.server"
|
||||
|
||||
let { theme } = $props<{ theme: Theme }>()
|
||||
|
||||
let links = [
|
||||
{
|
||||
title: "Timer",
|
||||
prefix: "timer",
|
||||
pathname: "/timer",
|
||||
},
|
||||
{
|
||||
title: "About",
|
||||
prefix: "about",
|
||||
pathname: "/about",
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<header class="flex items-center justify-between px-2 py-4">
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="px-4 text-xl font-semibold">BreakOften</a>
|
||||
<nav>
|
||||
<a class="home-link" href="/" title={"home"}>
|
||||
<img src="./favicon.png" alt="Home" />
|
||||
</a>
|
||||
|
||||
<div class="desktop">
|
||||
<div class="center-area" />
|
||||
|
||||
<div class="menu">
|
||||
{#each links as link}
|
||||
<a href={link.pathname}>
|
||||
{link.title}
|
||||
</a>
|
||||
{/each}
|
||||
|
||||
<slot name="external-links" />
|
||||
|
||||
<div class="appearance">
|
||||
<ThemePicker bind:theme />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<nav class="flex items-center gap-4">
|
||||
<button class="py-2 px-4"> Login </button>
|
||||
<style lang="postcss">
|
||||
nav {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
width: 100vw;
|
||||
margin: 0 auto;
|
||||
@apply bg-backgroundLight;
|
||||
font-family: var(--sk-font);
|
||||
user-select: none;
|
||||
transition: 0.4s var(--quint-out);
|
||||
transition-property: transform, background;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
<ThemePicker bind:theme />
|
||||
</nav>
|
||||
</header>
|
||||
nav::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -4px;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.05), transparent);
|
||||
}
|
||||
|
||||
.current-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.8em;
|
||||
margin-left: 0.4em;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
nav:not(.visible):not(:focus-within) {
|
||||
transform: translate(0, calc(var(--sk-nav-height)));
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menu :global(a) {
|
||||
line-height: 1;
|
||||
margin: 0 0.3em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.menu :global(a[aria-current="page"]) {
|
||||
color: var(--sk-theme-1);
|
||||
box-shadow: inset 0 -1px 0 0 var(--sk-theme-1);
|
||||
}
|
||||
|
||||
.menu :global(a[aria-current="page"]:hover) {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.home-link {
|
||||
max-width: max-content;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 1rem;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-size: 1.8rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.home-link img {
|
||||
width: 50px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.home-small {
|
||||
display: none;
|
||||
margin-left: -0.75rem;
|
||||
}
|
||||
|
||||
.home-large {
|
||||
display: block;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.home-link :global(strong) {
|
||||
color: white;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
.mobile-menu {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.desktop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
nav :global(.small) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
padding: 0 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.search {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.appearance {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 0.75rem;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
.appearance .caption {
|
||||
display: none;
|
||||
font-size: var(--sk-text-xs);
|
||||
line-height: 1;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
nav {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr 1fr;
|
||||
}
|
||||
|
||||
nav::after {
|
||||
top: auto;
|
||||
bottom: -4px;
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.05), transparent);
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: flex;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
padding: 0 var(--sk-page-padding-side) 0 0;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.desktop {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
nav :global(.small) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1240px) {
|
||||
nav {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
-o-object-fit: contain;
|
||||
object-fit: contain;
|
||||
-webkit-transform-origin: center center;
|
||||
transform-origin: center center;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
fill: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,38 +1,49 @@
|
|||
<script lang="ts" context="module">
|
||||
export const themes = ["light", "dark", "auto"] as const
|
||||
// export const themes = ["light", "dark", "auto"] as const
|
||||
export const themes = ["light", "dark"] as const
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { slide } from "svelte/transition"
|
||||
import type { Theme } from "../../hooks.server"
|
||||
import { applyAction, enhance } from "$app/forms"
|
||||
// import { applyAction, enhance } from "$app/forms"
|
||||
import { browser } from "$app/environment"
|
||||
|
||||
let { theme } = $props<{ theme: Theme }>()
|
||||
|
||||
// theme = theme || (browser && localStorage.getItem("theme")) as Theme || "dark"
|
||||
theme =
|
||||
(browser && (document.documentElement.dataset.theme as Theme)) || "dark"
|
||||
|
||||
function toggleTheme() {
|
||||
const currentIndex = themes.indexOf(theme)
|
||||
theme = themes[(currentIndex + 1) % themes.length]
|
||||
if (theme == "auto") {
|
||||
theme =
|
||||
browser && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "light"
|
||||
: "dark"
|
||||
if (browser) {
|
||||
localStorage.setItem("theme", theme)
|
||||
document.documentElement.dataset.theme = theme
|
||||
}
|
||||
// if (theme == "auto") {
|
||||
// theme =
|
||||
// browser && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
// ? "light"
|
||||
// : "dark"
|
||||
// }
|
||||
}
|
||||
|
||||
let icon = $derived.by(() => {
|
||||
if (theme === "light") return "🌞"
|
||||
if (theme === "dark") return "🌙"
|
||||
if (theme === "auto")
|
||||
return browser &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "🌙"
|
||||
: "🌞"
|
||||
// if (theme === "light") return "🌙"
|
||||
// if (theme === "dark") return "🌞"
|
||||
if (theme === "light") return "light"
|
||||
if (theme === "dark") return "dark"
|
||||
// if (theme === "auto")
|
||||
// return browser &&
|
||||
// window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
// ? "🌙"
|
||||
// : "🌞"
|
||||
})
|
||||
</script>
|
||||
|
||||
<form
|
||||
<!-- <form
|
||||
method="POST"
|
||||
action="/?/theme"
|
||||
use:enhance={async () => {
|
||||
|
@ -40,11 +51,84 @@
|
|||
await applyAction(result)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input name="theme" value={theme} hidden />
|
||||
{#key theme}
|
||||
> -->
|
||||
<!-- <input name="theme" value={theme} hidden /> -->
|
||||
<!-- {#key theme}
|
||||
<button transition:slide={{ axis: "x" }} onclick={toggleTheme}>
|
||||
{icon}
|
||||
</button>
|
||||
{/key}
|
||||
</form>
|
||||
{/key} -->
|
||||
<!-- </form> -->
|
||||
|
||||
{#key theme}
|
||||
<button
|
||||
on:click={toggleTheme}
|
||||
type="button"
|
||||
aria-pressed={icon === "dark" ? "true" : "false"}
|
||||
>
|
||||
{#if browser}
|
||||
<span class="check" class:checked={icon === "dark"}>
|
||||
<span class="icon">
|
||||
{#if icon === "dark"}
|
||||
{@html `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 21q-3.775 0-6.388-2.613T3 12q0-3.45 2.25-5.988T11 3.05q.625-.075.975.45t-.025 1.1q-.425.65-.638 1.375T11.1 7.5q0 2.25 1.575 3.825T16.5 12.9q.775 0 1.538-.225t1.362-.625q.525-.35 1.075-.037t.475.987q-.35 3.45-2.937 5.725T12 21Zm0-2q2.2 0 3.95-1.213t2.55-3.162q-.5.125-1 .2t-1 .075q-3.075 0-5.238-2.163T9.1 7.5q0-.5.075-1t.2-1q-1.95.8-3.163 2.55T5 12q0 2.9 2.05 4.95T12 19Zm-.25-6.75Z"/></svg>`}
|
||||
{:else}
|
||||
{@html `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M0 0h24v24H0z"/><path fill="currentColor" d="M12 19a1 1 0 0 1 .993.883L13 20v1a1 1 0 0 1-1.993.117L11 21v-1a1 1 0 0 1 1-1zm6.313-2.09l.094.083l.7.7a1 1 0 0 1-1.32 1.497l-.094-.083l-.7-.7a1 1 0 0 1 1.218-1.567l.102.07zm-11.306.083a1 1 0 0 1 .083 1.32l-.083.094l-.7.7a1 1 0 0 1-1.497-1.32l.083-.094l.7-.7a1 1 0 0 1 1.414 0zM4 11a1 1 0 0 1 .117 1.993L4 13H3a1 1 0 0 1-.117-1.993L3 11h1zm17 0a1 1 0 0 1 .117 1.993L21 13h-1a1 1 0 0 1-.117-1.993L20 11h1zM6.213 4.81l.094.083l.7.7a1 1 0 0 1-1.32 1.497l-.094-.083l-.7-.7A1 1 0 0 1 6.11 4.74l.102.07zm12.894.083a1 1 0 0 1 .083 1.32l-.083.094l-.7.7a1 1 0 0 1-1.497-1.32l.083-.094l.7-.7a1 1 0 0 1 1.414 0zM12 2a1 1 0 0 1 .993.883L13 3v1a1 1 0 0 1-1.993.117L11 4V3a1 1 0 0 1 1-1zm0 5a5 5 0 1 1-4.995 5.217L7 12l.005-.217A5 5 0 0 1 12 7z"/></g></svg>`}
|
||||
{/if}
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/key}
|
||||
|
||||
<style lang="postcss">
|
||||
button {
|
||||
position: relative;
|
||||
border-radius: 11px;
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
@apply border border-solid border-foreground border-opacity-30;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
@apply text-foreground border-foreground;
|
||||
}
|
||||
|
||||
.check {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
@apply bg-background text-foreground;
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, 0.04),
|
||||
0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
transition: transform 0.25s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.check.checked {
|
||||
transform: translate(18px);
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.icon :global(svg) {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
|
||||
let { data } = $props()
|
||||
|
||||
let theme = $state(data.theme as Theme)
|
||||
let theme = $state("" as Theme)
|
||||
|
||||
$effect(() => {
|
||||
browser && (document.documentElement.dataset.theme = theme)
|
||||
// browser && (document.documentElement.dataset.theme = theme)
|
||||
// browser && (document.documentElement.dataset.theme = theme === "auto" ? "dark" : theme)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
<main>
|
||||
<div class="container">
|
||||
<h1>About</h1>
|
||||
<p>
|
||||
BreakOften is a web application designed to help users prevent computer
|
||||
vision syndrome (CVS) by reminding them to take regular breaks from their
|
||||
screens. The app encourages users to take short breaks every 20 minutes
|
||||
and 20 seconds, followed by a longer break after every three short breaks.
|
||||
</p>
|
||||
<p>
|
||||
Computer vision syndrome is a common problem caused by prolonged use of
|
||||
digital screens, leading to symptoms such as eye strain, headaches, and
|
||||
neck pain. BreakOften aims to mitigate these symptoms by promoting regular
|
||||
breaks and encouraging users to engage in eye exercises and relaxation
|
||||
techniques during their breaks.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
margin-top: 6rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
Binary file not shown.
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.1 MiB |
|
@ -1,40 +1,12 @@
|
|||
import adapter from "@sveltejs/adapter-auto"
|
||||
// import adapterGhpages from "svelte-adapter-ghpages";
|
||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// vitePlugin: {
|
||||
// dynamicCompileOptions({filename}){
|
||||
// if(filename.includes('node_modules')){
|
||||
// return {runes: undefined} // or false, check what works
|
||||
// }
|
||||
// },
|
||||
// },
|
||||
|
||||
// compilerOptions: {
|
||||
// runes: true
|
||||
// },
|
||||
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
// Use different adapters based on the environment.
|
||||
// For GitHub Pages deployment, use svelte-adapter-ghpages.
|
||||
// For local development, you can use a different adapter like @sveltejs/adapter-static or any other suitable one.
|
||||
adapter: adapter(),
|
||||
// adapter: process.env.NODE_ENV === 'production' ? adapterGhpages({
|
||||
// pages: 'build',
|
||||
// assets: 'build',
|
||||
// fallback: null
|
||||
// }) : adapter(),
|
||||
// prerender: {
|
||||
// entries: []
|
||||
// },
|
||||
// paths: {
|
||||
// base: process.env.NODE_ENV === 'production' ? "/BreakOften" : "",
|
||||
// },
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -11,12 +11,14 @@ export default {
|
|||
createThemes({
|
||||
light: {
|
||||
background: colors.slate[50],
|
||||
backgroundLight: colors.slate[100],
|
||||
foreground: colors.slate[950],
|
||||
primary: colors.blue[600],
|
||||
primaryLight: colors.blue[800],
|
||||
},
|
||||
dark: {
|
||||
background: colors.slate[950],
|
||||
backgroundLight: colors.slate[700],
|
||||
foreground: colors.slate[50],
|
||||
primary: colors.pink[600],
|
||||
primaryLight: colors.pink[800],
|
||||
|
|
|
@ -11,9 +11,4 @@
|
|||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue