Files
LaunchPad/packages/ui/src/ResultsList.tsx

115 lines
3.9 KiB
TypeScript

import type { KeyboardEvent } from "react";
import type { Service } from "@launchpad/shared";
export interface ResultsListProps {
services: Service[];
selectedIndex: number;
emptyLabel?: string;
onHover: (index: number) => void;
onOpen: (service: Service) => void;
onToggleFavorite?: (service: Service) => void;
}
/**
* Zeigt die (bereits per rankServices sortierten) Suchtreffer an.
* Die Tastatur-Navigation (Pfeiltasten/Enter) wird vom Elternelement
* gesteuert; diese Komponente ist rein darstellend + klick-/tastaturbar.
*/
export function ResultsList({
services,
selectedIndex,
emptyLabel = "Keine Dienste gefunden.",
onHover,
onOpen,
onToggleFavorite,
}: ResultsListProps) {
if (services.length === 0) {
return (
<div
className="mt-4 rounded-2xl border border-black/5 bg-white/50 px-5 py-8 text-center
text-sm text-black/40 dark:border-white/5 dark:bg-white/5 dark:text-white/40"
>
{emptyLabel}
</div>
);
}
return (
<ul
role="listbox"
className="mt-4 flex max-h-[60vh] flex-col overflow-y-auto rounded-2xl border
border-black/10 bg-white/80 shadow-lg backdrop-blur-md dark:border-white/10
dark:bg-white/5"
>
{services.map((service, index) => {
const active = index === selectedIndex;
return (
<li key={service.id} role="option" aria-selected={active}>
<div
tabIndex={-1}
onMouseEnter={() => onHover(index)}
onClick={() => onOpen(service)}
onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Enter") onOpen(service);
}}
className={`flex w-full cursor-pointer items-center gap-3 px-5 py-3 text-left
transition-colors ${
active
? "bg-black/5 dark:bg-white/10"
: "hover:bg-black/[0.03] dark:hover:bg-white/5"
}`}
>
{service.favicon ? (
<img src={service.favicon} alt="" className="h-5 w-5 shrink-0 rounded" />
) : (
<span
className="flex h-5 w-5 shrink-0 items-center justify-center rounded
bg-black/10 text-[10px] font-medium text-black/50 dark:bg-white/10
dark:text-white/50"
>
{service.displayName.charAt(0).toUpperCase()}
</span>
)}
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium text-black dark:text-white">
{service.displayName}
</span>
<span className="block truncate text-xs text-black/40 dark:text-white/40">
{service.hostname}
</span>
</span>
{service.category ? (
<span className="shrink-0 text-xs text-black/30 dark:text-white/30">
{service.category}
</span>
) : null}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onToggleFavorite?.(service);
}}
disabled={!onToggleFavorite}
aria-pressed={service.favorite}
aria-label={
service.favorite ? "Als Favorit entfernen" : "Als Favorit markieren"
}
className={`shrink-0 text-lg leading-none transition-colors ${
service.favorite
? "text-amber-500"
: "text-black/15 hover:text-amber-400 dark:text-white/15 dark:hover:text-amber-400"
} ${onToggleFavorite ? "" : "cursor-default"}`}
>
</button>
</div>
</li>
);
})}
</ul>
);
}