feat(fend): skeleton components
This commit is contained in:
parent
3c6a31f86b
commit
608d863c88
18 changed files with 395 additions and 77 deletions
|
|
@ -3,10 +3,15 @@
|
|||
<description>The standard record format used by a majority of players.</description>
|
||||
|
||||
<record type="public" expanded="true">
|
||||
<field label="Name" type="text" />
|
||||
<field label="Species" type="species" />
|
||||
<field label="Name" type="text" required="true" />
|
||||
<field label="Species" type="species" required="true" />
|
||||
<field label="Subspecies" type="subspecies" />
|
||||
<field label="Pronouns" type="text" />
|
||||
<field label="Pronouns" type="multi-select">
|
||||
<option value="he/him" label="he/him" />
|
||||
<option value="she/her" label="she/her" />
|
||||
<option value="they/them" label="they/them" />
|
||||
<option value="it/its" label="it/its" />
|
||||
</field>
|
||||
<field label="Date of Birth" type="date" placeholder="March 15th, 2438" />
|
||||
<field label="Citizenship" type="citizenship" />
|
||||
<field label="Spoken Languages" type="languages" />
|
||||
|
|
@ -29,7 +34,7 @@
|
|||
|
||||
<record type="medical" expanded="false">
|
||||
<preamble>The following information is protected by doctor-patient confidentiality laws. Do not release without patient's consent.</preamble>
|
||||
<field label="Opt-Outs" type="checkbox">
|
||||
<field label="Opt-Outs" type="multi-select">
|
||||
<option value="no-borg" label="Do Not Borgify" />
|
||||
<option value="no-revive" label="Do Not Revive" />
|
||||
<option value="no-prosthetic" label="Do Not Prostheticize" />
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { X } from 'lucide-svelte';
|
||||
import type { CheckboxField } from '$lib/types';
|
||||
|
||||
let { field, value = [], onChange }: { field: CheckboxField; value: string[]; onChange: (v: string[]) => void } = $props();
|
||||
|
||||
let customInput = $state('');
|
||||
|
||||
let knownValues = $derived(new Set(field.options.map((o) => o.value)));
|
||||
let customValues = $derived(value.filter((v) => !knownValues.has(v)));
|
||||
|
||||
function toggle(optValue: string) {
|
||||
if (value.includes(optValue)) {
|
||||
onChange(value.filter((v) => v !== optValue));
|
||||
|
|
@ -10,10 +16,22 @@
|
|||
onChange([...value, optValue]);
|
||||
}
|
||||
}
|
||||
|
||||
function addCustom() {
|
||||
const trimmed = customInput.trim();
|
||||
if (trimmed && !value.includes(trimmed)) {
|
||||
onChange([...value, trimmed]);
|
||||
}
|
||||
customInput = '';
|
||||
}
|
||||
|
||||
function removeCustom(val: string) {
|
||||
onChange(value.filter((v) => v !== val));
|
||||
}
|
||||
</script>
|
||||
|
||||
<fieldset>
|
||||
<legend class="text-sm font-medium">{field.label}</legend>
|
||||
<legend class="text-sm font-medium">{field.label}{#if field.required}<span style="color: var(--accent);"> *</span>{/if}</legend>
|
||||
<div class="mt-1 flex flex-col gap-1">
|
||||
{#each field.options as opt}
|
||||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||||
|
|
@ -25,5 +43,31 @@
|
|||
{opt.label}
|
||||
</label>
|
||||
{/each}
|
||||
{#each customValues as cv}
|
||||
<span class="flex items-center gap-2 text-sm">
|
||||
<input type="checkbox" checked={true} onchange={() => removeCustom(cv)} />
|
||||
{cv}
|
||||
<button onclick={() => removeCustom(cv)} class="hover:opacity-60">
|
||||
<X size={12} />
|
||||
</button>
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={customInput}
|
||||
placeholder="Other..."
|
||||
onkeydown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addCustom(); } }}
|
||||
class="rounded px-3 py-1.5 text-sm"
|
||||
style="background: var(--bg-input); border: 1px solid var(--border); color: var(--text);"
|
||||
/>
|
||||
<button
|
||||
onclick={addCustom}
|
||||
class="text-sm hover:opacity-80"
|
||||
style="color: var(--text-muted);"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
|
|||
|
|
@ -3,19 +3,54 @@
|
|||
import { citizenships } from '$lib/data';
|
||||
|
||||
let { field, value = '', onChange }: { field: CitizenshipField; value: string; onChange: (v: string) => void } = $props();
|
||||
|
||||
let custom = $state(false);
|
||||
|
||||
function handleSelect(v: string) {
|
||||
if (v === '__custom') {
|
||||
custom = true;
|
||||
onChange('');
|
||||
} else {
|
||||
custom = false;
|
||||
onChange(v);
|
||||
}
|
||||
}
|
||||
|
||||
let isCustom = $derived(custom || (value !== '' && !citizenships.some((c) => c.name === value)));
|
||||
</script>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium">{field.label}</span>
|
||||
<select
|
||||
{value}
|
||||
onchange={(e) => onChange((e.target as HTMLSelectElement).value)}
|
||||
class="mt-1 block w-full rounded px-3 py-2 text-sm"
|
||||
style="background: var(--bg-input); border: 1px solid var(--border); color: var(--text);"
|
||||
>
|
||||
<option value="">—</option>
|
||||
{#each citizenships as c}
|
||||
<option value={c.name}>{c.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<span class="text-sm font-medium">{field.label}{#if field.required}<span style="color: var(--accent);"> *</span>{/if}</span>
|
||||
{#if isCustom}
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<input
|
||||
type="text"
|
||||
{value}
|
||||
placeholder="Enter citizenship"
|
||||
oninput={(e) => onChange((e.target as HTMLInputElement).value)}
|
||||
class="block w-full rounded px-3 py-2 text-sm"
|
||||
style="background: var(--bg-input); border: 1px solid var(--border); color: var(--text);"
|
||||
/>
|
||||
<button
|
||||
onclick={() => { custom = false; onChange(''); }}
|
||||
class="text-sm whitespace-nowrap hover:opacity-80"
|
||||
style="color: var(--text-muted);"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<select
|
||||
{value}
|
||||
onchange={(e) => handleSelect((e.target as HTMLSelectElement).value)}
|
||||
class="mt-1 block w-full rounded px-3 py-2 text-sm"
|
||||
style="background: var(--bg-input); border: 1px solid var(--border); color: var(--text);"
|
||||
>
|
||||
<option value="">—</option>
|
||||
{#each citizenships as c}
|
||||
<option value={c.name}>{c.name}</option>
|
||||
{/each}
|
||||
<option value="__custom">Other...</option>
|
||||
</select>
|
||||
{/if}
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
</script>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium">{field.label}</span>
|
||||
<span class="text-sm font-medium">{field.label}{#if field.required}<span style="color: var(--accent);"> *</span>{/if}</span>
|
||||
<input
|
||||
type="text"
|
||||
{value}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import NumberField from './NumberField.svelte';
|
||||
import SelectField from './SelectField.svelte';
|
||||
import CheckboxField from './CheckboxField.svelte';
|
||||
import MultiSelectField from './MultiSelectField.svelte';
|
||||
import DateField from './DateField.svelte';
|
||||
import HeightField from './HeightField.svelte';
|
||||
import WeightField from './WeightField.svelte';
|
||||
|
|
@ -33,7 +34,7 @@
|
|||
{:else if field.type === 'select'}
|
||||
<SelectField {field} {value} {onChange} />
|
||||
{:else if field.type === 'multi-select'}
|
||||
<CheckboxField field={{ ...field, type: 'checkbox' }} {value} {onChange} />
|
||||
<MultiSelectField {field} {value} {onChange} />
|
||||
{:else if field.type === 'checkbox'}
|
||||
<CheckboxField {field} {value} {onChange} />
|
||||
{:else if field.type === 'date'}
|
||||
|
|
|
|||
|
|
@ -8,18 +8,18 @@
|
|||
</script>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium">{field.label}</span>
|
||||
<span class="text-sm font-medium">{field.label}{#if field.required}<span style="color: var(--accent);"> *</span>{/if}</span>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<input
|
||||
type="number"
|
||||
value={value ?? ''}
|
||||
min={0}
|
||||
oninput={(e) => onChange(Number((e.target as HTMLInputElement).value))}
|
||||
class="block w-full rounded px-3 py-2 text-sm"
|
||||
class="w-24 rounded px-3 py-2 text-sm"
|
||||
style="background: var(--bg-input); border: 1px solid var(--border); color: var(--text);"
|
||||
/>
|
||||
<span class="text-sm whitespace-nowrap" style="color: var(--text-muted);">
|
||||
cm {converted ? `(${converted})` : ''}
|
||||
<span class="text-sm" style="color: var(--text-muted);">
|
||||
cm{#if converted} ({converted}){/if}
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { X } from 'lucide-svelte';
|
||||
import type { LanguagesField } from '$lib/types';
|
||||
import { languages } from '$lib/data';
|
||||
|
||||
|
|
@ -8,27 +9,89 @@
|
|||
onChange: (v: string[]) => void;
|
||||
} = $props();
|
||||
|
||||
function toggle(name: string) {
|
||||
if (value.includes(name)) {
|
||||
onChange(value.filter((v) => v !== name));
|
||||
} else {
|
||||
onChange([...value, name]);
|
||||
let input = $state('');
|
||||
let open = $state(false);
|
||||
|
||||
let available = $derived(
|
||||
languages
|
||||
.filter((l) => !value.includes(l.name))
|
||||
.filter((l) => !input || l.name.toLowerCase().includes(input.toLowerCase()))
|
||||
);
|
||||
|
||||
function add(name: string) {
|
||||
onChange([...value, name]);
|
||||
input = '';
|
||||
}
|
||||
|
||||
function addCustom() {
|
||||
const trimmed = input.trim();
|
||||
if (trimmed && !value.includes(trimmed)) {
|
||||
onChange([...value, trimmed]);
|
||||
}
|
||||
input = '';
|
||||
}
|
||||
|
||||
function remove(name: string) {
|
||||
onChange(value.filter((v) => v !== name));
|
||||
}
|
||||
</script>
|
||||
|
||||
<fieldset>
|
||||
<legend class="text-sm font-medium">{field.label}</legend>
|
||||
<div class="mt-1 flex flex-col gap-1">
|
||||
{#each languages as lang}
|
||||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value.includes(lang.name)}
|
||||
onchange={() => toggle(lang.name)}
|
||||
/>
|
||||
{lang.name}
|
||||
</label>
|
||||
<div class="block">
|
||||
<span class="text-sm font-medium">{field.label}{#if field.required}<span style="color: var(--accent);"> *</span>{/if}</span>
|
||||
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
{#each value as lang}
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-sm" style="background: var(--border); color: var(--text);">
|
||||
{lang}
|
||||
<button onclick={() => remove(lang)} class="hover:opacity-60">
|
||||
<X size={12} />
|
||||
</button>
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="relative mt-1">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={input}
|
||||
placeholder="Add language..."
|
||||
onfocus={() => { open = true; }}
|
||||
onblur={() => { setTimeout(() => { open = false; }, 150); }}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (available.length) add(available[0].name);
|
||||
else addCustom();
|
||||
}
|
||||
}}
|
||||
class="block w-full rounded px-3 py-2 text-sm"
|
||||
style="background: var(--bg-input); border: 1px solid var(--border); color: var(--text);"
|
||||
/>
|
||||
{#if open && (available.length || input.trim())}
|
||||
<ul class="absolute z-10 w-full mt-1 rounded shadow-lg max-h-48 overflow-y-auto" style="background: var(--bg-card); border: 1px solid var(--border);">
|
||||
{#each available as lang}
|
||||
<li>
|
||||
<button
|
||||
onmousedown={(e) => { e.preventDefault(); add(lang.name); }}
|
||||
class="block w-full text-left px-3 py-1.5 text-sm hover:opacity-80"
|
||||
style="color: var(--text);"
|
||||
>
|
||||
{lang.name}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
{#if input.trim() && !languages.some((l) => l.name.toLowerCase() === input.trim().toLowerCase())}
|
||||
<li>
|
||||
<button
|
||||
onmousedown={(e) => { e.preventDefault(); addCustom(); }}
|
||||
class="block w-full text-left px-3 py-1.5 text-sm"
|
||||
style="color: var(--text-muted);"
|
||||
>
|
||||
Add "{input.trim()}"
|
||||
</button>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
</script>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium">{field.label}</span>
|
||||
<span class="text-sm font-medium">{field.label}{#if field.required}<span style="color: var(--accent);"> *</span>{/if}</span>
|
||||
<textarea
|
||||
{value}
|
||||
placeholder="One entry per line"
|
||||
|
|
|
|||
100
src/lib/components/fields/MultiSelectField.svelte
Normal file
100
src/lib/components/fields/MultiSelectField.svelte
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<script lang="ts">
|
||||
import { X } from 'lucide-svelte';
|
||||
import type { MultiSelectField } from '$lib/types';
|
||||
|
||||
let { field, value = [], onChange }: {
|
||||
field: MultiSelectField;
|
||||
value: string[];
|
||||
onChange: (v: string[]) => void;
|
||||
} = $props();
|
||||
|
||||
let input = $state('');
|
||||
let open = $state(false);
|
||||
|
||||
let available = $derived(
|
||||
field.options
|
||||
.filter((o) => !value.includes(o.value))
|
||||
.filter((o) => !input || o.label.toLowerCase().includes(input.toLowerCase()))
|
||||
);
|
||||
|
||||
function add(val: string) {
|
||||
onChange([...value, val]);
|
||||
input = '';
|
||||
}
|
||||
|
||||
function addCustom() {
|
||||
const trimmed = input.trim();
|
||||
if (trimmed && !value.includes(trimmed)) {
|
||||
onChange([...value, trimmed]);
|
||||
}
|
||||
input = '';
|
||||
}
|
||||
|
||||
function remove(val: string) {
|
||||
onChange(value.filter((v) => v !== val));
|
||||
}
|
||||
|
||||
function displayLabel(val: string): string {
|
||||
return field.options.find((o) => o.value === val)?.label ?? val;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="block">
|
||||
<span class="text-sm font-medium">{field.label}{#if field.required}<span style="color: var(--accent);"> *</span>{/if}</span>
|
||||
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
{#each value as val}
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-sm" style="background: var(--border); color: var(--text);">
|
||||
{displayLabel(val)}
|
||||
<button onclick={() => remove(val)} class="hover:opacity-60">
|
||||
<X size={12} />
|
||||
</button>
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="relative mt-1">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={input}
|
||||
placeholder="Add..."
|
||||
onfocus={() => { open = true; }}
|
||||
onblur={() => { setTimeout(() => { open = false; }, 150); }}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (available.length) add(available[0].value);
|
||||
else addCustom();
|
||||
}
|
||||
}}
|
||||
class="block w-full rounded px-3 py-2 text-sm"
|
||||
style="background: var(--bg-input); border: 1px solid var(--border); color: var(--text);"
|
||||
/>
|
||||
{#if open && (available.length || input.trim())}
|
||||
<ul class="absolute z-10 w-full mt-1 rounded shadow-lg max-h-48 overflow-y-auto" style="background: var(--bg-card); border: 1px solid var(--border);">
|
||||
{#each available as opt}
|
||||
<li>
|
||||
<button
|
||||
onmousedown={(e) => { e.preventDefault(); add(opt.value); }}
|
||||
class="block w-full text-left px-3 py-1.5 text-sm hover:opacity-80"
|
||||
style="color: var(--text);"
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
{#if input.trim() && !field.options.some((o) => o.label.toLowerCase() === input.trim().toLowerCase())}
|
||||
<li>
|
||||
<button
|
||||
onmousedown={(e) => { e.preventDefault(); addCustom(); }}
|
||||
class="block w-full text-left px-3 py-1.5 text-sm"
|
||||
style="color: var(--text-muted);"
|
||||
>
|
||||
Add "{input.trim()}"
|
||||
</button>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
</script>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium">{field.label}</span>
|
||||
<span class="text-sm font-medium">{field.label}{#if field.required}<span style="color: var(--accent);"> *</span>{/if}</span>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<input
|
||||
type="number"
|
||||
|
|
|
|||
|
|
@ -2,19 +2,53 @@
|
|||
import type { SelectField } from '$lib/types';
|
||||
|
||||
let { field, value = '', onChange }: { field: SelectField; value: string; onChange: (v: string) => void } = $props();
|
||||
|
||||
let custom = $state(false);
|
||||
|
||||
function handleSelect(v: string) {
|
||||
if (v === '__custom') {
|
||||
custom = true;
|
||||
onChange('');
|
||||
} else {
|
||||
custom = false;
|
||||
onChange(v);
|
||||
}
|
||||
}
|
||||
|
||||
let isCustom = $derived(custom || (value !== '' && !field.options.some((o) => o.value === value)));
|
||||
</script>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium">{field.label}</span>
|
||||
<select
|
||||
{value}
|
||||
onchange={(e) => onChange((e.target as HTMLSelectElement).value)}
|
||||
class="mt-1 block w-full rounded px-3 py-2 text-sm"
|
||||
style="background: var(--bg-input); border: 1px solid var(--border); color: var(--text);"
|
||||
>
|
||||
<option value="">—</option>
|
||||
{#each field.options as opt}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<span class="text-sm font-medium">{field.label}{#if field.required}<span style="color: var(--accent);"> *</span>{/if}</span>
|
||||
{#if isCustom}
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<input
|
||||
type="text"
|
||||
{value}
|
||||
oninput={(e) => onChange((e.target as HTMLInputElement).value)}
|
||||
class="block w-full rounded px-3 py-2 text-sm"
|
||||
style="background: var(--bg-input); border: 1px solid var(--border); color: var(--text);"
|
||||
/>
|
||||
<button
|
||||
onclick={() => { custom = false; onChange(''); }}
|
||||
class="text-sm whitespace-nowrap hover:opacity-80"
|
||||
style="color: var(--text-muted);"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<select
|
||||
{value}
|
||||
onchange={(e) => handleSelect((e.target as HTMLSelectElement).value)}
|
||||
class="mt-1 block w-full rounded px-3 py-2 text-sm"
|
||||
style="background: var(--bg-input); border: 1px solid var(--border); color: var(--text);"
|
||||
>
|
||||
<option value="">—</option>
|
||||
{#each field.options as opt}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
{/each}
|
||||
<option value="__custom">Other...</option>
|
||||
</select>
|
||||
{/if}
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<div class="block">
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium">{field.label}</span>
|
||||
<span class="text-sm font-medium">{field.label}{#if field.required}<span style="color: var(--accent);"> *</span>{/if}</span>
|
||||
<select
|
||||
{value}
|
||||
onchange={(e) => onChange((e.target as HTMLSelectElement).value)}
|
||||
|
|
|
|||
|
|
@ -14,23 +14,57 @@
|
|||
let subs = $derived(currentSpecies?.subspecies ?? []);
|
||||
let label = $derived(currentSpecies?.subspeciesLabel ?? field.label);
|
||||
let selected = $derived(subs.find((s) => s.id === value));
|
||||
|
||||
let custom = $state(false);
|
||||
|
||||
function handleSelect(v: string) {
|
||||
if (v === '__custom') {
|
||||
custom = true;
|
||||
onChange('');
|
||||
} else {
|
||||
custom = false;
|
||||
onChange(v);
|
||||
}
|
||||
}
|
||||
|
||||
let isCustom = $derived(custom || (value !== '' && !subs.some((s) => s.id === value)));
|
||||
</script>
|
||||
|
||||
{#if subs.length > 0}
|
||||
{#if subs.length > 0 || isCustom}
|
||||
<div class="block">
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium">{label}</span>
|
||||
<select
|
||||
{value}
|
||||
onchange={(e) => onChange((e.target as HTMLSelectElement).value)}
|
||||
class="mt-1 block w-full rounded px-3 py-2 text-sm"
|
||||
style="background: var(--bg-input); border: 1px solid var(--border); color: var(--text);"
|
||||
>
|
||||
<option value="">—</option>
|
||||
{#each subs as sub}
|
||||
<option value={sub.id}>{sub.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<span class="text-sm font-medium">{label}{#if field.required}<span style="color: var(--accent);"> *</span>{/if}</span>
|
||||
{#if isCustom}
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<input
|
||||
type="text"
|
||||
{value}
|
||||
oninput={(e) => onChange((e.target as HTMLInputElement).value)}
|
||||
class="block w-full rounded px-3 py-2 text-sm"
|
||||
style="background: var(--bg-input); border: 1px solid var(--border); color: var(--text);"
|
||||
/>
|
||||
<button
|
||||
onclick={() => { custom = false; onChange(''); }}
|
||||
class="text-sm whitespace-nowrap hover:opacity-80"
|
||||
style="color: var(--text-muted);"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<select
|
||||
{value}
|
||||
onchange={(e) => handleSelect((e.target as HTMLSelectElement).value)}
|
||||
class="mt-1 block w-full rounded px-3 py-2 text-sm"
|
||||
style="background: var(--bg-input); border: 1px solid var(--border); color: var(--text);"
|
||||
>
|
||||
<option value="">—</option>
|
||||
{#each subs as sub}
|
||||
<option value={sub.id}>{sub.name}</option>
|
||||
{/each}
|
||||
<option value="__custom">Other...</option>
|
||||
</select>
|
||||
{/if}
|
||||
</label>
|
||||
{#if selected?.description}
|
||||
<p class="mt-1 text-sm italic" style="color: var(--text-muted);">{selected.description}</p>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
</script>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium">{field.label}</span>
|
||||
<span class="text-sm font-medium">{field.label}{#if field.required}<span style="color: var(--accent);"> *</span>{/if}</span>
|
||||
<input
|
||||
type="text"
|
||||
{value}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
</script>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium">{field.label}</span>
|
||||
<span class="text-sm font-medium">{field.label}{#if field.required}<span style="color: var(--accent);"> *</span>{/if}</span>
|
||||
<textarea
|
||||
{value}
|
||||
placeholder={field.placeholder}
|
||||
|
|
|
|||
|
|
@ -4,22 +4,22 @@
|
|||
|
||||
let { field, value, onChange }: { field: WeightField; value: number | undefined; onChange: (v: number) => void } = $props();
|
||||
|
||||
let converted = $derived(value ? Math.round(kgToLb(value)) : '');
|
||||
let converted = $derived(value ? Math.round(kgToLb(value)) : 0);
|
||||
</script>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium">{field.label}</span>
|
||||
<span class="text-sm font-medium">{field.label}{#if field.required}<span style="color: var(--accent);"> *</span>{/if}</span>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<input
|
||||
type="number"
|
||||
value={value ?? ''}
|
||||
min={0}
|
||||
oninput={(e) => onChange(Number((e.target as HTMLInputElement).value))}
|
||||
class="block w-full rounded px-3 py-2 text-sm"
|
||||
class="w-24 rounded px-3 py-2 text-sm"
|
||||
style="background: var(--bg-input); border: 1px solid var(--border); color: var(--text);"
|
||||
/>
|
||||
<span class="text-sm whitespace-nowrap" style="color: var(--text-muted);">
|
||||
kg {converted ? `(${converted} lb)` : ''}
|
||||
<span class="text-sm" style="color: var(--text-muted);">
|
||||
kg{#if converted} ({converted} lb){/if}
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -62,7 +62,8 @@ function parseOptions(field: any): SelectOption[] {
|
|||
|
||||
function parseField(raw: any): FieldDef {
|
||||
const base = {
|
||||
label: raw['@_label']
|
||||
label: raw['@_label'],
|
||||
...(raw['@_required'] === 'true' && { required: true })
|
||||
};
|
||||
const type = raw['@_type'];
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export interface SelectOption {
|
|||
|
||||
export interface BaseFieldDef {
|
||||
label: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export interface TextField extends BaseFieldDef {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue