add theme patterns
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user