refactor: separate schema fields components

This commit is contained in:
2026-09-05 15:56:21 +05:00
parent f286044ab9
commit 4a13bfbd16
7 changed files with 356 additions and 155 deletions
+20 -126
View File
@@ -1,6 +1,11 @@
<script lang="ts">
import { RotateCcw } from "@lucide/svelte";
import type { ToolSchema, Dimension } from "$lib/registry-schema";
import type { ToolSchema } from "$lib/registry-schema";
import CheckboxControl from "./fields/schema/CheckboxControl.svelte";
import ColorControl from "./fields/schema/ColorControl.svelte";
import RangeControl from "./fields/schema/RangeControl.svelte";
import SelectControl from "./fields/schema/SelectControl.svelte";
import TextControl from "./fields/schema/TextControl.svelte";
import DimensionField from "./fields/DimensionField.svelte";
interface Props {
@@ -11,6 +16,16 @@
}
let { schema, values, onchange, onreset }: Props = $props();
const FIELDS = {
number: RangeControl,
slider: RangeControl,
color: ColorControl,
select: SelectControl,
checkbox: CheckboxControl,
text: TextControl,
dimension: DimensionField,
} as const;
function labelOf(id: string): string {
return id
.replace(/([a-z])([A-Z])/g, "$1 $2")
@@ -20,73 +35,13 @@
</script>
{#each Object.entries(schema.fields) as [id, field] (id)}
{#if field.spec.kind === "number" || field.spec.kind === "slider"}
<label class="control">
<span>
{labelOf(id)}
<output>{String(values[id] ?? field.spec.default)}</output>
</span>
<input
type="range"
min={field.spec.min ?? 0}
max={field.spec.max ?? 100}
step={field.spec.step ?? 1}
value={Number(values[id] ?? field.spec.default)}
oninput={(e) =>
onchange(id, Number((e.target as HTMLInputElement).value))}
/>
</label>
{:else if field.spec.kind === "color"}
<label class="control color-control">
<span>{labelOf(id)}</span>
<span class="color-row">
<span
class="swatch"
style="background:{String(values[id] ?? field.spec.default)}"
></span>
<input
type="color"
value={String(values[id] ?? field.spec.default)}
oninput={(e) => onchange(id, (e.target as HTMLInputElement).value)}
/>
</span>
</label>
{:else if field.spec.kind === "select"}
<label class="control">
<span>{labelOf(id)}</span>
<select
class="select-field"
value={String(values[id] ?? field.spec.default)}
oninput={(e) => onchange(id, (e.target as HTMLSelectElement).value)}
>
{#each field.spec.options as option (option.value)}
<option value={option.value}>{option.label}</option>
{/each}
</select>
</label>
{:else if field.spec.kind === "checkbox"}
<label class="control checkbox-control">
<span>{labelOf(id)}</span>
<input
type="checkbox"
class="checkbox-field"
checked={Boolean(values[id] ?? field.spec.default)}
onchange={(e) => onchange(id, (e.target as HTMLInputElement).checked)}
/>
</label>
{:else if field.spec.kind === "dimension"}
<div class="control">
<DimensionField
{@const Control = FIELDS[field.spec.kind]}
<Control
label={labelOf(id)}
value={(values[id] as Dimension) ?? {
width: field.spec.width,
height: field.spec.height,
}}
value={values[id]}
spec={field.spec}
oninput={(v) => onchange(id, v)}
onchange={(v) => onchange(id, v)}
/>
</div>
{/if}
{/each}
<div class="panel-foot">
@@ -97,67 +52,6 @@
</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);
}
.control > span {
display: flex;
justify-content: space-between;
}
.control output {
color: var(--color-text);
}
input[type="range"] {
width: 100%;
accent-color: var(--color-main);
color: var(--color-text);
}
.select-field {
width: 100%;
padding: var(--space-m) var(--space-l);
background: var(--color-background);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-s);
color: var(--color-text);
font: var(--font-size-s) var(--font-mono);
}
.checkbox-control {
display: flex;
justify-content: space-between;
align-items: center;
}
.checkbox-field {
width: var(--space-xl);
height: var(--space-xl);
accent-color: var(--color-main);
}
.color-control > span:first-child {
justify-content: flex-start;
}
.color-row {
display: flex;
align-items: center;
gap: var(--space-m);
}
.swatch {
width: var(--space-xxl);
height: var(--space-xxl);
border-radius: var(--radius-s);
border: var(--size-border) solid var(--color-border);
}
input[type="color"] {
width: 100%;
height: var(--space-xxl);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-s);
background: var(--color-background);
cursor: pointer;
}
.panel-foot {
display: flex;
justify-content: space-between;
@@ -1,50 +1,97 @@
<script lang="ts">
import type { Dimension, DimensionSpec } from "$lib/registry-schema";
import NumberField from "./NumberField.svelte";
import type {
Dimension,
DimensionSpec,
FieldSpec,
} from "$lib/registry-schema";
interface Props {
label: string;
value: Dimension;
spec: DimensionSpec;
oninput?: (value: Dimension) => void;
value: unknown;
spec: FieldSpec;
onchange?: (value: Dimension) => void;
}
let { label, value = $bindable(), spec, oninput }: Props = $props();
let { label, value, spec, onchange }: Props = $props();
const sp = $derived(spec as DimensionSpec);
const current = $derived.by(() => {
const v = value as Partial<Dimension> | undefined;
return {
width:
typeof v?.width === "number" && Number.isFinite(v.width)
? v.width
: sp.width,
height:
typeof v?.height === "number" && Number.isFinite(v.height)
? v.height
: sp.height,
};
});
function setAxis(axis: "width" | "height", n: number) {
value = { ...value, [axis]: n };
oninput?.(value);
onchange?.({ ...current, [axis]: n });
}
</script>
<div class="dimension-field">
<div class="control dimension-control">
<span class="dimension-label">{label}</span>
<NumberField
label="Width"
value={value.width}
min={spec.min}
max={spec.max}
oninput={(n) => setAxis("width", n)}
<div class="dimension-field">
<label class="dimension-axis">
<span>Width</span>
<input
type="number"
min={sp.min}
max={sp.max}
value={current.width}
oninput={(e) =>
setAxis("width", Number((e.target as HTMLInputElement).value))}
/>
<NumberField
label="Height"
value={value.height}
min={spec.min}
max={spec.max}
oninput={(n) => setAxis("height", n)}
</label>
<label class="dimension-axis">
<span>Height</span>
<input
type="number"
min={sp.min}
max={sp.max}
value={current.height}
oninput={(e) =>
setAxis("height", Number((e.target as HTMLInputElement).value))}
/>
</label>
</div>
</div>
<style>
.dimension-field {
.control {
display: grid;
grid-template-columns: 1fr 1fr;
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);
}
.dimension-label {
grid-column: 1 / -1;
color: var(--color-text);
font-size: var(--font-size-s);
letter-spacing: var(--space-text-l);
text-transform: uppercase;
}
.dimension-field {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-m);
}
.dimension-axis > span {
display: block;
margin-bottom: var(--space-m);
}
.dimension-axis input {
width: 100%;
padding: var(--space-m) var(--space-l);
background: var(--color-background);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-s);
color: var(--color-text);
font: var(--font-size-s) var(--font-mono);
}
</style>
@@ -0,0 +1,47 @@
<script lang="ts">
import type { CheckboxSpec, FieldSpec } from "$lib/registry-schema";
interface Props {
label: string;
value: unknown;
spec: FieldSpec;
onchange?: (value: boolean) => void;
}
let { label, value, spec, onchange }: Props = $props();
const sp = $derived(spec as CheckboxSpec);
const current = $derived(
typeof value === "boolean" ? value : Boolean(sp.default),
);
</script>
<label class="control checkbox-control">
<span>{label}</span>
<input
type="checkbox"
class="checkbox-field"
checked={current}
onchange={(e) => onchange?.((e.target as HTMLInputElement).checked)}
/>
</label>
<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);
}
.checkbox-control {
display: flex;
justify-content: space-between;
align-items: center;
}
.checkbox-field {
width: var(--space-xl);
height: var(--space-xl);
accent-color: var(--color-main);
}
</style>
@@ -0,0 +1,59 @@
<script lang="ts">
import type { ColorSpec, FieldSpec } from "$lib/registry-schema";
interface Props {
label: string;
value: unknown;
spec: FieldSpec;
onchange?: (value: string) => void;
}
let { label, value, spec, onchange }: Props = $props();
const sp = $derived(spec as ColorSpec);
const current = $derived(typeof value === "string" ? value : sp.default);
</script>
<label class="control color-control">
<span>{label}</span>
<span class="color-row">
<span class="swatch" style="background:{current}"></span>
<input
type="color"
value={current}
oninput={(e) => onchange?.((e.target as HTMLInputElement).value)}
/>
</span>
</label>
<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);
}
.color-control > span:first-child {
justify-content: flex-start;
}
.color-row {
display: flex;
align-items: center;
gap: var(--space-m);
}
.swatch {
width: var(--space-xxl);
height: var(--space-xxl);
border-radius: var(--radius-s);
border: var(--size-border) solid var(--color-border);
}
input[type="color"] {
width: 100%;
height: var(--space-xxl);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-s);
background: var(--color-background);
cursor: pointer;
}
</style>
@@ -0,0 +1,58 @@
<script lang="ts">
import type { FieldSpec, NumberSpec, SliderSpec } from "$lib/registry-schema";
interface Props {
label: string;
value: unknown;
spec: FieldSpec;
onchange?: (value: number) => void;
}
let { label, value, spec, onchange }: Props = $props();
const sp = $derived(spec as NumberSpec | SliderSpec);
const current = $derived(
typeof value === "number" && Number.isFinite(value) ? value : sp.default,
);
function handle(e: Event) {
onchange?.(Number((e.target as HTMLInputElement).value));
}
</script>
<label class="control">
<span>
{label}
<output>{current}</output>
</span>
<input
type="range"
min={sp.min ?? 0}
max={sp.max ?? 100}
step={sp.step ?? 1}
value={current}
oninput={handle}
/>
</label>
<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);
}
.control > span {
display: flex;
justify-content: space-between;
}
.control output {
color: var(--color-text);
}
input[type="range"] {
width: 100%;
accent-color: var(--color-main);
color: var(--color-text);
}
</style>
@@ -0,0 +1,51 @@
<script lang="ts">
import type { FieldSpec, SelectSpec } from "$lib/registry-schema";
interface Props {
label: string;
value: unknown;
spec: FieldSpec;
onchange?: (value: string) => void;
}
let { label, value, spec, onchange }: Props = $props();
const sp = $derived(spec as SelectSpec);
const current = $derived(
typeof value === "string" && sp.options.some((o) => o.value === value)
? value
: sp.default,
);
</script>
<label class="control">
<span>{label}</span>
<select
class="select-field"
value={current}
oninput={(e) => onchange?.((e.target as HTMLSelectElement).value)}
>
{#each sp.options as option (option.value)}
<option value={option.value}>{option.label}</option>
{/each}
</select>
</label>
<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);
}
.select-field {
width: 100%;
padding: var(--space-m) var(--space-l);
background: var(--color-background);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-s);
color: var(--color-text);
font: var(--font-size-s) var(--font-mono);
}
</style>
@@ -0,0 +1,45 @@
<script lang="ts">
import type { FieldSpec, TextSpec } from "$lib/registry-schema";
interface Props {
label: string;
value: unknown;
spec: FieldSpec;
onchange?: (value: string) => void;
}
let { label, value, spec, onchange }: Props = $props();
const sp = $derived(spec as TextSpec);
const current = $derived(typeof value === "string" ? value : sp.default);
</script>
<label class="control">
<span>{label}</span>
<input
class="text-field"
type="text"
value={current}
placeholder={sp.placeholder}
oninput={(e) => onchange?.((e.target as HTMLInputElement).value)}
/>
</label>
<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);
}
.text-field {
width: 100%;
padding: var(--space-m) var(--space-l);
background: var(--color-background);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-s);
color: var(--color-text);
font: var(--font-size-s) var(--font-mono);
}
</style>