add: dynamic themes and streamlined theme creation system

This commit is contained in:
2025-12-14 20:06:36 +01:00
parent 152bd7cdb1
commit 23e19932d2
11 changed files with 96 additions and 102 deletions

View File

@@ -1,15 +1,13 @@
<script lang="ts"> <script lang="ts">
import TopPattern from './svgs/TopPattern.svelte'; import TopPattern from './svgs/TopPattern.svelte';
import BottomPattern from './svgs/BottomPattern.svelte'; import BottomPattern from './svgs/BottomPattern.svelte';
import { getTheme } from '$lib/utils/themes'; import { getTheme, getPatternColor, PATTERN_OPACITY } from '$lib/utils/themes';
const patternOpacity = 0.1;
let { let {
themeName, themeName,
showTop = true, showTop = true,
showBottom = true, showBottom = true,
color = "#FFFFFF" color
}: { }: {
themeName?: string | null; themeName?: string | null;
showTop?: boolean; showTop?: boolean;
@@ -18,21 +16,14 @@
} = $props(); } = $props();
const theme = $derived(getTheme(themeName)); const theme = $derived(getTheme(themeName));
const patternColor = $derived(getPatternColor(color));
</script> </script>
{#if theme.pattern !== 'none'} {#if theme.pattern !== 'none'}
{#if showTop} {#if showTop}
<TopPattern <TopPattern pattern={theme.pattern} color={patternColor} opacity={PATTERN_OPACITY} />
pattern={theme.pattern}
color={color}
opacity={patternOpacity}
/>
{/if} {/if}
{#if showBottom} {#if showBottom}
<BottomPattern <BottomPattern pattern={theme.pattern} color={patternColor} opacity={PATTERN_OPACITY} />
pattern={theme.pattern}
color={color}
opacity={patternOpacity}
/>
{/if} {/if}
{/if} {/if}

View File

@@ -1,8 +1,6 @@
<script lang="ts"> <script lang="ts">
import CardPattern from './svgs/CardPattern.svelte'; import CardPattern from './svgs/CardPattern.svelte';
import { getTheme } from '$lib/utils/themes'; import { getTheme, getPatternColor, PATTERN_OPACITY } from '$lib/utils/themes';
const patternOpacity = 0.1;
let { let {
themeName, themeName,
@@ -13,11 +11,9 @@
} = $props(); } = $props();
const theme = $derived(getTheme(themeName)); const theme = $derived(getTheme(themeName));
const patternColor = $derived(getPatternColor(color));
</script> </script>
{#if theme.pattern !== 'none'} {#if theme.pattern !== 'none'}
<CardPattern <CardPattern pattern={theme.pattern} color={patternColor} opacity={PATTERN_OPACITY} />
pattern={theme.pattern}
opacity={patternOpacity}
/>
{/if} {/if}

View File

@@ -1,38 +1,32 @@
<script lang="ts"> <script lang="ts">
import { asset } from '$app/paths';
let { let {
color = 'currentColor', pattern = 'none',
opacity = 0.1, color = '#000000',
pattern = 'waves' opacity = 0.1
}: { }: {
pattern?: string;
color?: string; color?: string;
opacity?: number; opacity?: number;
pattern?: 'waves' | 'geometric' | 'dots';
} = $props(); } = $props();
const patternPath = $derived(asset(`/themes/${pattern}/bgbottom.svg`));
</script> </script>
<div class="absolute bottom-0 left-0 right-0 pointer-events-none overflow-hidden" style="opacity: {opacity};"> {#if pattern !== 'none'}
{#if pattern === 'waves'} <div
<svg class="w-full h-auto" viewBox="0 0 1440 200" xmlns="http://www.w3.org/2000/svg"> class="absolute bottom-0 left-0 right-0 pointer-events-none overflow-hidden"
<path style="
fill={color} mask-image: url({patternPath});
d="M0,100 C240,50 480,150 720,100 C960,50 1200,150 1440,100 L1440,200 L0,200 Z" -webkit-mask-image: url({patternPath});
/> mask-size: cover;
</svg> -webkit-mask-size: cover;
{:else if pattern === 'geometric'} mask-repeat: repeat;
<svg class="w-full h-auto" viewBox="0 0 1440 200" xmlns="http://www.w3.org/2000/svg"> -webkit-mask-repeat: repeat;
<path background-color: {color};
fill={color} opacity: {opacity};
d="M0,200 L720,50 L1440,200 Z" height: 200px;
/> "
</svg> />
{:else if pattern === 'dots'} {/if}
<svg class="w-full h-auto" viewBox="0 0 1440 200" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots-pattern-bottom" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
<circle cx="20" cy="20" r="3" fill={color} />
</pattern>
</defs>
<rect width="1440" height="200" fill="url(#dots-pattern-bottom)" />
</svg>
{/if}
</div>

View File

@@ -1,14 +1,33 @@
<script lang="ts"> <script lang="ts">
import { asset } from '$app/paths'; import { asset } from '$app/paths';
let { let {
pattern = 'none' pattern = 'none',
color = '#000000',
opacity = 0.1
}: { }: {
pattern?: string; pattern?: string;
color?: string;
opacity?: number;
} = $props(); } = $props();
const patternPath = `/themes/${pattern}/item.svg`;
const patternOpacity = 0.2; const patternPath = $derived(asset(`/themes/${pattern}/item.svg`));
</script> </script>
<div class="absolute bottom-0 right-0 pointer-events-none overflow-hidden rounded-b-lg" style="opacity: {patternOpacity};"> {#if pattern !== 'none'}
<img alt="card svg background pattern" src={asset(patternPath)}> <div
</div> class="absolute bottom-0 right-0 pointer-events-none overflow-hidden rounded-b-lg"
style="
mask-image: url({patternPath});
-webkit-mask-image: url({patternPath});
mask-size: cover;
-webkit-mask-size: cover;
mask-repeat: repeat;
-webkit-mask-repeat: repeat;
background-color: {color};
opacity: {opacity};
width: 200px;
height: 200px;
"
/>
{/if}

View File

@@ -1,45 +1,32 @@
<script lang="ts"> <script lang="ts">
import { asset } from "$app/paths"; import { asset } from '$app/paths';
import { themeStore } from '$lib/stores/theme.svelte';
let { let {
pattern = 'none' pattern = 'none',
color = '#000000',
opacity = 0.1
}: { }: {
pattern?: string; pattern?: string;
color?: string;
opacity?: number;
} = $props(); } = $props();
const patternOpacity = 0.1;
let svgContainer = $state<HTMLDivElement>(); const patternPath = $derived(asset(`/themes/${pattern}/bgtop.svg`));
let svgContent = $state('');
const lightColor = "#000000";
const darkColor = "#FFFFFF";
$effect(() => {
const patternPath = `/themes/${pattern}/bgtop.svg`;
const color = themeStore.getResolvedTheme() === 'dark' ? darkColor : lightColor;
fetch(asset(patternPath))
.then(res => res.text())
.then(svg => {
// Manipulate SVG to change colors
const parser = new DOMParser();
const doc = parser.parseFromString(svg, 'image/svg+xml');
const svgEl = doc.querySelector('svg');
if (svgEl) {
// Change fill colors
svgEl.querySelectorAll('[fill]').forEach(el => {
if (el.getAttribute('fill') !== 'none') {
el.setAttribute('fill', color);
}
});
svgContent = svgEl.outerHTML;
}
});
});
</script> </script>
<div class="absolute top-0 left-0 right-0 pointer-events-none overflow-hidden" bind:this={svgContainer} style="opacity: {patternOpacity};"> {#if pattern !== 'none'}
{@html svgContent} <div
</div> class="absolute top-0 left-0 right-0 pointer-events-none overflow-hidden"
style="
mask-image: url({patternPath});
-webkit-mask-image: url({patternPath});
mask-size: cover;
-webkit-mask-size: cover;
mask-repeat: repeat;
-webkit-mask-repeat: repeat;
background-color: {color};
opacity: {opacity};
height: 200px;
"
/>
{/if}

View File

@@ -12,10 +12,8 @@ class ThemeStore {
this.current = stored || 'system'; this.current = stored || 'system';
this.applyTheme(); this.applyTheme();
// Listen for system theme changes
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', () => { mediaQuery.addEventListener('change', () => {
// Re-apply theme if in system mode
if (this.current === 'system') { if (this.current === 'system') {
this.applyTheme(); this.applyTheme();
} }
@@ -37,6 +35,8 @@ class ThemeStore {
} }
getResolvedTheme(): ResolvedTheme { getResolvedTheme(): ResolvedTheme {
if (!browser) return 'light';
const isDark = this.current === 'dark' || const isDark = this.current === 'dark' ||
(this.current === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); (this.current === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
return isDark ? 'dark' : 'light'; return isDark ? 'dark' : 'light';

View File

@@ -1,6 +1,4 @@
/** import { themeStore } from '$lib/stores/theme.svelte';
* Theme configuration for SVG overlays
*/
export type ThemePattern = 'waves' | 'geometric' | 'dots' | 'none'; export type ThemePattern = 'waves' | 'geometric' | 'dots' | 'none';
@@ -12,23 +10,24 @@ export interface Theme {
export const AVAILABLE_THEMES: Record<string, Theme> = { export const AVAILABLE_THEMES: Record<string, Theme> = {
none: { none: {
name: 'None', name: 'None',
pattern: 'none', pattern: 'none'
}, },
waves: { waves: {
name: 'Waves', name: 'Waves',
pattern: 'waves', pattern: 'waves'
}, },
geometric: { geometric: {
name: 'Geometric', name: 'Geometric',
pattern: 'geometric', pattern: 'geometric'
}, },
dots: { dots: {
name: 'Dots', name: 'Dots',
pattern: 'dots', pattern: 'dots'
} }
}; };
export const DEFAULT_THEME = 'none'; export const DEFAULT_THEME = 'none';
export const PATTERN_OPACITY = 0.1;
export function getTheme(themeName?: string | null): Theme { export function getTheme(themeName?: string | null): Theme {
if (!themeName || !AVAILABLE_THEMES[themeName]) { if (!themeName || !AVAILABLE_THEMES[themeName]) {
@@ -36,3 +35,7 @@ export function getTheme(themeName?: string | null): Theme {
} }
return AVAILABLE_THEMES[themeName]; return AVAILABLE_THEMES[themeName];
} }
export function getPatternColor(customColor?: string): string {
return customColor || (themeStore.getResolvedTheme() === 'dark' ? '#FFFFFF' : '#000000');
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52"><path fill="#000000" d="M0 17.83V0h17.83a3 3 0 0 1-5.66 2H5.9A5 5 0 0 1 2 5.9v6.27a3 3 0 0 1-2 5.66zm0 18.34a3 3 0 0 1 2 5.66v6.27A5 5 0 0 1 5.9 52h6.27a3 3 0 0 1 5.66 0H0V36.17zM36.17 52a3 3 0 0 1 5.66 0h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 0 1 0-5.66V52H36.17zM0 31.93v-9.78a5 5 0 0 1 3.8.72l4.43-4.43a3 3 0 1 1 1.42 1.41L5.2 24.28a5 5 0 0 1 0 5.52l4.44 4.43a3 3 0 1 1-1.42 1.42L3.8 31.2a5 5 0 0 1-3.8.72zm52-14.1a3 3 0 0 1 0-5.66V5.9A5 5 0 0 1 48.1 2h-6.27a3 3 0 0 1-5.66-2H52v17.83zm0 14.1a4.97 4.97 0 0 1-1.72-.72l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1 0-5.52l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43c.53-.35 1.12-.6 1.72-.72v9.78zM22.15 0h9.78a5 5 0 0 1-.72 3.8l4.44 4.43a3 3 0 1 1-1.42 1.42L29.8 5.2a5 5 0 0 1-5.52 0l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1-.72-3.8zm0 52c.13-.6.37-1.19.72-1.72l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43a5 5 0 0 1 5.52 0l4.43-4.43a3 3 0 1 1 1.42 1.41l-4.44 4.43c.36.53.6 1.12.72 1.72h-9.78zm9.75-24a5 5 0 0 1-3.9 3.9v6.27a3 3 0 1 1-2 0V31.9a5 5 0 0 1-3.9-3.9h-6.27a3 3 0 1 1 0-2h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 1 1 2 0v6.27a5 5 0 0 1 3.9 3.9h6.27a3 3 0 1 1 0 2H31.9z"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52"><path fill="#000000" d="M0 17.83V0h17.83a3 3 0 0 1-5.66 2H5.9A5 5 0 0 1 2 5.9v6.27a3 3 0 0 1-2 5.66zm0 18.34a3 3 0 0 1 2 5.66v6.27A5 5 0 0 1 5.9 52h6.27a3 3 0 0 1 5.66 0H0V36.17zM36.17 52a3 3 0 0 1 5.66 0h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 0 1 0-5.66V52H36.17zM0 31.93v-9.78a5 5 0 0 1 3.8.72l4.43-4.43a3 3 0 1 1 1.42 1.41L5.2 24.28a5 5 0 0 1 0 5.52l4.44 4.43a3 3 0 1 1-1.42 1.42L3.8 31.2a5 5 0 0 1-3.8.72zm52-14.1a3 3 0 0 1 0-5.66V5.9A5 5 0 0 1 48.1 2h-6.27a3 3 0 0 1-5.66-2H52v17.83zm0 14.1a4.97 4.97 0 0 1-1.72-.72l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1 0-5.52l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43c.53-.35 1.12-.6 1.72-.72v9.78zM22.15 0h9.78a5 5 0 0 1-.72 3.8l4.44 4.43a3 3 0 1 1-1.42 1.42L29.8 5.2a5 5 0 0 1-5.52 0l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1-.72-3.8zm0 52c.13-.6.37-1.19.72-1.72l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43a5 5 0 0 1 5.52 0l4.43-4.43a3 3 0 1 1 1.42 1.41l-4.44 4.43c.36.53.6 1.12.72 1.72h-9.78zm9.75-24a5 5 0 0 1-3.9 3.9v6.27a3 3 0 1 1-2 0V31.9a5 5 0 0 1-3.9-3.9h-6.27a3 3 0 1 1 0-2h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 1 1 2 0v6.27a5 5 0 0 1 3.9 3.9h6.27a3 3 0 1 1 0 2H31.9z"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52"><path fill="#000000" d="M0 17.83V0h17.83a3 3 0 0 1-5.66 2H5.9A5 5 0 0 1 2 5.9v6.27a3 3 0 0 1-2 5.66zm0 18.34a3 3 0 0 1 2 5.66v6.27A5 5 0 0 1 5.9 52h6.27a3 3 0 0 1 5.66 0H0V36.17zM36.17 52a3 3 0 0 1 5.66 0h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 0 1 0-5.66V52H36.17zM0 31.93v-9.78a5 5 0 0 1 3.8.72l4.43-4.43a3 3 0 1 1 1.42 1.41L5.2 24.28a5 5 0 0 1 0 5.52l4.44 4.43a3 3 0 1 1-1.42 1.42L3.8 31.2a5 5 0 0 1-3.8.72zm52-14.1a3 3 0 0 1 0-5.66V5.9A5 5 0 0 1 48.1 2h-6.27a3 3 0 0 1-5.66-2H52v17.83zm0 14.1a4.97 4.97 0 0 1-1.72-.72l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1 0-5.52l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43c.53-.35 1.12-.6 1.72-.72v9.78zM22.15 0h9.78a5 5 0 0 1-.72 3.8l4.44 4.43a3 3 0 1 1-1.42 1.42L29.8 5.2a5 5 0 0 1-5.52 0l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1-.72-3.8zm0 52c.13-.6.37-1.19.72-1.72l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43a5 5 0 0 1 5.52 0l4.43-4.43a3 3 0 1 1 1.42 1.41l-4.44 4.43c.36.53.6 1.12.72 1.72h-9.78zm9.75-24a5 5 0 0 1-3.9 3.9v6.27a3 3 0 1 1-2 0V31.9a5 5 0 0 1-3.9-3.9h-6.27a3 3 0 1 1 0-2h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 1 1 2 0v6.27a5 5 0 0 1 3.9 3.9h6.27a3 3 0 1 1 0 2H31.9z"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52"><path fill="#000000" d="M0 17.83V0h17.83a3 3 0 0 1-5.66 2H5.9A5 5 0 0 1 2 5.9v6.27a3 3 0 0 1-2 5.66zm0 18.34a3 3 0 0 1 2 5.66v6.27A5 5 0 0 1 5.9 52h6.27a3 3 0 0 1 5.66 0H0V36.17zM36.17 52a3 3 0 0 1 5.66 0h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 0 1 0-5.66V52H36.17zM0 31.93v-9.78a5 5 0 0 1 3.8.72l4.43-4.43a3 3 0 1 1 1.42 1.41L5.2 24.28a5 5 0 0 1 0 5.52l4.44 4.43a3 3 0 1 1-1.42 1.42L3.8 31.2a5 5 0 0 1-3.8.72zm52-14.1a3 3 0 0 1 0-5.66V5.9A5 5 0 0 1 48.1 2h-6.27a3 3 0 0 1-5.66-2H52v17.83zm0 14.1a4.97 4.97 0 0 1-1.72-.72l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1 0-5.52l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43c.53-.35 1.12-.6 1.72-.72v9.78zM22.15 0h9.78a5 5 0 0 1-.72 3.8l4.44 4.43a3 3 0 1 1-1.42 1.42L29.8 5.2a5 5 0 0 1-5.52 0l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1-.72-3.8zm0 52c.13-.6.37-1.19.72-1.72l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43a5 5 0 0 1 5.52 0l4.43-4.43a3 3 0 1 1 1.42 1.41l-4.44 4.43c.36.53.6 1.12.72 1.72h-9.78zm9.75-24a5 5 0 0 1-3.9 3.9v6.27a3 3 0 1 1-2 0V31.9a5 5 0 0 1-3.9-3.9h-6.27a3 3 0 1 1 0-2h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 1 1 2 0v6.27a5 5 0 0 1 3.9 3.9h6.27a3 3 0 1 1 0 2H31.9z"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB