Files
paseo/packages/website/src/plugins/plugin-card.tsx
T
Mohamed Boudra c062280429 Ship the plugin directory redesign and keep registry installs behind a daemon flag (#6080)
* website: plugin page design mockups (four directions, throwaway routes)

* website: final plugin page mockup (A directory, B browse frame, list cards, D detail)

* website: browse pages use a four-column card grid in the final plugin mockup

* feat(plugins): keep registry installs behind a daemon flag, off by default

Off restores the pre-registry behavior: bare owner/repo is GitHub shorthand and the
daemon never resolves or contacts a registry on install, update preview, or update.
Enable with pluginRegistryEnabled: true or PASEO_PLUGIN_REGISTRY_ENABLED=1.

* feat(website): ship the plugin directory, browse pages, and single-column detail

Directory: count, build/submit links, What's new, the nine categories, and Most installed
with a This week / This month / All time switch. Browse pages at /plugins/all and
/plugins/category/<slug> with a category sidebar, tabs, and a four-column grid; old
/plugins?category= links redirect. Install counts gain daily KV buckets kept 60 days.

* fix(website): keep a README's leading GitHub alert on the plugin page

* feat(website): search plugins from the directory and browse pages

Typing replaces the URL with /plugins/all?q=<term> (or the current category
with ?q=), which renders matching plugins on the server. Matching covers
name, description, ID, and author. Old /plugins?q= links redirect.

* fix(website): keep the directory's ranking window when searching
2026-10-04 20:38:39 +01:00

117 lines
3.8 KiB
TypeScript

import { Download } from "lucide-react";
import { pluginHref } from "./links";
import { PluginTile } from "./plugin-tile";
import { formatInstalls, getCategory, type Plugin } from "./registry";
export const PLUGIN_GRID_CLASS =
"grid gap-x-4 gap-y-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4";
/** First screenshot, or the plugin tile on a quiet backdrop when there is none. */
function PluginShot({ plugin }: { plugin: Plugin }) {
const url = plugin.screenshots[0];
return (
<div
aria-hidden
className="aspect-[16/10] overflow-hidden rounded-xl border border-white/10 bg-white/[0.03] transition-colors group-hover:border-white/20"
>
{url ? (
<img
src={url}
alt=""
loading="lazy"
className="h-full w-full object-cover object-left-top"
/>
) : (
<div className="flex h-full w-full items-center justify-center bg-[radial-gradient(circle_at_30%_20%,rgba(35,153,86,0.12),transparent_60%)]">
<PluginTile plugin={plugin} size="lg" />
</div>
)}
</div>
);
}
/** What's new card: screenshot, name, and when it was added. */
export function NewPluginCard({ plugin, added }: { plugin: Plugin; added: string }) {
return (
<a href={pluginHref(plugin.id)} className="group block w-[70%] flex-shrink-0 sm:w-auto">
<PluginShot plugin={plugin} />
<div className="mt-3 flex items-center gap-2.5">
<PluginTile plugin={plugin} size="sm" />
<div className="min-w-0">
<p className="truncate text-sm font-medium text-white">{plugin.name}</p>
<p className="truncate text-xs text-extra-muted-foreground">Added {added}</p>
</div>
</div>
</a>
);
}
/** Grid card: screenshot, name, one muted stat, and a two-line description. */
export function PluginCard({
plugin,
installs,
added,
}: {
plugin: Plugin;
/** Shown with a download icon; otherwise `added` is shown. */
installs?: number;
added?: string;
}) {
return (
<a href={pluginHref(plugin.id)} className="group block">
<PluginShot plugin={plugin} />
<div className="mt-3 flex items-start gap-2.5">
<PluginTile plugin={plugin} size="sm" />
<div className="min-w-0 flex-1">
<div className="flex items-baseline justify-between gap-2">
<p className="truncate text-sm font-medium text-white">{plugin.name}</p>
<span className="inline-flex flex-shrink-0 items-center gap-1 text-xs tabular-nums text-extra-muted-foreground">
{installs === undefined ? (
added
) : (
<>
<Download className="h-3 w-3" />
{formatInstalls(installs)}
</>
)}
</span>
</div>
<p className="mt-0.5 line-clamp-2 min-h-10 text-xs leading-5 text-muted-foreground">
{plugin.description}
</p>
</div>
</div>
</a>
);
}
/** Ranked row for the directory's Most installed list. */
export function PluginRankRow({
plugin,
rank,
installs,
}: {
plugin: Plugin;
rank: number;
installs: number;
}) {
return (
<a
href={pluginHref(plugin.id)}
className="flex items-center gap-3 rounded-lg px-2 py-2.5 transition-colors hover:bg-white/[0.04]"
>
<span className="w-4 text-right text-sm tabular-nums text-extra-muted-foreground">
{rank}
</span>
<PluginTile plugin={plugin} size="sm" />
<div className="min-w-0 flex-1">
<p className="truncate text-sm text-white">{plugin.name}</p>
<p className="truncate text-xs text-extra-muted-foreground">
{getCategory(plugin.categories[0])?.label}
</p>
</div>
<span className="text-xs tabular-nums text-muted-foreground">{formatInstalls(installs)}</span>
</a>
);
}