refactor: inline single-use components, remove dead code
- Inline TopBar into root layout (+layout.svelte) — only used there - Inline Center into home page (+page.svelte) — only used there - Remove ComboBox.svelte — dead code, no imports - Remove TipTap.svelte — dead code, no imports - Remove empty container/ui/admin/tiptap directories - Keep Window.svelte in components/containers/ — used by 4 pages
This commit is contained in:
@@ -1,160 +0,0 @@
|
||||
<script lang="ts">
|
||||
type Option = { label: string; value: string | number };
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
name: string;
|
||||
options: Option[];
|
||||
placeholder?: string;
|
||||
value?: string | number;
|
||||
};
|
||||
|
||||
let { id, name, options, placeholder = 'Search...', value }: Props = $props();
|
||||
|
||||
let search = $state('');
|
||||
let open = $state(false);
|
||||
let highlightIndex = $state(-1);
|
||||
let inputEl = $state<HTMLInputElement | null>(null);
|
||||
let listEl = $state<HTMLUListElement | null>(null);
|
||||
|
||||
let selectedValue = $state<string | number | undefined>(undefined);
|
||||
|
||||
$effect(() => {
|
||||
if (value != null && value !== '') {
|
||||
selectedValue = value;
|
||||
}
|
||||
});
|
||||
|
||||
let selectedLabel = $derived(() => {
|
||||
if (selectedValue == null || selectedValue === '') return '';
|
||||
const match = options.find((o) => String(o.value) === String(selectedValue));
|
||||
return match?.label ?? '';
|
||||
});
|
||||
|
||||
let filtered = $derived(() => {
|
||||
if (!search) return options;
|
||||
const q = search.toLowerCase();
|
||||
return options.filter((o) => o.label.toLowerCase().includes(q));
|
||||
});
|
||||
|
||||
function select(opt: Option) {
|
||||
selectedValue = opt.value;
|
||||
search = '';
|
||||
open = false;
|
||||
highlightIndex = -1;
|
||||
}
|
||||
|
||||
function clear() {
|
||||
selectedValue = undefined;
|
||||
search = '';
|
||||
open = true;
|
||||
inputEl?.focus();
|
||||
}
|
||||
|
||||
function handleInputFocus() {
|
||||
open = true;
|
||||
highlightIndex = -1;
|
||||
}
|
||||
|
||||
function handleInputBlur(e: FocusEvent) {
|
||||
const related = e.relatedTarget as HTMLElement | null;
|
||||
if (related?.closest(`[data-combobox="${id}"]`)) return;
|
||||
open = false;
|
||||
highlightIndex = -1;
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
const items = filtered();
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
open = true;
|
||||
highlightIndex = Math.min(highlightIndex + 1, items.length - 1);
|
||||
scrollToHighlighted();
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
highlightIndex = Math.max(highlightIndex - 1, 0);
|
||||
scrollToHighlighted();
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (open && highlightIndex >= 0 && highlightIndex < items.length) {
|
||||
select(items[highlightIndex]);
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
open = false;
|
||||
search = '';
|
||||
highlightIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToHighlighted() {
|
||||
requestAnimationFrame(() => {
|
||||
const el = listEl?.children[highlightIndex] as HTMLElement | undefined;
|
||||
el?.scrollIntoView({ block: 'nearest' });
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative" data-combobox={id}>
|
||||
<input type="hidden" {name} value={selectedValue ?? ''} />
|
||||
<input type="hidden" name={name + 'Label'} value={selectedValue != null && selectedValue !== '' ? selectedLabel() : search} />
|
||||
|
||||
{#if selectedValue != null && selectedValue !== ''}
|
||||
<div
|
||||
class="flex w-full items-center justify-between rounded-lg border border-gray-300 bg-white px-3 py-2 transition-shadow outline-none focus-within:border-blue-500 focus-within:ring-2 focus-within:ring-blue-500"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 cursor-pointer text-left text-gray-900"
|
||||
onclick={() => clear()}
|
||||
>
|
||||
{selectedLabel()}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-2 shrink-0 text-gray-400 hover:text-gray-600"
|
||||
onclick={() => clear()}
|
||||
tabindex={-1}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<input
|
||||
type="text"
|
||||
{id}
|
||||
bind:this={inputEl}
|
||||
bind:value={search}
|
||||
{placeholder}
|
||||
autocomplete="off"
|
||||
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 transition-shadow outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
|
||||
onfocus={handleInputFocus}
|
||||
onblur={handleInputBlur}
|
||||
onkeydown={handleKeydown}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if open}
|
||||
<ul
|
||||
bind:this={listEl}
|
||||
class="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-lg border border-gray-200 bg-white py-1 shadow-lg"
|
||||
role="listbox"
|
||||
>
|
||||
{#each filtered() as opt, i}
|
||||
<li role="option" aria-selected={highlightIndex === i}>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-3 py-2 text-left text-sm {highlightIndex === i
|
||||
? 'bg-blue-50 text-blue-700'
|
||||
: 'text-gray-700 hover:bg-gray-50'}"
|
||||
onmousedown={(e: MouseEvent) => { e.preventDefault(); select(opt); }}
|
||||
onmouseenter={() => (highlightIndex = i)}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
</li>
|
||||
{:else}
|
||||
<li class="px-3 py-2 text-sm text-gray-400">No matches</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user