Skip to content

Instantly share code, notes, and snippets.

@TKasperczyk
Last active January 17, 2025 12:07
Show Gist options
  • Select an option

  • Save TKasperczyk/29f0ccf13a25e105259e05c2b3f49470 to your computer and use it in GitHub Desktop.

Select an option

Save TKasperczyk/29f0ccf13a25e105259e05c2b3f49470 to your computer and use it in GitHub Desktop.
<script lang="ts">
import { Button } from "$shadcn/button";
import { Popover, PopoverContent, PopoverTrigger } from "$shadcn/popover";
import Check from "lucide-svelte/icons/check";
import Palette from "lucide-svelte/icons/palette";
import Moon from "lucide-svelte/icons/moon";
import Sun from "lucide-svelte/icons/sun";
import { onMount } from "svelte";
import { colorThemes } from "$lib/config/theme";
type ColorMode = "base" | "dark";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const themeColors = Object.entries(colorThemes).map(([name, _]) => ({
name: `${name[0].toUpperCase()}${name.slice(1)}`,
value: name
})) as { name: string; value: keyof typeof colorThemes }[];
type Theme = keyof typeof colorThemes;
let currentThemeColor = $state<Theme>("default");
let currentColorMode = $state<ColorMode>("dark");
function setThemeColor(color: Theme) {
currentThemeColor = color;
applyTheme(color, currentColorMode);
}
function toggleDarkMode() {
currentColorMode = currentColorMode === "dark" ? "base" : "dark";
applyTheme(currentThemeColor, currentColorMode);
}
function toCssVariableName(key: string): string {
return `--${key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)}`;
}
function handleLocalStorageError(error: unknown) {
if (error instanceof DOMException && error.name === "QuotaExceededError") {
console.warn("localStorage quota exceeded. Unable to save theme preference.");
} else if (error instanceof DOMException && error.name === "SecurityError") {
console.warn("localStorage access denied due to security settings.");
} else {
console.warn("Failed to access localStorage:", JSON.stringify(error));
}
}
function applyTheme(theme: Theme, colorMode: ColorMode) {
const root = document.documentElement;
const themeColors = {
...colorThemes[theme]["base"],
...(colorMode === "dark" ? colorThemes[theme]["dark"] : {})
};
for (const [key, value] of Object.entries(themeColors)) {
const cssVariableName = toCssVariableName(key);
root.style.setProperty(cssVariableName, value);
}
if (colorMode === "dark") {
root.classList.add("dark");
} else {
root.classList.remove("dark");
}
// Save the theme preference
try {
localStorage.setItem("ATLAS_theme", theme);
localStorage.setItem("ATLAS_darkMode", colorMode);
} catch (error) {
handleLocalStorageError(error);
}
}
onMount(() => {
try {
// Load saved theme preference
const savedTheme = (localStorage.getItem("ATLAS_theme") || "default") as Theme;
const savedColorMode = (localStorage.getItem("ATLAS_darkMode") || "dark") as ColorMode;
currentColorMode = savedColorMode;
currentThemeColor = savedTheme;
applyTheme(savedTheme, savedColorMode);
} catch (error) {
handleLocalStorageError(error);
}
});
</script>
<Popover>
<PopoverTrigger>
{#snippet child({ props })}
<Button variant="outline" size="icon" class="w-10 h-10 rounded-full" type="button" { ...props }>
<Palette class="h-4 w-4" />
<span class="sr-only">Select theme color</span>
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="w-56">
<div class="grid gap-4">
<h4 class="font-medium leading-none">Choose theme color</h4>
<div class="grid gap-2">
{#each themeColors as color}
<Button
variant="ghost"
class="w-full justify-start"
type="button"
onclick={() => setThemeColor(color.value)}
aria-pressed={currentThemeColor === color.value}
>
<div
class="w-4 h-4 rounded-full mr-2"
style="background-color: hsl({colorThemes[color.value][currentColorMode].primary})"
></div>
{color.name}
<div
class="mr-2 h-4 w-4 opacity-0 transition-opacity ml-auto"
class:opacity-100={currentThemeColor === color.value}
>
<Check class="h-4 w-4" />
</div>
</Button>
{/each}
<Button
variant="ghost"
onclick={toggleDarkMode}
class="w-full justify-start"
type="button"
>
{#if currentColorMode === "dark"}
<Sun class="h-4 w-4 mr-2" />
Light Mode
{:else}
<Moon class="h-4 w-4 mr-2" />
Dark Mode
{/if}
</Button>
</div>
</div>
</PopoverContent>
</Popover>
const commonBase = {
error: "0 84% 60%",
warning: "40 100% 45%",
success: "142 76% 36%",
// ...
};
const commonDark = {
// ...
};
const defaultBaseTheme = {
...commonBase,
background: "0 0% 100%",
foreground: "20 14.3% 4.1%",
card: "24.6 40% 95%",
// ...
};
const defaultDarkTheme = {
...commonDark,
background: "20 14.3% 4.1%",
// ...
};
function createTheme(
baseOverrides: Partial<typeof defaultBaseTheme>,
darkOverrides: Partial<typeof defaultDarkTheme>
) {
return {
base: {
...defaultBaseTheme,
...baseOverrides
},
dark: {
...defaultDarkTheme,
...darkOverrides
}
};
}
export const colorThemes = {
default: createTheme({}, {}),
magenta: createTheme(
{
primary: "346.8 77.2% 49.8%",
ring: "346.8 77.2% 49.8%",
card: "346.8 20% 95%"
},
{
primary: "346.8 77.2% 49.8%",
card: "346.8 10% 10%",
ring: "346.8 77.2% 49.8%"
}
),
green: createTheme(
{
primary: "142.1 76.2% 36.3%",
ring: "142.1 76.2% 36.3%",
card: "142.4 20% 95%"
},
{
primary: "142.1 70.6% 45.3%",
card: "142.4 5% 10%",
ring: "142.4 71.8% 29.2%"
}
),
// ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment