refactor: move form fields to separate folder

This commit is contained in:
2026-09-02 21:11:43 +05:00
parent a24a182d18
commit 6ec3e75df4
15 changed files with 21 additions and 21 deletions
@@ -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: 0.5rem;
font-size: 0.85rem;
color: var(--foreground);
cursor: pointer;
}
.checkbox-field input {
accent-color: var(--blue);
}
</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>
<div class="control-block">
<label>{label}</label>
<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>
</div>
<style>
.control-block :global(.icon) {
color: var(--foreground);
}
label {
display: flex;
justify-content: space-between;
font: 10px var(--font-mono);
letter-spacing: 0.08em;
color: var(--muted);
margin-bottom: 8px;
}
.color-field {
display: flex;
align-items: center;
gap: 8px;
padding-left: 8px;
padding-right: 8px;
height: 32px;
border: 1px solid var(--line);
}
.swatch {
width: 14px;
height: 14px;
border: 1px solid var(--line);
flex: none;
}
.color-field input {
min-width: 0;
color: var(--foreground);
font: 11px var(--font-mono);
background: 0 0;
border: 0;
outline: 0;
flex: 1;
}
.color-field :global(svg) {
color: var(--muted);
flex: none;
}
</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,35 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
compact?: boolean;
children: Snippet;
}
let { compact = false, children }: Props = $props();
</script>
<div class="controls" class:compact>
{@render children()}
</div>
<style>
.controls {
display: grid;
grid-template-columns: 1.1fr 1fr 1.2fr;
gap: 16px;
}
.controls.compact {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 800px) {
.controls,
.controls.compact {
grid-auto-flow: row;
grid-auto-columns: auto;
}
}
</style>
@@ -0,0 +1,47 @@
<script lang="ts">
import Field from "./Field.svelte";
interface Props {
label: string;
value: number;
min?: number;
max?: number;
step?: number;
oninput?: (value: number) => void;
}
let { label, value = $bindable(), min, max, step, oninput }: Props = $props();
function handle(e: Event) {
value = Number((e.target as HTMLInputElement).value);
oninput?.(value);
}
</script>
<Field {label}>
<input
class="number-field"
type="number"
{min}
{max}
{step}
{value}
oninput={handle}
/>
</Field>
<style>
.number-field {
width: 100%;
font-family: var(--font-mono);
font-size: 11px;
color: var(--foreground);
background: var(--background);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 0.35rem 0.5rem;
}
.number-field:focus {
outline: none;
border-color: var(--blue);
}
</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: 11px;
color: var(--foreground);
background: var(--background);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 0.35rem 0.5rem;
}
</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: 10px var(--font-mono);
letter-spacing: 0.08em;
color: var(--muted);
margin-bottom: 8px;
}
output {
font: 10px var(--font-mono);
color: var(--foreground);
}
input[type="range"] {
color: var(--foreground);
line-height: inherit;
width: 100%;
accent-color: var(--blue);
}
</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: 11px;
color: var(--foreground);
background: var(--background);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 0.35rem 0.5rem;
}
.text-field:focus {
outline: none;
border-color: var(--blue);
}
</style>
@@ -0,0 +1,23 @@
<script lang="ts">
import Toggle from "../ui/Toggle.svelte";
interface Props {
label: string;
checked: boolean;
onchange?: (checked: boolean) => void;
}
let { label, checked = $bindable(), onchange }: Props = $props();
</script>
<div class="toggle-row">
<span>{label}</span>
<Toggle bind:checked {onchange} {label} />
</div>
<style>
.toggle-row {
color: var(--foreground);
cursor: pointer;
}
</style>