add theme patterns

This commit is contained in:
JCEEE
2026-09-10 13:23:55 +01:00
parent 27492b1558
commit a270d965fe
13 changed files with 1429 additions and 27 deletions
@@ -0,0 +1,184 @@
<script lang="ts">
import '../theme-patterns.css';
import {
themeShades,
PATTERN_COUNT,
patternId,
THEME_SIZE_MIN,
THEME_SIZE_MAX,
THEME_SIZE_DEFAULT,
THEME_OPACITY_MIN,
THEME_OPACITY_MAX,
THEME_OPACITY_STEP,
THEME_OPACITY_DEFAULT
} from '$lib/theme';
let {
value = '',
color = '#6366f1',
size = THEME_SIZE_DEFAULT,
opacity = THEME_OPACITY_DEFAULT,
onchange,
onsize,
onopacity
}: {
value?: string;
color?: string;
size?: number;
opacity?: number;
onchange: (v: string) => void;
onsize?: (v: number) => void;
onopacity?: (v: number) => void;
} = $props();
let shades = $derived(themeShades(color));
// Inline vars beat the pattern class defaults; --s shrinks the pattern so
// thumbnails show a sample of the tiling.
let vars = $derived(
`--c1:${shades.c1};--c2:${shades.c2};--c3:${shades.c3};--c4:${shades.c4};--s:26px`
);
// Local slider state — bound directly to the inputs (no controlled-value
// re-render mid-drag), then emitted on input for the live preview.
let sizeVal = $state(size);
let opacityVal = $state(opacity);
// Keep local state in sync if the props ever change (e.g. after a save).
$effect(() => {
sizeVal = size;
});
$effect(() => {
opacityVal = opacity;
});
</script>
<div class="pattern-grid">
<button
type="button"
class="pattern-tile none"
class:selected={value === ''}
title="No background pattern"
onclick={() => onchange('')}
>
<span class="none-dot" style="background:{color}"></span>
</button>
{#each Array(PATTERN_COUNT) as _, i}
{@const id = patternId(i + 1)}
<button
type="button"
class="pattern-tile pattern-{id}"
class:selected={value === id}
title={`Pattern ${i + 1}`}
style={vars}
onclick={() => onchange(id)}
></button>
{/each}
</div>
{#if value !== ''}
{#if onsize}
<div class="slider-row">
<div class="slider-head">
<label for="theme-size">Pattern size</label>
<span class="slider-val">{sizeVal}</span>
</div>
<input
id="theme-size"
type="range"
min={THEME_SIZE_MIN}
max={THEME_SIZE_MAX}
step="0.5"
bind:value={sizeVal}
oninput={() => onsize?.(sizeVal)}
/>
<span class="slider-hint">scales with screen width</span>
</div>
{/if}
{#if onopacity}
<div class="slider-row">
<div class="slider-head">
<label for="theme-opacity">Opacity</label>
<span class="slider-val">{Math.round(opacityVal * 100)}%</span>
</div>
<input
id="theme-opacity"
type="range"
min={THEME_OPACITY_MIN}
max={THEME_OPACITY_MAX}
step={THEME_OPACITY_STEP}
bind:value={opacityVal}
oninput={() => onopacity?.(opacityVal)}
/>
<span class="slider-hint">max = current look</span>
</div>
{/if}
{/if}
<style>
.pattern-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
gap: 0.5rem;
margin-bottom: 0.5rem;
/* Static grid — currently 19 tiles fit without scrolling. Padding keeps
the selected tile's outline from clipping at the top-left corner. */
padding: 0.4rem;
}
.pattern-tile {
aspect-ratio: 1;
border-radius: 8px;
border: 1px solid #e5e7eb;
cursor: pointer;
padding: 0;
/* No background here — the imported .pattern-pN classes supply it.
A local background (0,2,0) would beat .pattern-pN (0,1,0) and hide
every preview. */
transition: outline 0.12s ease;
}
.pattern-tile:hover {
border-color: #9ca3af;
}
.pattern-tile.selected {
outline: 3px solid #111827;
outline-offset: 2px;
}
.pattern-tile.none {
display: flex;
align-items: center;
justify-content: center;
background: #fff;
}
.none-dot {
width: 14px;
height: 14px;
border-radius: 50%;
}
.slider-row {
margin-top: 0.75rem;
font-size: 0.8rem;
color: #374151;
}
.slider-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.3rem;
}
.slider-head label {
font-weight: 600;
}
.slider-row input[type='range'] {
width: 100%;
display: block;
accent-color: #6366f1;
}
.slider-val {
font-weight: 700;
color: #4338ca;
}
.slider-hint {
color: #9ca3af;
font-size: 0.72rem;
display: block;
margin-top: 0.15rem;
}
</style>