Add theme switch to the header

This commit is contained in:
Rūdolfs Ošiņš
2024-08-28 14:39:51 +02:00
parent c145669236
commit bb68d6c577
8 changed files with 253 additions and 81 deletions
+3
View File
@@ -1,9 +1,12 @@
<script lang="ts">
import { Router, Route } from "svelte-routing";
import { theme } from "@app/components/ThemeSwitch.svelte";
import DesignSystem from "@app/views/DesignSystem.svelte";
import Repos from "@app/views/Repos.svelte";
import Startup from "@app/views/Startup.svelte";
$: document.documentElement.setAttribute("data-theme", $theme);
</script>
<Router>
+12 -2
View File
@@ -1,5 +1,7 @@
<script lang="ts">
export let variant: "primary" | "secondary" | "ghost";
export let stylePadding: string | undefined = undefined;
export let styleHeight: string | undefined = undefined;
$: style = `--button-color-1: var(--color-fill-${variant});`;
</script>
@@ -123,7 +125,13 @@
</style>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="container" on:click role="button" tabindex="0" {style}>
<div
class="container"
on:click
role="button"
tabindex="0"
{style}
style:height={styleHeight}>
<div class="pixel p1-1" />
<div class="pixel p1-2" />
<div class="pixel p1-3" />
@@ -138,7 +146,9 @@
<div class="pixel p3-1" />
<div class="pixel p3-2" />
<div class="pixel p3-3 txt-semibold txt-small"><slot /></div>
<div class="pixel p3-3 txt-semibold txt-small" style:padding={stylePadding}>
<slot />
</div>
<div class="pixel p3-4" />
<div class="pixel p3-5" />
+8 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts">
export let variant: "primary" | "secondary" | "ghost" | "transparent";
export let stylePadding: string | undefined = undefined;
export let styleHeight: string | undefined = undefined;
$: style =
variant === "transparent"
@@ -71,7 +72,13 @@
</style>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="container" on:click role="button" tabindex="0" {style}>
<div
class="container"
on:click
role="button"
tabindex="0"
{style}
style:height={styleHeight}>
<div class="pixel p1-1" />
<div class="pixel p1-2" />
<div class="pixel p1-3" />
+30 -11
View File
@@ -1,7 +1,10 @@
<script lang="ts">
import Background from "./Header/Background.svelte";
import Border from "./Border.svelte";
import Fill from "./Fill.svelte";
import Icon from "./Icon.svelte";
import Label from "./Label.svelte";
import Popover from "./Popover.svelte";
import ThemeSwitch from "./ThemeSwitch.svelte";
</script>
<style>
@@ -11,7 +14,7 @@
}
header {
padding: 0 0.5rem;
gap: 1.5rem;
gap: 0.25rem;
height: 3rem;
}
.wrapper {
@@ -32,20 +35,36 @@
<Icon name="arrow-left" />
<Icon name="arrow-right" />
</div>
<Label
styleBorderColor="var(--color-fill-ghost)"
styleFillColor="var(--color-fill-ghost)">
<Fill variant="ghost" stylePadding="0 0.5rem" styleHeight="32px">
Repositories
</Label>
<Label styleBorderColor="var(--color-fill-ghost)">
</Fill>
<Border variant="ghost" stylePadding="0 0.25rem" styleHeight="32px">
<Icon name="plus" />
</Label>
</Border>
</div>
<div class="flex-item">
<Label styleBorderColor="var(--color-fill-ghost)">
<div class="flex-item" style:gap="0.5rem">
<Border variant="ghost" stylePadding="0 0.5rem" styleHeight="32px">
<Icon name="offline" /> Offline
</Label>
</Border>
<Popover popoverPositionRight="0" popoverPositionTop="3rem">
<Border
slot="toggle"
let:toggle
on:click={toggle}
variant="ghost"
stylePadding="0 0.25rem"
styleHeight="32px">
<Icon name="settings" />
</Border>
<Border variant="ghost" slot="popover" stylePadding="0.5rem 1rem">
<div style="display: flex; gap: 2rem; align-items: center;">
Theme <ThemeSwitch />
</div>
</Border>
</Popover>
</div>
</div>
<Background />
</header>
+65
View File
@@ -12,10 +12,13 @@
| "diff"
| "file"
| "inbox"
| "moon"
| "offline"
| "plus"
| "repo"
| "seedling"
| "settings"
| "sun"
| "warning";
</script>
@@ -127,6 +130,30 @@
<path d="M6 9H10L10 10H6L6 9Z" />
<path d="M3 13H13V14H3L3 13Z" />
<path d="M3 2H13V3H3L3 2Z" />
{:else if name === "moon"}
<path d="M4 3H6V4H4V3Z" />
<path d="M3 4L4 4L4 6H3V4Z" />
<path d="M2 6L3 6L3 10H2V6Z" />
<path d="M3 10H4V12H3L3 10Z" />
<path d="M4 12H6V13H4L4 12Z" />
<path d="M6 13H10V14H6L6 13Z" />
<path d="M6 13H10V14H6L6 13Z" />
<path d="M12 10H13V12H12V10Z" />
<path d="M8 9H10V10H8V9Z" />
<path d="M6 6H7V7H6V6Z" />
<path d="M6 7H7V8H6V7Z" />
<path d="M7 8H8V9H7V8Z" />
<path d="M10 10H12V11H10V10Z" />
<path d="M5 5H6V6H5V5Z" />
<path d="M10 12H12V13H10L10 12Z" />
<path d="M5 4H6L6 5H5L5 4Z" />
<path d="M13 9H14V11H13V9Z" />
<path d="M5 2H7V3H5V2Z" />
<path d="M5 2H7V3H5V2Z" />
<path d="M5 2H7V3H5V2Z" />
<path d="M11 4H12V7H11V4Z" />
<path d="M10 5H13V6H10V5Z" />
<path d="M9 3H10V4H9V3Z" />
{:else if name === "offline"}
<path d="M3 6L3 8H2L2 6H3Z" />
<path d="M13 10V8H14V10H13Z" />
@@ -226,6 +253,44 @@
<path d="M7.33301 8V7L6.33301 7L6.33301 8H7.33301Z" />
<path d="M7.33301 9L7.33301 7L6.33301 7L6.33301 9H7.33301Z" />
<path d="M6.33301 10V9H5.33301L5.33301 10H6.33301Z" />
{:else if name === "settings"}
<path d="M7 5H14V6H7V5Z" />
<path d="M9 11L2 11L2 10L9 10V11Z" />
<path d="M2 5H5V6H2V5Z" />
<path d="M14 11H11V10H14V11Z" />
<path d="M9 8H11V9H9V8Z" />
<path d="M5 3H7V4H5V3Z" />
<path d="M9 12H11V13H9V12Z" />
<path d="M5 7H7V8H5V7Z" />
<path d="M8 9L9 9V12H8V9Z" />
<path d="M4 4L5 4L5 7H4V4Z" />
<path d="M11 9L12 9V12H11V9Z" />
<path d="M7 4L8 4V7L7 7V4Z" />
{:else if name === "sun"}
<path d="M8 2H9V3H8V2Z" />
<path d="M14 8V9H13V8H14Z" />
<path d="M4 7V8H3V7H4Z" />
<path d="M7 12H8V13H7V12Z" />
<path d="M8 3H9L9 4H8L8 3Z" />
<path d="M13 8V9H12V8H13Z" />
<path d="M3 7V8H2L2 7H3Z" />
<path d="M7 13H8V14H7V13Z" />
<path d="M7 5H9V6H7V5Z" />
<path d="M7 10H9V11H7V10Z" />
<path d="M5 7H6V9H5V7Z" />
<path d="M10 7H11V9H10V7Z" />
<path d="M11 5H12V6H11V5Z" />
<path d="M10 12H11V11H10V12Z" />
<path d="M6 4H5V5H6V4Z" />
<path d="M5 11H4V10H5V11Z" />
<path d="M12 4H13V5H12V4Z" />
<path d="M11 13H12V12H11V13Z" />
<path d="M5 3H4V4H5L5 3Z" />
<path d="M4 12H3V11H4L4 12Z" />
<path d="M6 6H7V7L6 7V6Z" />
<path d="M6 9L7 9L7 10H6V9Z" />
<path d="M9 9L10 9L10 10H9L9 9Z" />
<path d="M9 6H10V7H9V6Z" />
{:else if name === "warning"}
<path d="M7 2H9V3H7V2Z" />
<path d="M6 3H7V5H6V3Z" />
-67
View File
@@ -1,67 +0,0 @@
<script lang="ts">
export let styleBorderColor: string;
export let styleFillColor: string = "transparent";
export let emptyPixels: 2 | 1 = 2;
const gridTemplate = `${"2px ".repeat(emptyPixels)}auto ${"2px ".repeat(emptyPixels)}`;
</script>
<style>
.content {
padding: 2px 8px;
display: flex;
gap: 6px;
align-items: center;
}
.container {
white-space: nowrap;
user-select: none;
column-gap: 0;
height: 2rem;
row-gap: 0;
display: grid;
}
</style>
<div
class="container"
style:grid-template-columns={gridTemplate}
style:grid-template-rows={gridTemplate}>
{@html "<div></div>".repeat(emptyPixels)}
<div style:background-color={styleBorderColor} />
{@html "<div></div>".repeat(emptyPixels)}
{#if emptyPixels === 2}
<div />
<div style:background-color={styleBorderColor} />
<div style:background-color={styleFillColor} />
<div style:background-color={styleBorderColor} />
<div />
{/if}
<div style:background-color={styleBorderColor} />
{#if emptyPixels === 2}
<div style:background-color={styleFillColor} />
{/if}
<div
class="content txt-semibold txt-small"
style:background-color={styleFillColor}>
<slot />
</div>
{#if emptyPixels === 2}
<div style:background-color={styleFillColor} />
{/if}
<div style:background-color={styleBorderColor} />
{#if emptyPixels === 2}
<div />
<div style:background-color={styleBorderColor} />
<div style:background-color={styleFillColor} />
<div style:background-color={styleBorderColor} />
<div />
{/if}
{@html "<div></div>".repeat(emptyPixels)}
<div style:background-color={styleBorderColor} />
{@html "<div></div>".repeat(emptyPixels)}
</div>
+70
View File
@@ -0,0 +1,70 @@
<script lang="ts" context="module">
import { writable } from "svelte/store";
const focused = writable<HTMLDivElement | undefined>(undefined);
export function closeFocused() {
focused.set(undefined);
}
</script>
<script lang="ts">
export let popoverContainerMinWidth: string | undefined = undefined;
export let popoverBorderRadius: string | undefined = undefined;
export let popoverPadding: string | undefined = undefined;
export let popoverPositionBottom: string | undefined = undefined;
export let popoverPositionLeft: string | undefined = undefined;
export let popoverPositionRight: string | undefined = undefined;
export let popoverPositionTop: string | undefined = undefined;
export let expanded = false;
let thisComponent: HTMLDivElement;
function clickOutside(ev: MouseEvent | TouchEvent) {
if ($focused && !ev.composedPath().includes($focused)) {
closeFocused();
}
}
function toggle() {
expanded = !expanded;
if ($focused === thisComponent) {
closeFocused();
} else {
focused.set(thisComponent);
}
}
$: expanded = $focused === thisComponent;
</script>
<style>
.container {
position: relative;
}
.popover {
position: absolute;
z-index: 10;
}
</style>
<svelte:window on:click={clickOutside} on:touchstart={clickOutside} />
<div
bind:this={thisComponent}
class="container"
style:min-width={popoverContainerMinWidth}>
<slot name="toggle" {expanded} {toggle} />
{#if expanded}
<div
class="popover"
style:bottom={popoverPositionBottom}
style:left={popoverPositionLeft}
style:right={popoverPositionRight}
style:top={popoverPositionTop}
style:padding={popoverPadding}
style:border-radius={popoverBorderRadius}>
<slot name="popover" {toggle} />
</div>
{/if}
</div>
+65
View File
@@ -0,0 +1,65 @@
<script lang="ts" context="module">
type Theme = "dark" | "light";
export const theme = writable<Theme>(loadTheme());
function loadTheme(): Theme {
const { matches } = window.matchMedia("(prefers-color-scheme: dark)");
const storedTheme = localStorage ? localStorage.getItem("theme") : null;
if (storedTheme === null) {
return matches ? "dark" : "light";
} else {
return storedTheme as Theme;
}
}
export function storeTheme(newTheme: Theme): void {
theme.set(newTheme);
if (localStorage) {
localStorage.setItem("theme", newTheme);
} else {
console.warn(
"localStorage isn't available, not able to persist the selected theme without it.",
);
}
}
</script>
<script lang="ts">
import { writable } from "svelte/store";
import Border from "./Border.svelte";
import Fill from "./Fill.svelte";
import Icon from "./Icon.svelte";
</script>
<div style="display: flex; gap: 1rem;">
<Border styleHeight="32px" variant="secondary">
<Fill
stylePadding="0 0.5rem"
variant={$theme === "dark" ? "secondary" : "transparent"}
on:click={() => {
storeTheme("dark");
}}>
<Icon name="moon" />
Dark
</Fill>
<Fill
stylePadding="0 0.5rem"
variant={$theme === "light" ? "secondary" : "transparent"}
on:click={() => {
storeTheme("light");
}}>
<span
style="display: flex; align-items: center; gap: 0.5rem"
style:color={$theme === "light"
? "var(--color-foreground-white)"
: "var(--color-foreground-contrast)"}>
<Icon name="sun" />
Light
</span>
</Fill>
</Border>
</div>