feat(fend): skeleton components
This commit is contained in:
parent
3c6a31f86b
commit
608d863c88
18 changed files with 395 additions and 77 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue