733 lines
18 KiB
Svelte
733 lines
18 KiB
Svelte
<script lang="ts">
|
|
import { onMount, tick, onDestroy } from "svelte";
|
|
import { goto } from "$app/navigation";
|
|
import { commandBarOpen } from "$lib/stores/commandBar";
|
|
import { allActiveContexts } from "$lib/stores/agenda";
|
|
import { allPages } from "$lib/stores/wiki";
|
|
import { currentScope, scopeSettings } from "$lib/stores/scopeContext";
|
|
import { sidebarCollapsed } from "$lib/stores/sidebarCollapsed";
|
|
import {
|
|
getOrCreateJournalTopic,
|
|
createHistoryEntry,
|
|
createPage,
|
|
upsertContext,
|
|
contextNameExists,
|
|
pageNameExists,
|
|
createEvent,
|
|
} from "$lib/db/repositories";
|
|
import { newId, today } from "$lib/db/helpers";
|
|
import { authFetch } from "$lib/auth/apiClient";
|
|
import { searchResultsLimit } from "$lib/stores/settings";
|
|
|
|
const contextsQuery = allActiveContexts();
|
|
const pagesQuery = allPages();
|
|
|
|
let query = $state("");
|
|
let inputEl = $state<HTMLInputElement>();
|
|
let selectedIndex = $state(0);
|
|
let recentContextIds = $state<string[]>([]);
|
|
let isMac = $state(false);
|
|
|
|
// Server FTS results
|
|
interface ServerResult {
|
|
id: string;
|
|
type: "nav-history" | "nav-wiki";
|
|
icon: string;
|
|
label: string;
|
|
badge: string;
|
|
action: () => void;
|
|
}
|
|
let serverResults = $state<ServerResult[]>([]);
|
|
let isOffline = $state(false);
|
|
let searchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
$effect(() => {
|
|
const q = query.trim();
|
|
if (q.length < 2 || q.startsWith("/")) {
|
|
serverResults = [];
|
|
isOffline = false;
|
|
if (searchTimer) clearTimeout(searchTimer);
|
|
return;
|
|
}
|
|
if (searchTimer) clearTimeout(searchTimer);
|
|
searchTimer = setTimeout(async () => {
|
|
try {
|
|
const limit = $searchResultsLimit;
|
|
const res = await authFetch(`/api/search?q=${encodeURIComponent(q)}&limit=${limit}`);
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
const data = await res.json() as {
|
|
history: { id: string; topicId: string; contextId: string; date: string; snippet: string }[];
|
|
pages: { id: string; title: string; snippet: string }[];
|
|
};
|
|
const localPageIds = new Set(
|
|
($pagesQuery ?? [])
|
|
.filter((p) => p.title.toLowerCase().includes(q.toLowerCase()))
|
|
.map((p) => p.id),
|
|
);
|
|
const combined: ServerResult[] = [
|
|
...data.history.map((h) => {
|
|
const isJournal = h.topicId === 'daily-log-journal';
|
|
const targetContextId = isJournal ? 'daily-log' : h.contextId;
|
|
return {
|
|
id: `hist-${h.id}`,
|
|
type: "nav-history" as const,
|
|
icon: isJournal ? "📓" : "📋",
|
|
label: h.snippet.replace(/<[^>]+>/g, ""),
|
|
badge: isJournal ? `JOURNAL ${h.date}` : `MEETING ${h.date}`,
|
|
action: () => {
|
|
closeBar();
|
|
goto(`/context/${targetContextId}?date=${h.date}`);
|
|
},
|
|
};
|
|
}),
|
|
...data.pages
|
|
.filter((p) => !localPageIds.has(p.id))
|
|
.map((p) => ({
|
|
id: `page-fts-${p.id}`,
|
|
type: "nav-wiki" as const,
|
|
icon: "📄",
|
|
label: p.title,
|
|
badge: "WIKI",
|
|
action: () => {
|
|
closeBar();
|
|
goto(`/wiki/${p.id}`);
|
|
},
|
|
})),
|
|
];
|
|
serverResults = combined;
|
|
isOffline = false;
|
|
} catch {
|
|
isOffline = true;
|
|
serverResults = [];
|
|
}
|
|
}, 250);
|
|
});
|
|
|
|
onMount(() => {
|
|
isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
|
});
|
|
|
|
// Subscribe to the store to handle focus
|
|
$effect(() => {
|
|
if ($commandBarOpen) {
|
|
query = "";
|
|
selectedIndex = 0;
|
|
loadRecentContexts();
|
|
tick().then(() => inputEl?.focus());
|
|
// Prevent body scroll when open
|
|
document.body.style.overflow = "hidden";
|
|
} else {
|
|
document.body.style.overflow = "";
|
|
}
|
|
});
|
|
|
|
onDestroy(() => {
|
|
if (typeof document !== "undefined") {
|
|
document.body.style.overflow = "";
|
|
}
|
|
});
|
|
|
|
function loadRecentContexts() {
|
|
try {
|
|
const stored = sessionStorage.getItem("recentContexts");
|
|
if (stored) {
|
|
recentContextIds = JSON.parse(stored);
|
|
} else {
|
|
recentContextIds = [];
|
|
}
|
|
} catch {
|
|
recentContextIds = [];
|
|
}
|
|
}
|
|
|
|
function getContextIcon(type: string) {
|
|
switch (type) {
|
|
case "meeting":
|
|
return "📅";
|
|
case "project":
|
|
return "📋";
|
|
case "person":
|
|
return "👤";
|
|
case "company":
|
|
return "🏢";
|
|
default:
|
|
return "📁";
|
|
}
|
|
}
|
|
|
|
function getSubtype(type: string): string {
|
|
switch (type) {
|
|
case "meeting":
|
|
return "MEETING";
|
|
case "project":
|
|
return "PROJEKT";
|
|
case "person":
|
|
return "PERSON";
|
|
case "company":
|
|
return "FIRMA";
|
|
default:
|
|
return "KONTEXT";
|
|
}
|
|
}
|
|
|
|
function promptIfEmpty(current: string, label: string): string {
|
|
if (current) return current;
|
|
return window.prompt(`${label}:`) ?? "";
|
|
}
|
|
|
|
const results = $derived.by(() => {
|
|
const q = query.trim().toLowerCase();
|
|
|
|
if (q.startsWith("/")) {
|
|
// Action mode
|
|
const parts = q.split(" ");
|
|
const cmd = parts[0];
|
|
const text = parts.slice(1).join(" ").trim();
|
|
|
|
const actions = [];
|
|
|
|
// Show scope-dynamic /note only when not narrowed to /pnote or /bnote
|
|
if ("/note".startsWith(cmd) && !"/pnote".startsWith(cmd) && !"/bnote".startsWith(cmd)) {
|
|
actions.push({
|
|
id: "cmd-note",
|
|
type: "action",
|
|
icon: "📝",
|
|
label: text
|
|
? `Notiz (${$currentScope === "private" ? "Privat" : "Firma"}): "${text}"`
|
|
: `Neu: Notiz (${$currentScope === "private" ? "Privat" : "Firma"})`,
|
|
badge: "BEFEHL",
|
|
action: async () => {
|
|
const t = promptIfEmpty(text, "Titel der Notiz");
|
|
if (!t) return;
|
|
const isPrivateScope = $currentScope === "private";
|
|
await executeNoteCommand(t, isPrivateScope);
|
|
},
|
|
});
|
|
}
|
|
|
|
if ("/pnote".startsWith(cmd) && cmd.length > 1) {
|
|
actions.push({
|
|
id: "cmd-pnote",
|
|
type: "action",
|
|
icon: "📝",
|
|
label: text
|
|
? `Notiz (Privat): "${text}"`
|
|
: "Neu: Notiz (Privat)",
|
|
badge: "BEFEHL",
|
|
action: async () => {
|
|
const t = promptIfEmpty(text, "Titel der Notiz");
|
|
if (!t) return;
|
|
await executeNoteCommand(t, true);
|
|
},
|
|
});
|
|
}
|
|
|
|
if ("/bnote".startsWith(cmd) && cmd.length > 1) {
|
|
actions.push({
|
|
id: "cmd-bnote",
|
|
type: "action",
|
|
icon: "📝",
|
|
label: text
|
|
? `Notiz (Firma): "${text}"`
|
|
: "Neu: Notiz (Firma)",
|
|
badge: "BEFEHL",
|
|
action: async () => {
|
|
const t = promptIfEmpty(text, "Titel der Notiz");
|
|
if (!t) return;
|
|
await executeNoteCommand(t, false);
|
|
},
|
|
});
|
|
}
|
|
|
|
// Show scope-dynamic /todo only when not narrowed to /ptodo or /btodo
|
|
if ("/todo".startsWith(cmd) && !"/ptodo".startsWith(cmd) && !"/btodo".startsWith(cmd)) {
|
|
actions.push({
|
|
id: "cmd-todo",
|
|
type: "action",
|
|
icon: "⏰",
|
|
label: text
|
|
? `Todo (${$currentScope === "private" ? "Privat" : "Firma"}): "${text}"`
|
|
: `Neu: Todo (${$currentScope === "private" ? "Privat" : "Firma"})`,
|
|
badge: "BEFEHL",
|
|
action: async () => {
|
|
const t = promptIfEmpty(text, "Titel des Todos");
|
|
if (!t) return;
|
|
const isPrivateScope = $currentScope === "private";
|
|
await executeTodoCommand(t, isPrivateScope);
|
|
},
|
|
});
|
|
}
|
|
|
|
if ("/ptodo".startsWith(cmd) && cmd.length > 1) {
|
|
actions.push({
|
|
id: "cmd-ptodo",
|
|
type: "action",
|
|
icon: "⏰",
|
|
label: text
|
|
? `Todo (Privat): "${text}"`
|
|
: "Neu: Todo (Privat)",
|
|
badge: "BEFEHL",
|
|
action: async () => {
|
|
const t = promptIfEmpty(text, "Titel des Todos");
|
|
if (!t) return;
|
|
await executeTodoCommand(t, true);
|
|
},
|
|
});
|
|
}
|
|
|
|
if ("/btodo".startsWith(cmd) && cmd.length > 1) {
|
|
actions.push({
|
|
id: "cmd-btodo",
|
|
type: "action",
|
|
icon: "⏰",
|
|
label: text
|
|
? `Todo (Firma): "${text}"`
|
|
: "Neu: Todo (Firma)",
|
|
badge: "BEFEHL",
|
|
action: async () => {
|
|
const t = promptIfEmpty(text, "Titel des Todos");
|
|
if (!t) return;
|
|
await executeTodoCommand(t, false);
|
|
},
|
|
});
|
|
}
|
|
|
|
if ("/page".startsWith(cmd) && !"/ppage".startsWith(cmd) && !"/bpage".startsWith(cmd)) {
|
|
actions.push({
|
|
id: "cmd-page",
|
|
type: "action",
|
|
icon: "📄",
|
|
label: text
|
|
? `Wiki-Seite (${$currentScope === "private" ? "Privat" : "Firma"}): "${text}"`
|
|
: `Neu: Wiki-Seite (${$currentScope === "private" ? "Privat" : "Firma"})`,
|
|
badge: "BEFEHL",
|
|
action: async () => {
|
|
const t = promptIfEmpty(text, "Titel der Wiki-Seite");
|
|
if (!t) return;
|
|
if (await pageNameExists(t)) {
|
|
alert(`Wiki-Seite "${t}" existiert bereits.`);
|
|
return;
|
|
}
|
|
const isPrivateScope = $currentScope === "private";
|
|
const page = await createPage(t, isPrivateScope);
|
|
closeBar();
|
|
goto(`/wiki/${page.id}`);
|
|
},
|
|
});
|
|
}
|
|
|
|
if ("/ppage".startsWith(cmd) && cmd.length > 1) {
|
|
actions.push({
|
|
id: "cmd-ppage",
|
|
type: "action",
|
|
icon: "📄",
|
|
label: text
|
|
? `Wiki-Seite (Privat): "${text}"`
|
|
: `Neu: Wiki-Seite (Privat)`,
|
|
badge: "BEFEHL",
|
|
action: async () => {
|
|
const t = promptIfEmpty(text, "Titel der Wiki-Seite");
|
|
if (!t) return;
|
|
if (await pageNameExists(t)) {
|
|
alert(`Wiki-Seite "${t}" existiert bereits.`);
|
|
return;
|
|
}
|
|
const page = await createPage(t, true);
|
|
closeBar();
|
|
goto(`/wiki/${page.id}`);
|
|
},
|
|
});
|
|
}
|
|
|
|
if ("/bpage".startsWith(cmd) && cmd.length > 1) {
|
|
actions.push({
|
|
id: "cmd-bpage",
|
|
type: "action",
|
|
icon: "📄",
|
|
label: text
|
|
? `Wiki-Seite (Firma): "${text}"`
|
|
: `Neu: Wiki-Seite (Firma)`,
|
|
badge: "BEFEHL",
|
|
action: async () => {
|
|
const t = promptIfEmpty(text, "Titel der Wiki-Seite");
|
|
if (!t) return;
|
|
if (await pageNameExists(t)) {
|
|
alert(`Wiki-Seite "${t}" existiert bereits.`);
|
|
return;
|
|
}
|
|
const page = await createPage(t, false);
|
|
closeBar();
|
|
goto(`/wiki/${page.id}`);
|
|
},
|
|
});
|
|
}
|
|
|
|
if ("/person".startsWith(cmd)) {
|
|
actions.push({
|
|
id: "cmd-person",
|
|
type: "action",
|
|
icon: "👤",
|
|
label: text ? `Person: "${text}"` : `Neu: Person`,
|
|
badge: "BEFEHL",
|
|
action: async () => {
|
|
const raw = promptIfEmpty(text, "Name der Person");
|
|
if (!raw) return;
|
|
const displayName = raw.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
const fullName = `Person ${displayName}`;
|
|
if (await contextNameExists(fullName, "person")) {
|
|
alert(`Person "${displayName}" existiert bereits.`);
|
|
return;
|
|
}
|
|
const id = newId();
|
|
await upsertContext({ id, name: fullName, type: "person", meta: { fullName: displayName, email: "", phone: "", duSince: "", personSubType: "colleague" } });
|
|
closeBar();
|
|
goto(`/context/${id}`);
|
|
},
|
|
});
|
|
}
|
|
|
|
if ("/firma".startsWith(cmd)) {
|
|
actions.push({
|
|
id: "cmd-firma",
|
|
type: "action",
|
|
icon: "🏢",
|
|
label: text ? `Firma: "${text}"` : `Neu: Firma`,
|
|
badge: "BEFEHL",
|
|
action: async () => {
|
|
const t = promptIfEmpty(text, "Name der Firma");
|
|
if (!t) return;
|
|
const fullName = `Firma ${t}`;
|
|
if (await contextNameExists(fullName, "company")) {
|
|
alert(`Firma "${t}" existiert bereits.`);
|
|
return;
|
|
}
|
|
const id = newId();
|
|
await upsertContext({ id, name: fullName, type: "company" });
|
|
closeBar();
|
|
goto(`/context/${id}`);
|
|
},
|
|
});
|
|
}
|
|
if ("/sidebar".startsWith(cmd)) {
|
|
actions.push({
|
|
id: "cmd-sidebar",
|
|
type: "action",
|
|
icon: $sidebarCollapsed ? "▶" : "◀",
|
|
label: $sidebarCollapsed ? "Sidebar einblenden" : "Sidebar ausblenden",
|
|
badge: "BEFEHL",
|
|
action: () => {
|
|
sidebarCollapsed.toggle();
|
|
closeBar();
|
|
},
|
|
});
|
|
}
|
|
|
|
if ("/home".startsWith(cmd)) {
|
|
actions.push({
|
|
id: "cmd-home",
|
|
type: "action",
|
|
icon: "🏠",
|
|
label: "Journal (Startseite)",
|
|
badge: "BEFEHL",
|
|
action: () => {
|
|
closeBar();
|
|
goto("/");
|
|
},
|
|
});
|
|
}
|
|
|
|
if ("/help".startsWith(cmd)) {
|
|
actions.push({
|
|
id: "cmd-help",
|
|
type: "action",
|
|
icon: "❓",
|
|
label: "Hilfe & Befehle anzeigen",
|
|
badge: "BEFEHL",
|
|
action: () => {
|
|
closeBar();
|
|
goto("/help");
|
|
},
|
|
});
|
|
}
|
|
|
|
if ("/jf".startsWith(cmd)) {
|
|
actions.push({
|
|
id: "cmd-jf",
|
|
type: "action",
|
|
icon: "📅",
|
|
label: "Zu Jour-Fix springen...",
|
|
badge: "BEFEHL",
|
|
action: () => {}, // Managed by the next items
|
|
});
|
|
|
|
// Add matching JFs
|
|
if (text && $contextsQuery) {
|
|
const jfs = $contextsQuery.filter(
|
|
(c) =>
|
|
c.type === "meeting" &&
|
|
c.name.toLowerCase().includes(text),
|
|
);
|
|
jfs.forEach((jf) => {
|
|
actions.push({
|
|
id: `jf-${jf.id}`,
|
|
type: "nav-context",
|
|
icon: "📅",
|
|
label: jf.name,
|
|
badge: "MEETING",
|
|
action: () => {
|
|
closeBar();
|
|
goto(`/context/${jf.id}`);
|
|
},
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
if ("/meeting".startsWith(cmd)) {
|
|
actions.push({
|
|
id: "cmd-meeting",
|
|
type: "action",
|
|
icon: "🤝",
|
|
label: text ? `Meeting: "${text}"` : `Neu: Meeting (Titel eingeben)`,
|
|
badge: "BEFEHL",
|
|
action: async () => {
|
|
const title = promptIfEmpty(text, "Meeting-Titel");
|
|
if (!title) return;
|
|
await createEvent(today(), "00:00", title, []);
|
|
closeBar();
|
|
goto("/");
|
|
},
|
|
});
|
|
}
|
|
|
|
return actions;
|
|
}
|
|
|
|
if (!q) {
|
|
// Recent mode
|
|
const recents = [];
|
|
if ($contextsQuery) {
|
|
for (const id of recentContextIds) {
|
|
const ctx = $contextsQuery.find((c) => c.id === id);
|
|
if (ctx) {
|
|
recents.push({
|
|
id: `ctx-${ctx.id}`,
|
|
type: "nav-context",
|
|
icon: getContextIcon(ctx.type),
|
|
label: ctx.name,
|
|
badge: getSubtype(ctx.type),
|
|
action: () => {
|
|
closeBar();
|
|
goto(`/context/${ctx.id}`);
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return recents.slice(0, 5);
|
|
}
|
|
|
|
// Navigate mode: Search contexts and wiki pages
|
|
const searchResults = [];
|
|
let ctxCount = 0;
|
|
if ($contextsQuery) {
|
|
for (const ctx of $contextsQuery) {
|
|
if (ctx.name.toLowerCase().includes(q) && ctxCount < 3) {
|
|
searchResults.push({
|
|
id: `ctx-${ctx.id}`,
|
|
type: "nav-context",
|
|
icon: getContextIcon(ctx.type),
|
|
label: ctx.name,
|
|
badge: getSubtype(ctx.type),
|
|
action: () => {
|
|
closeBar();
|
|
goto(`/context/${ctx.id}`);
|
|
},
|
|
});
|
|
ctxCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
let pageCount = 0;
|
|
if ($pagesQuery) {
|
|
for (const p of $pagesQuery) {
|
|
if (p.title.toLowerCase().includes(q) && pageCount < 3) {
|
|
searchResults.push({
|
|
id: `page-${p.id}`,
|
|
type: "nav-wiki",
|
|
icon: "📄",
|
|
label: p.title,
|
|
badge: "WIKI",
|
|
action: () => {
|
|
closeBar();
|
|
goto(`/wiki/${p.id}`);
|
|
},
|
|
});
|
|
pageCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return [...searchResults, ...serverResults].slice(0, 10);
|
|
});
|
|
|
|
// Reset selection when query changes
|
|
$effect(() => {
|
|
query; // reference to trigger
|
|
selectedIndex = 0;
|
|
});
|
|
|
|
function closeBar() {
|
|
$commandBarOpen = false;
|
|
}
|
|
|
|
function handleKeydown(e: KeyboardEvent) {
|
|
if (e.key === "Escape") {
|
|
e.preventDefault();
|
|
closeBar();
|
|
return;
|
|
}
|
|
|
|
if (!results || results.length === 0) return;
|
|
|
|
if (e.key === "ArrowDown") {
|
|
e.preventDefault();
|
|
selectedIndex = (selectedIndex + 1) % results.length;
|
|
ensureVisible();
|
|
} else if (e.key === "ArrowUp") {
|
|
e.preventDefault();
|
|
selectedIndex =
|
|
(selectedIndex - 1 + results.length) % results.length;
|
|
ensureVisible();
|
|
} else if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
const selected = results[selectedIndex];
|
|
if (selected && selected.action) {
|
|
selected.action();
|
|
}
|
|
}
|
|
}
|
|
|
|
function ensureVisible() {
|
|
tick().then(() => {
|
|
const el = document.getElementById(`kano-item-${selectedIndex}`);
|
|
if (el) el.scrollIntoView({ block: "nearest" });
|
|
});
|
|
}
|
|
|
|
async function executeNoteCommand(text: string, isPrivate: boolean) {
|
|
const topic = await getOrCreateJournalTopic();
|
|
await createHistoryEntry(
|
|
topic.id,
|
|
today(),
|
|
text,
|
|
null,
|
|
false,
|
|
isPrivate,
|
|
);
|
|
closeBar();
|
|
if (window.location.pathname === "/context/daily-log") {
|
|
// Trigger a reload or just let dexie liveQuery handle it
|
|
} else {
|
|
goto("/context/daily-log");
|
|
}
|
|
}
|
|
|
|
async function executeTodoCommand(text: string, isPrivate: boolean) {
|
|
const topic = await getOrCreateJournalTopic();
|
|
await createHistoryEntry(
|
|
topic.id,
|
|
today(),
|
|
text,
|
|
null,
|
|
true,
|
|
isPrivate,
|
|
);
|
|
closeBar();
|
|
if (window.location.pathname !== "/context/daily-log") {
|
|
goto("/context/daily-log");
|
|
}
|
|
}
|
|
|
|
function handleBackdropClick(e: MouseEvent) {
|
|
if (e.target === e.currentTarget) {
|
|
closeBar();
|
|
}
|
|
}
|
|
</script>
|
|
|
|
{#if $commandBarOpen}
|
|
<!-- Backdrop -->
|
|
<div
|
|
class="fixed inset-0 z-[200] flex items-start justify-center bg-black/50 backdrop-blur-sm p-4 pt-[20vh]"
|
|
onclick={handleBackdropClick}
|
|
role="dialog"
|
|
aria-label="Command Bar"
|
|
>
|
|
<!-- Modal -->
|
|
<div
|
|
class="w-full max-w-lg rounded-xl border border-amber-500/30 bg-zinc-800 shadow-2xl overflow-hidden flex flex-col"
|
|
>
|
|
<div class="flex items-center border-b border-[#444] px-4 py-3">
|
|
<span class="mr-3 text-lg text-muted">🔍</span>
|
|
<input
|
|
bind:this={inputEl}
|
|
bind:value={query}
|
|
onkeydown={handleKeydown}
|
|
type="text"
|
|
class="w-full bg-transparent text-lg text-white placeholder-muted outline-none"
|
|
placeholder="Suchen oder Befehl eingeben..."
|
|
/>
|
|
<span
|
|
class="ml-2 text-xs text-muted font-mono rounded bg-zinc-700 px-1.5 py-0.5"
|
|
>ESC</span
|
|
>
|
|
</div>
|
|
|
|
{#if results && results.length > 0}
|
|
<div class="max-h-80 overflow-y-auto py-2">
|
|
{#each results as item, i (item.id)}
|
|
<button
|
|
id={`kano-item-${i}`}
|
|
class="w-full flex items-center justify-between px-4 py-3 text-left transition-colors {i ===
|
|
selectedIndex
|
|
? 'bg-zinc-700'
|
|
: 'hover:bg-zinc-700/50'}"
|
|
onmouseover={() => (selectedIndex = i)}
|
|
onclick={() => item.action()}
|
|
>
|
|
<div class="flex items-center truncate">
|
|
<span class="mr-3 text-lg">{item.icon}</span>
|
|
<span class="truncate text-white"
|
|
>{item.label}</span
|
|
>
|
|
</div>
|
|
<div class="ml-4 flex items-center shrink-0">
|
|
<span
|
|
class="text-[10px] font-bold tracking-wider text-muted opacity-80"
|
|
>{item.badge}</span
|
|
>
|
|
{#if i === selectedIndex}
|
|
<span class="ml-3 text-sm text-accent"
|
|
>↵</span
|
|
>
|
|
{/if}
|
|
</div>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
{:else if query}
|
|
<div class="px-4 py-8 text-center text-muted">
|
|
Keine Ergebnisse für "{query}"
|
|
</div>
|
|
{/if}
|
|
{#if isOffline}
|
|
<div class="px-4 py-1.5 text-[11px] text-zinc-500 border-t border-zinc-700">
|
|
Server nicht erreichbar — nur lokale Ergebnisse
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|