diff --git a/data/templates/standard.xml b/data/templates/standard.xml index 3e4e33e..6b10c88 100644 --- a/data/templates/standard.xml +++ b/data/templates/standard.xml @@ -3,10 +3,15 @@ The standard record format used by a majority of players. - - + + - + + + + + + @@ -29,7 +34,7 @@ The following information is protected by doctor-patient confidentiality laws. Do not release without patient's consent. - + diff --git a/src/lib/components/fields/CheckboxField.svelte b/src/lib/components/fields/CheckboxField.svelte index 064707b..8964798 100644 --- a/src/lib/components/fields/CheckboxField.svelte +++ b/src/lib/components/fields/CheckboxField.svelte @@ -1,8 +1,14 @@ - {field.label} + {field.label}{#if field.required} *{/if} {#each field.options as opt} @@ -25,5 +43,31 @@ {opt.label} {/each} + {#each customValues as cv} + + removeCustom(cv)} /> + {cv} + removeCustom(cv)} class="hover:opacity-60"> + + + + {/each} + + + { 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);" + /> + + Add + diff --git a/src/lib/components/fields/CitizenshipField.svelte b/src/lib/components/fields/CitizenshipField.svelte index 56c817d..6647a31 100644 --- a/src/lib/components/fields/CitizenshipField.svelte +++ b/src/lib/components/fields/CitizenshipField.svelte @@ -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))); - {field.label} - 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);" - > - — - {#each citizenships as c} - {c.name} - {/each} - + {field.label}{#if field.required} *{/if} + {#if isCustom} + + 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);" + /> + { custom = false; onChange(''); }} + class="text-sm whitespace-nowrap hover:opacity-80" + style="color: var(--text-muted);" + > + Cancel + + + {:else} + 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);" + > + — + {#each citizenships as c} + {c.name} + {/each} + Other... + + {/if} diff --git a/src/lib/components/fields/DateField.svelte b/src/lib/components/fields/DateField.svelte index ac159f6..aa5002e 100644 --- a/src/lib/components/fields/DateField.svelte +++ b/src/lib/components/fields/DateField.svelte @@ -5,7 +5,7 @@ - {field.label} + {field.label}{#if field.required} *{/if} {:else if field.type === 'multi-select'} - + {:else if field.type === 'checkbox'} {:else if field.type === 'date'} diff --git a/src/lib/components/fields/HeightField.svelte b/src/lib/components/fields/HeightField.svelte index 77fc2e2..a0e308e 100644 --- a/src/lib/components/fields/HeightField.svelte +++ b/src/lib/components/fields/HeightField.svelte @@ -8,18 +8,18 @@ - {field.label} + {field.label}{#if field.required} *{/if} 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);" /> - - cm {converted ? `(${converted})` : ''} + + cm{#if converted} ({converted}){/if} diff --git a/src/lib/components/fields/LanguagesField.svelte b/src/lib/components/fields/LanguagesField.svelte index 5c652f3..3c3f50b 100644 --- a/src/lib/components/fields/LanguagesField.svelte +++ b/src/lib/components/fields/LanguagesField.svelte @@ -1,4 +1,5 @@ - - {field.label} - - {#each languages as lang} - - toggle(lang.name)} - /> - {lang.name} - + + {field.label}{#if field.required} *{/if} + + + {#each value as lang} + + {lang} + remove(lang)} class="hover:opacity-60"> + + + {/each} - + + + { 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())} + + {#each available as lang} + + { 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} + + + {/each} + {#if input.trim() && !languages.some((l) => l.name.toLowerCase() === input.trim().toLowerCase())} + + { e.preventDefault(); addCustom(); }} + class="block w-full text-left px-3 py-1.5 text-sm" + style="color: var(--text-muted);" + > + Add "{input.trim()}" + + + {/if} + + {/if} + + diff --git a/src/lib/components/fields/ListField.svelte b/src/lib/components/fields/ListField.svelte index 2df87c6..4a0e6a0 100644 --- a/src/lib/components/fields/ListField.svelte +++ b/src/lib/components/fields/ListField.svelte @@ -5,7 +5,7 @@ - {field.label} + {field.label}{#if field.required} *{/if} + 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; + } + + + + {field.label}{#if field.required} *{/if} + + + {#each value as val} + + {displayLabel(val)} + remove(val)} class="hover:opacity-60"> + + + + {/each} + + + + { 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())} + + {#each available as opt} + + { 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} + + + {/each} + {#if input.trim() && !field.options.some((o) => o.label.toLowerCase() === input.trim().toLowerCase())} + + { e.preventDefault(); addCustom(); }} + class="block w-full text-left px-3 py-1.5 text-sm" + style="color: var(--text-muted);" + > + Add "{input.trim()}" + + + {/if} + + {/if} + + diff --git a/src/lib/components/fields/NumberField.svelte b/src/lib/components/fields/NumberField.svelte index 099eb1d..3e8986b 100644 --- a/src/lib/components/fields/NumberField.svelte +++ b/src/lib/components/fields/NumberField.svelte @@ -5,7 +5,7 @@ - {field.label} + {field.label}{#if field.required} *{/if} 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))); - {field.label} - 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);" - > - — - {#each field.options as opt} - {opt.label} - {/each} - + {field.label}{#if field.required} *{/if} + {#if isCustom} + + 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);" + /> + { custom = false; onChange(''); }} + class="text-sm whitespace-nowrap hover:opacity-80" + style="color: var(--text-muted);" + > + Cancel + + + {:else} + 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);" + > + — + {#each field.options as opt} + {opt.label} + {/each} + Other... + + {/if} diff --git a/src/lib/components/fields/SpeciesField.svelte b/src/lib/components/fields/SpeciesField.svelte index 71ad060..4cdb979 100644 --- a/src/lib/components/fields/SpeciesField.svelte +++ b/src/lib/components/fields/SpeciesField.svelte @@ -9,7 +9,7 @@ - {field.label} + {field.label}{#if field.required} *{/if} onChange((e.target as HTMLSelectElement).value)} diff --git a/src/lib/components/fields/SubspeciesField.svelte b/src/lib/components/fields/SubspeciesField.svelte index 1ef69ee..d0a36d0 100644 --- a/src/lib/components/fields/SubspeciesField.svelte +++ b/src/lib/components/fields/SubspeciesField.svelte @@ -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))); -{#if subs.length > 0} +{#if subs.length > 0 || isCustom} - {label} - 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);" - > - — - {#each subs as sub} - {sub.name} - {/each} - + {label}{#if field.required} *{/if} + {#if isCustom} + + 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);" + /> + { custom = false; onChange(''); }} + class="text-sm whitespace-nowrap hover:opacity-80" + style="color: var(--text-muted);" + > + Cancel + + + {:else} + 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);" + > + — + {#each subs as sub} + {sub.name} + {/each} + Other... + + {/if} {#if selected?.description} {selected.description} diff --git a/src/lib/components/fields/TextField.svelte b/src/lib/components/fields/TextField.svelte index 20abf12..f10a138 100644 --- a/src/lib/components/fields/TextField.svelte +++ b/src/lib/components/fields/TextField.svelte @@ -5,7 +5,7 @@ - {field.label} + {field.label}{#if field.required} *{/if} - {field.label} + {field.label}{#if field.required} *{/if} void } = $props(); - let converted = $derived(value ? Math.round(kgToLb(value)) : ''); + let converted = $derived(value ? Math.round(kgToLb(value)) : 0); - {field.label} + {field.label}{#if field.required} *{/if} 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);" /> - - kg {converted ? `(${converted} lb)` : ''} + + kg{#if converted} ({converted} lb){/if} diff --git a/src/lib/data/parse.ts b/src/lib/data/parse.ts index 8ef3ddc..426979f 100644 --- a/src/lib/data/parse.ts +++ b/src/lib/data/parse.ts @@ -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']; diff --git a/src/lib/types.ts b/src/lib/types.ts index b0852cd..5d3d142 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -5,6 +5,7 @@ export interface SelectOption { export interface BaseFieldDef { label: string; + required?: boolean; } export interface TextField extends BaseFieldDef {
{selected.description}