wip: making themes more dynamic and easy to add

This commit is contained in:
rasmusq
2025-11-30 01:30:43 +01:00
parent d165e5992a
commit 152bd7cdb1
12 changed files with 76 additions and 98 deletions

View File

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

View File

@@ -1,23 +1,23 @@
<script lang="ts">
import CardPattern from './svgs/CardPattern.svelte';
import { getTheme, getThemeColor } from '$lib/utils/themes';
import { getTheme } from '$lib/utils/themes';
const patternOpacity = 0.1;
let {
themeName,
color
}: {
themeName?: string | null;
color?: string | null;
themeName?: string;
color?: string;
} = $props();
const theme = $derived(getTheme(themeName));
const themeColor = $derived(getThemeColor(color));
</script>
{#if theme.pattern !== 'none'}
<CardPattern
pattern={theme.pattern}
color={themeColor}
opacity={theme.opacity}
opacity={patternOpacity}
/>
{/if}

View File

@@ -1,38 +1,14 @@
<script lang="ts">
import { asset } from '$app/paths';
let {
color = 'currentColor',
opacity = 0.08,
pattern = 'waves'
pattern = 'none'
}: {
color?: string;
opacity?: number;
pattern?: 'waves' | 'geometric' | 'dots';
pattern?: string;
} = $props();
const patternPath = `/themes/${pattern}/item.svg`;
const patternOpacity = 0.2;
</script>
<div class="absolute bottom-0 left-0 right-0 pointer-events-none overflow-hidden rounded-b-lg" style="opacity: {opacity};">
{#if pattern === 'waves'}
<svg class="w-full h-auto" viewBox="0 0 400 80" xmlns="http://www.w3.org/2000/svg">
<path
fill={color}
d="M0,40 C100,20 200,60 300,40 C350,30 375,35 400,40 L400,80 L0,80 Z"
/>
</svg>
{:else if pattern === 'geometric'}
<svg class="w-full h-auto" viewBox="0 0 400 80" xmlns="http://www.w3.org/2000/svg">
<path
fill={color}
d="M0,80 L200,20 L400,80 Z"
/>
</svg>
{:else if pattern === 'dots'}
<svg class="w-full h-auto" viewBox="0 0 400 80" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots-pattern-card" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
<circle cx="10" cy="10" r="2" fill={color} />
</pattern>
</defs>
<rect width="400" height="80" fill="url(#dots-pattern-card)" />
</svg>
{/if}
<div class="absolute bottom-0 right-0 pointer-events-none overflow-hidden rounded-b-lg" style="opacity: {patternOpacity};">
<img alt="card svg background pattern" src={asset(patternPath)}>
</div>

View File

@@ -1,38 +1,45 @@
<script lang="ts">
import { asset } from "$app/paths";
import { themeStore } from '$lib/stores/theme.svelte';
let {
color = 'currentColor',
opacity = 0.1,
pattern = 'waves'
pattern = 'none'
}: {
color?: string;
opacity?: number;
pattern?: 'waves' | 'geometric' | 'dots';
pattern?: string;
} = $props();
const patternOpacity = 0.1;
let svgContainer = $state<HTMLDivElement>();
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>
<div class="absolute top-0 left-0 right-0 pointer-events-none overflow-hidden" style="opacity: {opacity};">
{#if pattern === 'waves'}
<svg class="w-full h-auto" viewBox="0 0 1440 200" xmlns="http://www.w3.org/2000/svg">
<path
fill={color}
d="M0,100 C240,150 480,50 720,100 C960,150 1200,50 1440,100 L1440,0 L0,0 Z"
/>
</svg>
{:else if pattern === 'geometric'}
<svg class="w-full h-auto" viewBox="0 0 1440 200" xmlns="http://www.w3.org/2000/svg">
<path
fill={color}
d="M0,0 L720,150 L1440,0 Z"
/>
</svg>
{:else if pattern === 'dots'}
<svg class="w-full h-auto" viewBox="0 0 1440 200" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots-pattern-top" 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-top)" />
</svg>
{/if}
<div class="absolute top-0 left-0 right-0 pointer-events-none overflow-hidden" bind:this={svgContainer} style="opacity: {patternOpacity};">
{@html svgContent}
</div>

View File

@@ -106,7 +106,7 @@
editingTitle = true;
}
}}
class="flex-shrink-0 w-8 h-8 flex items-center justify-center rounded-full border border-input hover:bg-accent transition-colors"
class="shrink-0 w-8 h-8 flex items-center justify-center rounded-full border border-input hover:bg-accent transition-colors"
aria-label={editingTitle ? "Save title" : "Edit title"}
>
{#if editingTitle}
@@ -116,12 +116,12 @@
{/if}
</button>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<div class="flex items-center gap-2 shrink-0">
<ThemePicker
value={wishlistTheme}
onValueChange={async (theme) => {
wishlistTheme = theme;
await onThemeUpdate(theme);
onThemeUpdate(theme);
// Force reactivity by updating the wishlist object
wishlist.theme = theme;
}}

View File

@@ -1,6 +1,7 @@
import { browser } from '$app/environment';
type Theme = 'light' | 'dark' | 'system';
type ResolvedTheme = 'light' | 'dark';
class ThemeStore {
current = $state<Theme>('system');
@@ -35,6 +36,12 @@ class ThemeStore {
}
}
getResolvedTheme(): ResolvedTheme {
const isDark = this.current === 'dark' ||
(this.current === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
return isDark ? 'dark' : 'light';
}
toggle() {
// Cycle through: light -> dark -> system -> light
if (this.current === 'light') {

View File

@@ -7,29 +7,24 @@ export type ThemePattern = 'waves' | 'geometric' | 'dots' | 'none';
export interface Theme {
name: string;
pattern: ThemePattern;
opacity: number;
}
export const AVAILABLE_THEMES: Record<string, Theme> = {
none: {
name: 'None',
pattern: 'none',
opacity: 0
},
waves: {
name: 'Waves',
pattern: 'waves',
opacity: 0.1
},
geometric: {
name: 'Geometric',
pattern: 'geometric',
opacity: 0.1
},
dots: {
name: 'Dots',
pattern: 'dots',
opacity: 0.15
}
};
@@ -41,16 +36,3 @@ export function getTheme(themeName?: string | null): Theme {
}
return AVAILABLE_THEMES[themeName];
}
/**
* Get color from a CSS color string or class
* Returns the provided color or a default primary color
*/
export function getThemeColor(color?: string | null): string {
// Use the provided color, or default to a visible color
if (color && color !== 'null' && color !== '') {
return color;
}
// Default to a blue color (can't use CSS variables in SVG fill)
return '#3b82f6';
}