185 lines
4.1 KiB
Svelte
185 lines
4.1 KiB
Svelte
<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>
|