mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-16 06:26:36 +00:00
feat: flatten components/kit -> components folder
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
label: string;
|
||||
checked: boolean;
|
||||
onchange?: (checked: boolean) => void;
|
||||
}
|
||||
let { label, checked = $bindable(), onchange }: Props = $props();
|
||||
|
||||
function handle(e: Event) {
|
||||
checked = (e.target as HTMLInputElement).checked;
|
||||
onchange?.(checked);
|
||||
}
|
||||
</script>
|
||||
|
||||
<label class="checkbox-field">
|
||||
<input type="checkbox" {checked} onchange={handle} />
|
||||
<span class="checkbox-label">{label}</span>
|
||||
</label>
|
||||
|
||||
<style>
|
||||
.checkbox-field {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-m);
|
||||
font-size: var(--font-size-m);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
}
|
||||
.checkbox-field input {
|
||||
accent-color: var(--color-main);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,80 @@
|
||||
<script lang="ts">
|
||||
import { ChevronDown } from "@lucide/svelte";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
value: string;
|
||||
dark?: boolean;
|
||||
chevron?: boolean;
|
||||
oninput?: (value: string) => void;
|
||||
}
|
||||
let {
|
||||
label,
|
||||
value = $bindable(),
|
||||
dark = false,
|
||||
chevron = false,
|
||||
oninput,
|
||||
}: Props = $props();
|
||||
|
||||
function handle(e: Event) {
|
||||
value = (e.target as HTMLInputElement).value;
|
||||
oninput?.(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<label class="control-block">
|
||||
<span class="control-title">{label}</span>
|
||||
<div class="color-field">
|
||||
<span class="swatch" class:dark style="background:{value}"></span>
|
||||
<input
|
||||
type="text"
|
||||
aria-label={label}
|
||||
{value}
|
||||
oninput={handle}
|
||||
spellcheck="false"
|
||||
/>
|
||||
{#if chevron}<ChevronDown class="icon" size={14} />{/if}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<style>
|
||||
.control-block :global(.icon) {
|
||||
color: var(--color-text);
|
||||
}
|
||||
.control-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font: var(--font-size-s) var(--font-mono);
|
||||
letter-spacing: var(--space-text-l);
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: var(--space-m);
|
||||
}
|
||||
.color-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-m);
|
||||
padding-left: var(--space-m);
|
||||
padding-right: var(--space-m);
|
||||
height: var(--space-xxxl);
|
||||
border: var(--size-border) solid var(--color-border);
|
||||
}
|
||||
.swatch {
|
||||
width: var(--space-l);
|
||||
height: var(--space-l);
|
||||
border: var(--size-border) solid var(--color-border);
|
||||
flex: none;
|
||||
}
|
||||
.color-field input {
|
||||
min-width: 0;
|
||||
color: var(--color-text);
|
||||
font: var(--font-size-s) var(--font-mono);
|
||||
background: 0 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.color-field :global(svg) {
|
||||
color: var(--color-text-muted);
|
||||
flex: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<script lang="ts">
|
||||
import type {
|
||||
Dimension,
|
||||
DimensionSpec,
|
||||
FieldSpec,
|
||||
} from "$lib/registry-schema";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
value: unknown;
|
||||
spec: FieldSpec;
|
||||
onchange?: (value: Dimension) => void;
|
||||
}
|
||||
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) {
|
||||
onchange?.({ ...current, [axis]: n });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="control dimension-control">
|
||||
<span class="dimension-label">{label}</span>
|
||||
<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))}
|
||||
/>
|
||||
</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>
|
||||
.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);
|
||||
}
|
||||
.dimension-label {
|
||||
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,35 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import MonoLabel from "../MonoLabel.svelte";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
children: Snippet;
|
||||
hint?: string;
|
||||
}
|
||||
let { label, children, hint }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="field">
|
||||
<MonoLabel>{label}</MonoLabel>
|
||||
<div class="field-control">{@render children()}</div>
|
||||
{#if hint}<span class="field-hint">{hint}</span>{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-s);
|
||||
}
|
||||
.field-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-m);
|
||||
}
|
||||
.field-hint {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-s);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import Field from "./Field.svelte";
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
value: string;
|
||||
options: Option[];
|
||||
onchange?: (value: string) => void;
|
||||
}
|
||||
let { label, value = $bindable(), options, onchange }: Props = $props();
|
||||
|
||||
function handle(e: Event) {
|
||||
value = (e.target as HTMLSelectElement).value;
|
||||
onchange?.(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Field {label}>
|
||||
<select class="select-field" {value} onchange={handle}>
|
||||
{#each options as opt (opt.value)}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</Field>
|
||||
|
||||
<style>
|
||||
.select-field {
|
||||
width: 100%;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-s);
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
border: var(--size-border) solid var(--color-border);
|
||||
border-radius: var(--radius-m);
|
||||
padding: var(--space-s) var(--space-m);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
label: string;
|
||||
value: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
suffix?: string;
|
||||
oninput?: (value: number) => void;
|
||||
}
|
||||
let {
|
||||
label,
|
||||
value = $bindable(),
|
||||
min = 0,
|
||||
max = 100,
|
||||
step = 1,
|
||||
suffix = "",
|
||||
oninput,
|
||||
}: Props = $props();
|
||||
|
||||
function handle(e: Event) {
|
||||
value = Number((e.target as HTMLInputElement).value);
|
||||
oninput?.(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="control-block">
|
||||
<label>{label} <output>{value}{suffix}</output></label>
|
||||
<input type="range" {min} {max} {step} {value} oninput={handle} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font: var(--font-size-s) var(--font-mono);
|
||||
letter-spacing: var(--space-text-l);
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: var(--space-m);
|
||||
}
|
||||
output {
|
||||
font: var(--font-size-s) var(--font-mono);
|
||||
color: var(--color-text);
|
||||
}
|
||||
input[type="range"] {
|
||||
color: var(--color-text);
|
||||
line-height: inherit;
|
||||
width: 100%;
|
||||
accent-color: var(--color-main);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
value: string;
|
||||
type?: string;
|
||||
placeholder?: string;
|
||||
readonly?: boolean;
|
||||
label?: string;
|
||||
oninput?: (value: string) => void;
|
||||
}
|
||||
let {
|
||||
value = $bindable(),
|
||||
type = "text",
|
||||
placeholder = "",
|
||||
readonly = false,
|
||||
label,
|
||||
oninput,
|
||||
}: Props = $props();
|
||||
|
||||
function handle(e: Event) {
|
||||
value = (e.target as HTMLInputElement).value;
|
||||
oninput?.(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<input
|
||||
class="text-field"
|
||||
{type}
|
||||
{placeholder}
|
||||
{readonly}
|
||||
{value}
|
||||
aria-label={label}
|
||||
oninput={handle}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.text-field {
|
||||
width: 100%;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-s);
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
border: var(--size-border) solid var(--color-border);
|
||||
border-radius: var(--radius-m);
|
||||
padding: var(--space-s) var(--space-m);
|
||||
}
|
||||
.text-field:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-main);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,107 @@
|
||||
<script lang="ts">
|
||||
interface Preset {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
label?: string;
|
||||
value: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
presets?: Preset[];
|
||||
onchange: (angle: number) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
label = "Angle",
|
||||
value,
|
||||
min = 0,
|
||||
max = 360,
|
||||
step = 1,
|
||||
presets = [
|
||||
{ value: 0, label: "0°" },
|
||||
{ value: 90, label: "90°" },
|
||||
{ value: 180, label: "180°" },
|
||||
{ value: 270, label: "270°" },
|
||||
],
|
||||
onchange,
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="angle-control">
|
||||
{#if label}
|
||||
<span class="angle-label">{label}</span>
|
||||
{/if}
|
||||
<div class="angle-row">
|
||||
<input
|
||||
type="range"
|
||||
{min}
|
||||
{max}
|
||||
{step}
|
||||
{value}
|
||||
oninput={(e) => onchange(Number((e.target as HTMLInputElement).value))}
|
||||
/>
|
||||
<output>{value}°</output>
|
||||
</div>
|
||||
<div class="angle-presets">
|
||||
{#each presets as preset (preset.value)}
|
||||
<button
|
||||
class:active={preset.value === value}
|
||||
onclick={() => onchange(preset.value)}>{preset.label}</button
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.angle-control {
|
||||
display: grid;
|
||||
gap: var(--space-m);
|
||||
}
|
||||
.angle-label {
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-s);
|
||||
letter-spacing: var(--space-text-l);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.angle-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
gap: var(--space-m);
|
||||
}
|
||||
.angle-row input[type="range"] {
|
||||
width: 100%;
|
||||
accent-color: var(--color-main);
|
||||
}
|
||||
.angle-row output {
|
||||
color: var(--color-text);
|
||||
font: var(--font-size-s) var(--font-mono);
|
||||
letter-spacing: var(--space-text-m);
|
||||
}
|
||||
.angle-presets {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--space-m);
|
||||
}
|
||||
.angle-presets button {
|
||||
padding: var(--space-m) 0;
|
||||
color: var(--color-text-muted);
|
||||
background: var(--color-background);
|
||||
border: var(--size-border) solid var(--color-border);
|
||||
border-radius: var(--radius-s);
|
||||
font: var(--font-size-s) var(--font-mono);
|
||||
letter-spacing: var(--space-text-m);
|
||||
cursor: pointer;
|
||||
}
|
||||
.angle-presets button:hover {
|
||||
color: var(--color-text);
|
||||
border-color: var(--color-main);
|
||||
}
|
||||
.angle-presets button.active {
|
||||
color: var(--color-main);
|
||||
border-color: var(--color-main);
|
||||
}
|
||||
</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,97 @@
|
||||
<script lang="ts">
|
||||
import type {
|
||||
ColorPair,
|
||||
ColorPairSpec,
|
||||
FieldSpec,
|
||||
} from "$lib/registry-schema";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
value: unknown;
|
||||
spec: FieldSpec;
|
||||
onchange?: (value: ColorPair) => void;
|
||||
}
|
||||
let { label, value, spec, onchange }: Props = $props();
|
||||
|
||||
const sp = $derived(spec as ColorPairSpec);
|
||||
const current = $derived.by(() => {
|
||||
const v = value as Partial<ColorPair> | undefined;
|
||||
return {
|
||||
from: typeof v?.from === "string" ? v.from : sp.from,
|
||||
to: typeof v?.to === "string" ? v.to : sp.to,
|
||||
};
|
||||
});
|
||||
|
||||
function setColor(axis: "from" | "to", hex: string) {
|
||||
onchange?.({ ...current, [axis]: hex });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="control color-pair-control">
|
||||
<span class="pair-label">{label}</span>
|
||||
<div class="pair-field">
|
||||
<label class="pair-axis">
|
||||
<span class="swatch" style="background:{current.from}"></span>
|
||||
<input
|
||||
type="color"
|
||||
value={current.from}
|
||||
oninput={(e) => setColor("from", (e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
<span class="axis-label">From</span>
|
||||
</label>
|
||||
<label class="pair-axis">
|
||||
<span class="swatch" style="background:{current.to}"></span>
|
||||
<input
|
||||
type="color"
|
||||
value={current.to}
|
||||
oninput={(e) => setColor("to", (e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
<span class="axis-label">To</span>
|
||||
</label>
|
||||
</div>
|
||||
</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);
|
||||
}
|
||||
.pair-label {
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-s);
|
||||
letter-spacing: var(--space-text-l);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.pair-field {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-m);
|
||||
}
|
||||
.pair-axis {
|
||||
display: grid;
|
||||
grid-template-columns: var(--space-xxl) 1fr;
|
||||
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);
|
||||
}
|
||||
.pair-axis 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;
|
||||
}
|
||||
.axis-label {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,162 @@
|
||||
<script lang="ts">
|
||||
import type { TextFont } from "$lib/core/domText";
|
||||
import type {
|
||||
FieldSpec,
|
||||
FontStyle,
|
||||
FontStyleSpec,
|
||||
} from "$lib/registry-schema";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
value: unknown;
|
||||
spec: FieldSpec;
|
||||
onchange?: (value: FontStyle) => void;
|
||||
}
|
||||
let { label, value, spec, onchange }: Props = $props();
|
||||
|
||||
const sp = $derived(spec as FontStyleSpec);
|
||||
const current = $derived.by(() => {
|
||||
const v = value as Partial<FontStyle> | undefined;
|
||||
return {
|
||||
font:
|
||||
v?.font === "sans" || v?.font === "serif" || v?.font === "mono"
|
||||
? v.font
|
||||
: sp.font,
|
||||
size:
|
||||
typeof v?.size === "number" && Number.isFinite(v.size)
|
||||
? v.size
|
||||
: sp.size,
|
||||
bold: typeof v?.bold === "boolean" ? v.bold : sp.bold,
|
||||
color: typeof v?.color === "string" ? v.color : sp.color,
|
||||
};
|
||||
});
|
||||
|
||||
function set<K extends keyof FontStyle>(key: K, val: FontStyle[K]) {
|
||||
onchange?.({ ...current, [key]: val });
|
||||
}
|
||||
|
||||
const FONTS: { value: TextFont; label: string }[] = [
|
||||
{ value: "sans", label: "Sans" },
|
||||
{ value: "serif", label: "Serif" },
|
||||
{ value: "mono", label: "Mono" },
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="control font-style-control">
|
||||
<span class="fs-label">{label}</span>
|
||||
<div class="fs-grid">
|
||||
<label class="fs-cell">
|
||||
<span class="fs-sub">Font</span>
|
||||
<select
|
||||
class="fs-select"
|
||||
value={current.font}
|
||||
oninput={(e) =>
|
||||
set("font", (e.target as HTMLSelectElement).value as TextFont)}
|
||||
>
|
||||
{#each FONTS as option (option.value)}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label class="fs-cell">
|
||||
<span class="fs-sub">Size</span>
|
||||
<input
|
||||
class="fs-input"
|
||||
type="number"
|
||||
min={sp.min}
|
||||
max={sp.max}
|
||||
value={current.size}
|
||||
oninput={(e) =>
|
||||
set("size", Number((e.target as HTMLInputElement).value))}
|
||||
/>
|
||||
</label>
|
||||
<label class="fs-cell">
|
||||
<span class="fs-sub">Color</span>
|
||||
<span class="fs-color">
|
||||
<span class="swatch" style="background:{current.color}"></span>
|
||||
<input
|
||||
class="fs-color-input"
|
||||
type="color"
|
||||
value={current.color}
|
||||
oninput={(e) => set("color", (e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
<label class="fs-cell fs-bold">
|
||||
<span class="fs-sub">Bold</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={current.bold}
|
||||
onchange={(e) => set("bold", (e.target as HTMLInputElement).checked)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</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);
|
||||
}
|
||||
.fs-label {
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-s);
|
||||
letter-spacing: var(--space-text-l);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.fs-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-m);
|
||||
}
|
||||
.fs-cell {
|
||||
display: grid;
|
||||
gap: var(--space-m);
|
||||
}
|
||||
.fs-sub {
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
.fs-select,
|
||||
.fs-input,
|
||||
.fs-color-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);
|
||||
}
|
||||
.fs-color {
|
||||
display: grid;
|
||||
grid-template-columns: var(--space-xxl) 1fr;
|
||||
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);
|
||||
}
|
||||
.fs-color-input {
|
||||
height: var(--space-xxl);
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.fs-bold {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
.fs-bold input[type="checkbox"] {
|
||||
width: var(--space-xl);
|
||||
height: var(--space-xl);
|
||||
accent-color: var(--color-main);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import AngleControl from "./AngleControl.svelte";
|
||||
import type { FieldSpec, Gradient, GradientSpec } from "$lib/registry-schema";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
value: unknown;
|
||||
spec: FieldSpec;
|
||||
onchange?: (value: Gradient) => void;
|
||||
}
|
||||
let { label, value, spec, onchange }: Props = $props();
|
||||
|
||||
const sp = $derived(spec as GradientSpec);
|
||||
const current = $derived.by(() => {
|
||||
const v = value as Partial<Gradient> | undefined;
|
||||
return {
|
||||
from: typeof v?.from === "string" ? v.from : sp.from,
|
||||
to: typeof v?.to === "string" ? v.to : sp.to,
|
||||
angle:
|
||||
typeof v?.angle === "number" && Number.isFinite(v.angle)
|
||||
? v.angle
|
||||
: sp.angle,
|
||||
};
|
||||
});
|
||||
|
||||
function set<K extends keyof Gradient>(key: K, val: Gradient[K]) {
|
||||
onchange?.({ ...current, [key]: val });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="control gradient-control">
|
||||
<span class="gradient-label">{label}</span>
|
||||
<div class="gradient-colors">
|
||||
<label class="color-field">
|
||||
<span>From</span>
|
||||
<span class="swatch" style="background:{current.from}"></span>
|
||||
<input
|
||||
type="color"
|
||||
value={current.from}
|
||||
oninput={(e) => set("from", (e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</label>
|
||||
<label class="color-field">
|
||||
<span>To</span>
|
||||
<span class="swatch" style="background:{current.to}"></span>
|
||||
<input
|
||||
type="color"
|
||||
value={current.to}
|
||||
oninput={(e) => set("to", (e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<AngleControl value={current.angle} onchange={(a) => set("angle", a)} />
|
||||
</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);
|
||||
}
|
||||
.gradient-label {
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-s);
|
||||
letter-spacing: var(--space-text-l);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.gradient-colors {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-m) var(--space-l);
|
||||
}
|
||||
.color-field {
|
||||
display: grid;
|
||||
grid-template-columns: auto var(--space-xxl) 1fr;
|
||||
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);
|
||||
}
|
||||
.color-field input[type="color"] {
|
||||
width: 100%;
|
||||
height: var(--space-xxl);
|
||||
padding: 0;
|
||||
background: var(--color-background);
|
||||
border: var(--size-border) solid var(--color-border);
|
||||
border-radius: var(--radius-s);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,87 @@
|
||||
<script lang="ts">
|
||||
import type { FieldSpec, Offset, OffsetSpec } from "$lib/registry-schema";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
value: unknown;
|
||||
spec: FieldSpec;
|
||||
onchange?: (value: Offset) => void;
|
||||
}
|
||||
let { label, value, spec, onchange }: Props = $props();
|
||||
|
||||
const sp = $derived(spec as OffsetSpec);
|
||||
const current = $derived.by(() => {
|
||||
const v = value as Partial<Offset> | undefined;
|
||||
return {
|
||||
x: typeof v?.x === "number" && Number.isFinite(v.x) ? v.x : sp.x,
|
||||
y: typeof v?.y === "number" && Number.isFinite(v.y) ? v.y : sp.y,
|
||||
};
|
||||
});
|
||||
|
||||
function setAxis(axis: "x" | "y", n: number) {
|
||||
onchange?.({ ...current, [axis]: n });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="control offset-control">
|
||||
<span class="offset-label">{label}</span>
|
||||
<div class="offset-field">
|
||||
<label class="offset-axis">
|
||||
<span>X</span>
|
||||
<input
|
||||
type="number"
|
||||
min={sp.min}
|
||||
max={sp.max}
|
||||
value={current.x}
|
||||
oninput={(e) =>
|
||||
setAxis("x", Number((e.target as HTMLInputElement).value))}
|
||||
/>
|
||||
</label>
|
||||
<label class="offset-axis">
|
||||
<span>Y</span>
|
||||
<input
|
||||
type="number"
|
||||
min={sp.min}
|
||||
max={sp.max}
|
||||
value={current.y}
|
||||
oninput={(e) =>
|
||||
setAxis("y", Number((e.target as HTMLInputElement).value))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</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);
|
||||
}
|
||||
.offset-label {
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-s);
|
||||
letter-spacing: var(--space-text-l);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.offset-field {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-m);
|
||||
}
|
||||
.offset-axis > span {
|
||||
display: block;
|
||||
margin-bottom: var(--space-m);
|
||||
}
|
||||
.offset-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,139 @@
|
||||
<script lang="ts">
|
||||
import type { FieldSpec, Plate, PlateSpec } from "$lib/registry-schema";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
value: unknown;
|
||||
spec: FieldSpec;
|
||||
onchange?: (value: Plate) => void;
|
||||
}
|
||||
let { label, value, spec, onchange }: Props = $props();
|
||||
|
||||
const sp = $derived(spec as PlateSpec);
|
||||
const current = $derived.by(() => {
|
||||
const v = value as Partial<Plate> | undefined;
|
||||
return {
|
||||
enabled: typeof v?.enabled === "boolean" ? v.enabled : sp.enabled,
|
||||
color: typeof v?.color === "string" ? v.color : sp.color,
|
||||
opacity:
|
||||
typeof v?.opacity === "number" && Number.isFinite(v.opacity)
|
||||
? v.opacity
|
||||
: sp.opacity,
|
||||
};
|
||||
});
|
||||
|
||||
function set<K extends keyof Plate>(key: K, val: Plate[K]) {
|
||||
onchange?.({ ...current, [key]: val });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="control plate-control">
|
||||
<span class="plate-label">{label}</span>
|
||||
<div class="plate-row {current.enabled ? '' : 'off'}">
|
||||
<label class="plate-enabled">
|
||||
<span>Backing plate</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={current.enabled}
|
||||
onchange={(e) => set("enabled", (e.target as HTMLInputElement).checked)}
|
||||
/>
|
||||
</label>
|
||||
<label class="plate-color">
|
||||
<span class="swatch" style="background:{current.color}"></span>
|
||||
<input
|
||||
type="color"
|
||||
value={current.color}
|
||||
disabled={!current.enabled}
|
||||
oninput={(e) => set("color", (e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</label>
|
||||
<label class="plate-opacity">
|
||||
<span>
|
||||
Opacity
|
||||
<output>{current.opacity}%</output>
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
step="5"
|
||||
value={current.opacity}
|
||||
disabled={!current.enabled}
|
||||
oninput={(e) =>
|
||||
set("opacity", Number((e.target as HTMLInputElement).value))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</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);
|
||||
}
|
||||
.plate-label {
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-s);
|
||||
letter-spacing: var(--space-text-l);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.plate-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-m) var(--space-l);
|
||||
}
|
||||
.plate-row.off {
|
||||
opacity: 0.55;
|
||||
}
|
||||
.plate-enabled {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.plate-enabled input[type="checkbox"] {
|
||||
width: var(--space-xl);
|
||||
height: var(--space-xl);
|
||||
accent-color: var(--color-main);
|
||||
}
|
||||
.plate-color {
|
||||
display: grid;
|
||||
grid-template-columns: var(--space-xxl) 1fr;
|
||||
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);
|
||||
}
|
||||
.plate-color input[type="color"] {
|
||||
width: 100%;
|
||||
height: var(--space-xxl);
|
||||
padding: 0;
|
||||
background: var(--color-background);
|
||||
border: var(--size-border) solid var(--color-border);
|
||||
border-radius: var(--radius-s);
|
||||
}
|
||||
.plate-opacity {
|
||||
display: grid;
|
||||
gap: var(--space-m);
|
||||
}
|
||||
.plate-opacity > span {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.plate-opacity output {
|
||||
color: var(--color-text);
|
||||
}
|
||||
.plate-opacity input[type="range"] {
|
||||
width: 100%;
|
||||
accent-color: var(--color-main);
|
||||
color: var(--color-text);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,90 @@
|
||||
<script lang="ts">
|
||||
import type { Position9 } from "$lib/core/textdraw";
|
||||
import {
|
||||
POSITION9_VALUES,
|
||||
type FieldSpec,
|
||||
type Position9Spec,
|
||||
} from "$lib/registry-schema";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
value: unknown;
|
||||
spec: FieldSpec;
|
||||
onchange?: (value: Position9) => void;
|
||||
}
|
||||
let { label, value, spec, onchange }: Props = $props();
|
||||
|
||||
const sp = $derived(spec as Position9Spec);
|
||||
const current = $derived(
|
||||
typeof value === "string" &&
|
||||
(POSITION9_VALUES as readonly string[]).includes(value)
|
||||
? (value as Position9)
|
||||
: sp.default,
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="control position-control">
|
||||
<span class="position-label">{label}</span>
|
||||
<div class="grid">
|
||||
{#each POSITION9_VALUES as position (position)}
|
||||
<button
|
||||
type="button"
|
||||
class="cell {current === position ? 'active' : ''}"
|
||||
aria-label={position}
|
||||
aria-pressed={current === position}
|
||||
onclick={() => onchange?.(position)}
|
||||
>
|
||||
<span class="dot"></span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</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);
|
||||
}
|
||||
.position-label {
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-s);
|
||||
letter-spacing: var(--space-text-l);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.grid {
|
||||
display: inline-grid;
|
||||
grid-template-columns: repeat(3, var(--space-xl));
|
||||
gap: var(--space-s);
|
||||
}
|
||||
.cell {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: var(--space-xl);
|
||||
height: var(--space-xl);
|
||||
padding: 0;
|
||||
background: var(--color-background);
|
||||
border: var(--size-border) solid var(--color-border);
|
||||
border-radius: var(--radius-s);
|
||||
cursor: pointer;
|
||||
}
|
||||
.cell:hover {
|
||||
border-color: var(--color-main);
|
||||
}
|
||||
.dot {
|
||||
width: var(--space-s);
|
||||
height: var(--space-s);
|
||||
border-radius: 50%;
|
||||
background: var(--color-border);
|
||||
}
|
||||
.active {
|
||||
border-color: var(--color-main);
|
||||
background: var(--color-main);
|
||||
}
|
||||
.active .dot {
|
||||
background: var(--color-text);
|
||||
}
|
||||
</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>
|
||||
Reference in New Issue
Block a user