generated from Dicken/dickendock
round24: Favicon-Groesse und Wort-Erkennung verbessert, ausgeblendet-Badge entfernt, Proxmox-Port-Bug gefixt, Suche um Geraete und Spaeter-lesen erweitert
This commit is contained in:
@@ -3,12 +3,13 @@ import { useState, useEffect } from "react";
|
||||
export interface FaviconProps {
|
||||
src?: string | null;
|
||||
fallbackLetter: string;
|
||||
size?: "sm" | "md";
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
const SIZE_CLASSES: Record<NonNullable<FaviconProps["size"]>, string> = {
|
||||
sm: "h-4 w-4",
|
||||
md: "h-5 w-5",
|
||||
lg: "h-7 w-7",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -131,9 +131,9 @@ function IconTile({ url, name, onSelect }: { url: string; name: string; onSelect
|
||||
onMouseEnter={() => setAnchorRect(buttonRef.current?.getBoundingClientRect() ?? null)}
|
||||
onMouseLeave={() => setAnchorRect(null)}
|
||||
onClick={() => onSelect(url)}
|
||||
className="rounded p-0.5 hover:bg-black/5 dark:hover:bg-white/10"
|
||||
className="rounded-lg p-1 hover:bg-black/5 dark:hover:bg-white/10"
|
||||
>
|
||||
<Favicon src={url} fallbackLetter={name} size="sm" />
|
||||
<Favicon src={url} fallbackLetter={name} size="lg" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
@@ -205,7 +205,7 @@ export function FaviconPicker({ existingFavicons, onSelect }: FaviconPickerProps
|
||||
|
||||
{open ? (
|
||||
<div
|
||||
className="absolute left-0 top-full z-20 mt-1 w-72 rounded-xl border border-black/10
|
||||
className="absolute left-0 top-full z-20 mt-1 w-80 rounded-xl border border-black/10
|
||||
bg-white p-2 shadow-lg dark:border-white/10 dark:bg-neutral-900"
|
||||
>
|
||||
<input
|
||||
@@ -218,12 +218,12 @@ export function FaviconPicker({ existingFavicons, onSelect }: FaviconPickerProps
|
||||
dark:border-white/10 dark:bg-white/10 dark:text-white dark:placeholder:text-white/30"
|
||||
/>
|
||||
|
||||
<div className="max-h-48 overflow-y-auto">
|
||||
<div className="max-h-56 overflow-y-auto">
|
||||
{showingSearch ? (
|
||||
searching ? (
|
||||
<p className="p-2 text-xs text-black/40 dark:text-white/40">Suche …</p>
|
||||
) : loadableDbResults.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{loadableDbResults.map((r) => (
|
||||
<IconTile
|
||||
key={r.url}
|
||||
@@ -245,7 +245,7 @@ export function FaviconPicker({ existingFavicons, onSelect }: FaviconPickerProps
|
||||
<p className="mb-1 px-1 text-[10px] font-medium uppercase tracking-wide text-black/30 dark:text-white/30">
|
||||
Bereits im System verwendet
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{loadableExisting.map((r) => (
|
||||
<IconTile
|
||||
key={r.url}
|
||||
|
||||
@@ -108,7 +108,7 @@ export function FavoritesBar({
|
||||
>
|
||||
<Favicon src={item.favicon} fallbackLetter={item.displayName} size="md" />
|
||||
</span>
|
||||
<span className="w-full truncate text-center text-[10px] leading-tight text-black/55 dark:text-white/55">
|
||||
<span className="line-clamp-2 w-full text-center text-[10px] leading-tight text-black/55 dark:text-white/55">
|
||||
{item.displayName}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -26,11 +26,31 @@ export interface ResultsListProps {
|
||||
onToggleFavorite?: (item: SearchResult) => void;
|
||||
}
|
||||
|
||||
function OnlineDot({ online }: { online: boolean }) {
|
||||
return (
|
||||
<span
|
||||
title={online ? "Online" : "Offline"}
|
||||
className={`h-1.5 w-1.5 shrink-0 rounded-full ${online ? "bg-emerald-500" : "bg-black/20 dark:bg-white/20"}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const KIND_BADGE: Record<Exclude<SearchResult["kind"], "service">, string> = {
|
||||
bookmark: "Lesezeichen",
|
||||
readlater: "Später lesen",
|
||||
device: "Gerät",
|
||||
};
|
||||
|
||||
/**
|
||||
* Zeigt die (bereits per rankServices sortierten) Suchtreffer an – Dienste
|
||||
* und Lesezeichen gemeinsam, unterscheidbar an einem kleinen Badge. Die
|
||||
* Tastatur-Navigation (Pfeiltasten/Enter) wird vom Elternelement gesteuert;
|
||||
* diese Komponente ist rein darstellend + klick-/tastaturbar.
|
||||
* Zeigt die (bereits per rankSearchResults sortierten) Suchtreffer an –
|
||||
* Dienste, Lesezeichen, Später-lesen und Geräte gemeinsam, unterscheidbar an
|
||||
* einem kleinen Badge. Geräte haben keine eigene URL zum Öffnen und werden
|
||||
* deshalb ausgegraut und nicht anklickbar dargestellt (nur zum Auffinden per
|
||||
* Name/IP/MAC, siehe rankSearchResults) - Dienste und Geräte zeigen
|
||||
* zusätzlich einen Online/Offline-Punkt.
|
||||
*
|
||||
* Die Tastatur-Navigation (Pfeiltasten/Enter) wird vom Elternelement
|
||||
* gesteuert; diese Komponente ist rein darstellend + klick-/tastaturbar.
|
||||
*
|
||||
* Füllt die Höhe des Elternelements aus (h-full) und scrollt selbst intern –
|
||||
* das Elternelement muss dafür `flex-1 min-h-0` sein (klassischer
|
||||
@@ -64,43 +84,53 @@ export function ResultsList({
|
||||
>
|
||||
{results.map((item, index) => {
|
||||
const active = index === selectedIndex;
|
||||
const isDevice = item.kind === "device";
|
||||
const subtitle =
|
||||
item.kind === "service" ? `${item.hostname}:${item.port}` : item.hostname;
|
||||
item.kind === "service"
|
||||
? `${item.hostname}:${item.port}`
|
||||
: item.kind === "device"
|
||||
? item.ip
|
||||
: item.hostname;
|
||||
const categoryColor = item.category ? categoryColors?.[item.category] : undefined;
|
||||
const online = item.kind === "device" ? item.online : item.kind === "service" ? item.deviceOnline : undefined;
|
||||
|
||||
return (
|
||||
<li key={`${item.kind}-${item.id}`} role="option" aria-selected={active}>
|
||||
<div
|
||||
tabIndex={-1}
|
||||
onMouseEnter={() => onHover(index)}
|
||||
onClick={() => onOpen(item)}
|
||||
onClick={() => !isDevice && onOpen(item)}
|
||||
onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === "Enter") onOpen(item);
|
||||
if (e.key === "Enter" && !isDevice) onOpen(item);
|
||||
}}
|
||||
style={
|
||||
categoryColor
|
||||
categoryColor && !isDevice
|
||||
? { backgroundColor: hexToRgba(categoryColor, active ? 0.18 : 0.1) ?? undefined }
|
||||
: undefined
|
||||
}
|
||||
className={`flex w-full cursor-pointer items-center gap-3 px-5 py-3 text-left
|
||||
transition-colors ${
|
||||
categoryColor
|
||||
? ""
|
||||
: active
|
||||
? "bg-black/5 dark:bg-white/10"
|
||||
: "hover:bg-black/[0.03] dark:hover:bg-white/5"
|
||||
}`}
|
||||
className={`flex w-full items-center gap-3 px-5 py-3 text-left transition-colors ${
|
||||
isDevice
|
||||
? "cursor-default opacity-50"
|
||||
: `cursor-pointer ${
|
||||
categoryColor
|
||||
? ""
|
||||
: active
|
||||
? "bg-black/5 dark:bg-white/10"
|
||||
: "hover:bg-black/[0.03] dark:hover:bg-white/5"
|
||||
}`
|
||||
}`}
|
||||
>
|
||||
<Favicon src={item.favicon} fallbackLetter={item.displayName} />
|
||||
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="flex items-center gap-1.5">
|
||||
{online !== undefined ? <OnlineDot online={online} /> : null}
|
||||
<span className="block truncate text-sm font-medium text-black dark:text-white">
|
||||
{item.displayName}
|
||||
</span>
|
||||
{item.kind === "bookmark" ? (
|
||||
{item.kind !== "service" ? (
|
||||
<span className="shrink-0 rounded-full bg-black/5 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-black/40 dark:bg-white/10 dark:text-white/40">
|
||||
Lesezeichen
|
||||
{KIND_BADGE[item.kind]}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
@@ -122,25 +152,27 @@ export function ResultsList({
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
<CopyButton url={item.url} />
|
||||
{!isDevice ? <CopyButton url={item.url} /> : null}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleFavorite?.(item);
|
||||
}}
|
||||
disabled={!onToggleFavorite}
|
||||
aria-pressed={item.favorite}
|
||||
aria-label={item.favorite ? "Als Favorit entfernen" : "Als Favorit markieren"}
|
||||
className={`shrink-0 text-lg leading-none transition-colors ${
|
||||
item.favorite
|
||||
? "text-amber-500"
|
||||
: "text-black/15 hover:text-amber-400 dark:text-white/15 dark:hover:text-amber-400"
|
||||
} ${onToggleFavorite ? "" : "cursor-default"}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={item.favorite ? faStarSolid : faStar} />
|
||||
</button>
|
||||
{item.kind === "service" || item.kind === "bookmark" ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleFavorite?.(item);
|
||||
}}
|
||||
disabled={!onToggleFavorite}
|
||||
aria-pressed={item.favorite}
|
||||
aria-label={item.favorite ? "Als Favorit entfernen" : "Als Favorit markieren"}
|
||||
className={`shrink-0 text-lg leading-none transition-colors ${
|
||||
item.favorite
|
||||
? "text-amber-500"
|
||||
: "text-black/15 hover:text-amber-400 dark:text-white/15 dark:hover:text-amber-400"
|
||||
} ${onToggleFavorite ? "" : "cursor-default"}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={item.favorite ? faStarSolid : faStar} />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user