import { AlertTriangle, Ban, Check, Download, Search } from "lucide-react"; import { type FC, useMemo, useState } from "react"; import { pluginIcon } from "@/api/plugins"; import { type StoreEntry, useStoreCatalog } from "@/api/store"; import { QueryState } from "@/components/query-state"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import { m } from "@/paraglide/messages"; import { RunnerBanner } from "./Runner"; import { SourceChip, TierBadge } from "./TierBadge"; /** Case-insensitive substring match across the fields an operator would actually search by. */ function matches(entry: StoreEntry, needle: string): boolean { if (!needle) return true; const q = needle.toLowerCase(); return [entry.title, entry.description, entry.pkg, entry.author].some((f) => f.toLowerCase().includes(q), ); } /** * Container: the catalog. Owns the catalog query plus the local search/source filter; installing is * escalated to the parent, which owns the tier-appropriate dialog and the resulting job โ€” so this * subsection never installs anything itself. */ export const BrowseTab: FC<{ onInstall: (entry: StoreEntry) => void; onInstallSpec: () => void; }> = ({ onInstall, onInstallSpec }) => { const catalog = useStoreCatalog(); const [query, setQuery] = useState(""); const [source, setSource] = useState(null); const entries = catalog.data?.plugins ?? []; const sources = catalog.data?.sources ?? []; const shown = useMemo( () => entries.filter( (e) => (source === null || e.source === source) && matches(e, query), ), [entries, source, query], ); return (
setQuery(e.target.value)} />
{/* One chip per source, so an operator can see a third-party catalog's entries alone. */} {sources.length > 1 && (
{sources.map((s) => ( ))}
)}
{shown.length === 0 ? ( {entries.length === 0 ? m.store_empty() : m.store_no_match()} ) : (
{shown.map((entry) => ( onInstall(entry)} /> ))}
)}
{/* The ONLY way to the raw-spec install. Deliberately a quiet footer link, not a button on a card: an unverified install should take a decision, never a stray click. */}
); }; /** * The catalog stores platform IDENTIFIERS (`linux | windows | macos` โ€” see the index * validator); these are their display names. Proper nouns, so deliberately not routed * through i18n, and `macos` is spelled the way Apple spells it. */ const PLATFORM_LABELS: Record = { linux: "Linux", windows: "Windows", macos: "macOS", }; /** * `CardContent` zeroes its top padding (`pt-0`/`sm:pt-0`) because it normally sits under a * `CardHeader` that already supplies it โ€” these cards have no header, so the top padding * has to come back explicitly, at BOTH breakpoints. `p-card` alone does not do it: `card` * is a custom `--spacing-*` token, which tailwind-merge does not recognise as a spacing * value and therefore never dedupes against `pt-0`, leaving the longhand to win. */ const HEADERLESS_CARD_PADDING = "p-card pt-card sm:pt-card"; /** One catalog entry. Blocked entries shout; incompatible ones grey out; neither can be installed. */ export const StoreCard: FC<{ entry: StoreEntry; onInstall: () => void }> = ({ entry, onInstall, }) => { const Icon = pluginIcon(entry.icon); const blocked = entry.blocked !== undefined; const installed = entry.installed_version !== undefined; const installable = !blocked && entry.compatible; return (

{entry.title}

{m.store_by_author({ author: entry.author })} ยท v{entry.version}

{/* Attribution, never verification: an external entry names who curated it. */} {entry.tier === "external" && }

{entry.description}

{entry.platforms.map((p) => ( {PLATFORM_LABELS[p] ?? p} ))}
{blocked && (

{m.store_blocked({ reason: entry.blocked ?? "" })}

)} {!entry.compatible && !blocked && (

{entry.incompatible_reason ?? m.store_incompatible()}

)} {/* Footer pinned to the bottom so cards in a row line their actions up. */}
{entry.update_available ? ( ) : installed ? ( {m.store_installed_label()} ) : ( )} {entry.homepage && ( {m.store_homepage()} )}
); };