feat: add multiple colors field

This commit is contained in:
2026-09-07 10:15:49 +05:00
parent cc70750727
commit 6be9d10c42
3 changed files with 181 additions and 0 deletions
@@ -10,6 +10,7 @@
import CheckboxControl from "./fields/schema/CheckboxControl.svelte"; import CheckboxControl from "./fields/schema/CheckboxControl.svelte";
import ColorControl from "./fields/schema/ColorControl.svelte"; import ColorControl from "./fields/schema/ColorControl.svelte";
import ColorPairControl from "./fields/schema/ColorPairControl.svelte"; import ColorPairControl from "./fields/schema/ColorPairControl.svelte";
import ColorsControl from "./fields/schema/ColorsControl.svelte";
import RangeControl from "./fields/schema/RangeControl.svelte"; import RangeControl from "./fields/schema/RangeControl.svelte";
import SelectControl from "./fields/schema/SelectControl.svelte"; import SelectControl from "./fields/schema/SelectControl.svelte";
import TextControl from "./fields/schema/TextControl.svelte"; import TextControl from "./fields/schema/TextControl.svelte";
@@ -31,6 +32,7 @@
slider: RangeControl, slider: RangeControl,
color: ColorControl, color: ColorControl,
"color-pair": ColorPairControl, "color-pair": ColorPairControl,
colors: ColorsControl,
select: SelectControl, select: SelectControl,
checkbox: CheckboxControl, checkbox: CheckboxControl,
text: TextControl, text: TextControl,
@@ -0,0 +1,156 @@
<script lang="ts">
import type {
ColorList,
ColorListSpec,
FieldSpec,
} from "$lib/registry-schema";
interface Props {
label: string;
value: unknown;
spec: FieldSpec;
onchange?: (value: ColorList) => void;
}
let { label, value, spec, onchange }: Props = $props();
const sp = $derived(spec as ColorListSpec);
const current = $derived.by((): ColorList => {
if (Array.isArray(value)) {
const valid = value.filter(
(c): c is string => typeof c === "string" && /^#[0-9a-f]{6}$/i.test(c),
);
if (valid.length > 0) return valid;
}
return [...sp.default];
});
function setColor(index: number, hex: string) {
const next = [...current];
next[index] = hex;
onchange?.(next);
}
function addColor() {
onchange?.([...current, "#000000"]);
}
function removeColor(index: number) {
if (current.length <= 1) return;
onchange?.(current.filter((_, i) => i !== index));
}
</script>
<div class="control colors-control">
<span class="ctrl-label">{label}</span>
<div class="chips">
{#each current as hex, i (i)}
<div class="chip">
<label class="swatch-wrap">
<span class="swatch" style="background:{hex}"></span>
<input
type="color"
value={hex}
oninput={(e) => setColor(i, (e.target as HTMLInputElement).value)}
/>
</label>
<span class="hex">{hex}</span>
<button
type="button"
class="remove"
onclick={() => removeColor(i)}
disabled={current.length <= 1}
aria-label="Remove color"
>
×
</button>
</div>
{/each}
</div>
<button type="button" class="add" onclick={addColor}>+ Add color</button>
</div>
<style>
.control {
display: grid;
gap: var(--space-m);
margin-bottom: var(--space-xl);
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
letter-spacing: var(--space-text-m);
}
.ctrl-label {
color: var(--color-text);
font-size: var(--font-size-s);
letter-spacing: var(--space-text-l);
text-transform: uppercase;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: var(--space-m);
}
.chip {
display: grid;
grid-template-columns: var(--space-xxl) minmax(0, auto) auto;
align-items: center;
gap: var(--space-m);
padding: var(--space-s) var(--space-m);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-m);
}
.swatch-wrap {
display: grid;
position: relative;
}
.swatch {
width: var(--space-xxl);
height: var(--space-xxl);
border-radius: var(--radius-s);
border: var(--size-border) solid var(--color-border);
}
.swatch-wrap input[type="color"] {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
}
.hex {
color: var(--color-text-muted);
font-size: var(--font-size-s);
}
.remove {
display: flex;
align-items: center;
justify-content: center;
width: var(--space-xl);
height: var(--space-xl);
border: none;
border-radius: var(--radius-s);
background: none;
color: var(--color-text-muted);
font-size: var(--font-size-s);
cursor: pointer;
}
.remove:hover:not(:disabled) {
color: var(--color-text);
background: var(--color-background-muted);
}
.remove:disabled {
opacity: 0.4;
cursor: default;
}
.add {
justify-self: start;
padding: var(--space-s) var(--space-m);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-m);
background: none;
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
cursor: pointer;
}
.add:hover {
color: var(--color-text);
border-color: var(--color-text-muted);
}
</style>
+23
View File
@@ -79,6 +79,14 @@ export interface ColorPairSpec {
to: string; to: string;
} }
/** Список цветов (палитра): массив hex-строк. */
export type ColorList = string[];
export interface ColorListSpec {
kind: "colors";
default: string[];
}
/** Смещение фигуры/объекта по центру: x + y в процентах. */ /** Смещение фигуры/объекта по центру: x + y в процентах. */
export interface Offset { export interface Offset {
x: number; x: number;
@@ -187,6 +195,7 @@ export const fieldSpecs = {
checkbox: {} as CheckboxSpec, checkbox: {} as CheckboxSpec,
dimension: {} as DimensionSpec, dimension: {} as DimensionSpec,
"color-pair": {} as ColorPairSpec, "color-pair": {} as ColorPairSpec,
colors: {} as ColorListSpec,
offset: {} as OffsetSpec, offset: {} as OffsetSpec,
position9: {} as Position9Spec, position9: {} as Position9Spec,
"font-style": {} as FontStyleSpec, "font-style": {} as FontStyleSpec,
@@ -262,6 +271,9 @@ export const field = {
colorPair: (s: { from: string; to: string }): Field<ColorPair> => ({ colorPair: (s: { from: string; to: string }): Field<ColorPair> => ({
spec: { kind: "color-pair", ...s }, spec: { kind: "color-pair", ...s },
}), }),
colors: (s: { default: string[] }): Field<ColorList> => ({
spec: { kind: "colors", ...s },
}),
offset: (s: { offset: (s: {
min: number; min: number;
max: number; max: number;
@@ -327,6 +339,8 @@ export function defaultSchemaParams<P>(
out[key] = { width: spec.width, height: spec.height }; out[key] = { width: spec.width, height: spec.height };
} else if (spec.kind === "color-pair") { } else if (spec.kind === "color-pair") {
out[key] = { from: spec.from, to: spec.to }; out[key] = { from: spec.from, to: spec.to };
} else if (spec.kind === "colors") {
out[key] = [...spec.default];
} else if (spec.kind === "offset") { } else if (spec.kind === "offset") {
out[key] = { x: spec.x, y: spec.y }; out[key] = { x: spec.x, y: spec.y };
} else if (spec.kind === "font-style") { } else if (spec.kind === "font-style") {
@@ -431,6 +445,15 @@ export function sanitizeSchemaParams<P>(
out[key] = { from, to }; out[key] = { from, to };
break; break;
} }
case "colors": {
const r =
Array.isArray(raw) &&
raw.every((c) => typeof c === "string" && /^#[0-9a-f]{6}$/i.test(c))
? (raw as string[])
: undefined;
out[key] = r && r.length > 0 ? [...r] : [...spec.default];
break;
}
case "position9": case "position9":
out[key] = out[key] =
typeof raw === "string" && typeof raw === "string" &&