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:
NeilClack
2026-07-07 10:40:59 -05:00
parent b87e2b0936
commit 27aae18b3a
6 changed files with 48 additions and 297 deletions
-160
View File
@@ -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}
>
&#10005;
</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>