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>
@@ -1,7 +0,0 @@
<script lang="ts">
let { children } = $props();
</script>
<div class="flex flex-1 flex-col items-center justify-center">
{@render children()}
</div>
-72
View File
@@ -1,72 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
let { initialContent = '', name = 'content' } = $props<{ initialContent: string; name?: string }>();
let element: HTMLElement;
let editor: Editor;
let content = $state('');
onMount(() => {
content = initialContent;
editor = new Editor({
element,
extensions: [StarterKit],
content: initialContent,
editorProps: {
attributes: {
class: 'outline-none h-full'
}
},
onUpdate: ({ editor }) => {
content = editor.getHTML();
}
});
return () => editor.destroy();
});
</script>
<div class="border border-gray-300 rounded-lg overflow-hidden">
<div class="flex gap-1 p-2 bg-gray-100 border-b border-gray-300">
<button
type="button"
onclick={() => editor.chain().focus().toggleBold().run()}
class="px-3 py-1 rounded font-bold hover:bg-gray-200 transition-colors"
>B</button>
<button
type="button"
onclick={() => editor.chain().focus().toggleItalic().run()}
class="px-3 py-1 rounded italic hover:bg-gray-200 transition-colors"
>I</button>
<button
type="button"
onclick={() => editor.chain().focus().toggleStrike().run()}
class="px-3 py-1 rounded line-through hover:bg-gray-200 transition-colors"
>S</button>
<div class="w-px bg-gray-300 mx-1"></div>
<button
type="button"
onclick={() => editor.chain().focus().toggleBulletList().run()}
class="px-3 py-1 rounded hover:bg-gray-200 transition-colors"
>• List</button>
<button
type="button"
onclick={() => editor.chain().focus().toggleOrderedList().run()}
class="px-3 py-1 rounded hover:bg-gray-200 transition-colors"
>1. List</button>
</div>
<div
bind:this={element}
role="textbox"
tabindex="-1"
class="p-3 min-h-40 prose prose-sm max-w-none cursor-text [&_.tiptap]:outline-none [&_.tiptap]:h-full [&_.tiptap_p]:my-1 [&_.tiptap_p:first-child]:mt-0 [&_.tiptap_p:last-child]:mb-0"
onmousedown={() => editor?.commands.focus()}
></div>
</div>
<input type="hidden" {name} value={content} />
@@ -1,50 +0,0 @@
<script lang="ts">
import { authClient } from '$lib/auth-client';
import { canAccessAdmin } from '$lib/svelte-admin/roles';
import { onMount } from 'svelte';
let { userRoleId = null }: { userRoleId: number | null } = $props();
const session = authClient.useSession();
let isAdmin = $derived(canAccessAdmin(userRoleId));
let dark = $state(false);
onMount(() => {
dark = document.documentElement.classList.contains('dark');
});
function toggleTheme() {
dark = !dark;
if (dark) {
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
}
}
</script>
<nav class="flex w-full items-center justify-between border-b border-gray-200 px-8 py-6 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-100">
<span><a href="/" class="dark:text-gray-100">Rhema Wave Technologies</a></span>
<div class="flex items-center gap-4">
<button onclick={toggleTheme} class="border border-gray-300 px-3 py-1 text-sm dark:border-gray-600 dark:text-gray-300">
{dark ? 'Light' : 'Dark'}
</button>
{#if $session.data}
{#if isAdmin}
<a href="/admin" class="dark:text-gray-300 dark:hover:text-gray-100">Admin Panel</a>
{/if}
<a href="/dashboard" class="dark:text-gray-300 dark:hover:text-gray-100">Dashboard</a>
<button
class="dark:text-gray-300 dark:hover:text-gray-100"
onclick={async () => {
await authClient.signOut();
window.location.href = '/';
}}>Log Out</button
>
{:else}
<span class="dark:text-gray-300"><a href="/login" class="dark:text-gray-300 dark:hover:text-gray-100">Log In</a> | <a href="/register" class="dark:text-gray-300 dark:hover:text-gray-100">Register</a></span>
{/if}
</div>
</nav>
+46 -2
View File
@@ -1,9 +1,31 @@
<script lang="ts">
import { onMount } from 'svelte';
import { authClient } from '$lib/auth-client';
import { canAccessAdmin } from '$lib/svelte-admin/roles';
import './layout.css';
import favicon from '$lib/assets/favicon.svg';
import TopBar from '$lib/components/ui/topBar/TopBar.svelte';
let { data, children } = $props();
const session = authClient.useSession();
let isAdmin = $derived(canAccessAdmin(data.userRoleId));
let dark = $state(false);
onMount(() => {
dark = document.documentElement.classList.contains('dark');
});
function toggleTheme() {
dark = !dark;
if (dark) {
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
}
}
</script>
<svelte:head>
@@ -18,7 +40,29 @@
<meta name="twitter:card" content="summary" />
</svelte:head>
<TopBar userRoleId={data.userRoleId} />
<nav class="flex w-full items-center justify-between border-b border-gray-200 px-8 py-6 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-100">
<span><a href="/" class="dark:text-gray-100">Rhema Wave Technologies</a></span>
<div class="flex items-center gap-4">
<button onclick={toggleTheme} class="border border-gray-300 px-3 py-1 text-sm dark:border-gray-600 dark:text-gray-300">
{dark ? 'Light' : 'Dark'}
</button>
{#if $session.data}
{#if isAdmin}
<a href="/admin" class="dark:text-gray-300 dark:hover:text-gray-100">Admin Panel</a>
{/if}
<a href="/dashboard" class="dark:text-gray-300 dark:hover:text-gray-100">Dashboard</a>
<button
class="dark:text-gray-300 dark:hover:text-gray-100"
onclick={async () => {
await authClient.signOut();
window.location.href = '/';
}}>Log Out</button
>
{:else}
<span class="dark:text-gray-300"><a href="/login" class="dark:text-gray-300 dark:hover:text-gray-100">Log In</a> | <a href="/register" class="dark:text-gray-300 dark:hover:text-gray-100">Register</a></span>
{/if}
</div>
</nav>
<div class="flex min-h-0 flex-1 px-8 dark:bg-gray-950">
{@render children()}
</div>
+2 -6
View File
@@ -1,7 +1,3 @@
<script lang="ts">
import Center from '$lib/components/containers/Center.svelte';
</script>
<Center>
<div class="flex flex-1 flex-col items-center justify-center">
<h1 class="dark:text-gray-100">Hello</h1>
</Center>
</div>