feat(fend): skeleton components

This commit is contained in:
Lewis Wynne 2026-03-23 18:51:48 +00:00
parent 3c6a31f86b
commit 608d863c88
18 changed files with 395 additions and 77 deletions

View file

@ -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>