fix(plugins): show full plugin names in marketplace cards

This commit is contained in:
_Mr丶s ゛😁®
2026-09-29 01:49:37 +08:00
committed by GitHub
parent f9ee8a2fb2
commit 9f2f5c525d
2 changed files with 64 additions and 61 deletions
@@ -1049,43 +1049,24 @@ onBeforeUnmount(() => {
<div class="mt-3 text-sm font-medium">{{ t("pluginPlatform.noMarketplacePlugins") }}</div>
<div class="mt-1 text-xs text-muted-foreground">{{ t("pluginPlatform.noMarketplacePluginsDescription") }}</div>
</div>
<!-- 列数交给面板实际宽度(容器内 auto-fill),不再跟随窗口断点;TransitionGroup 提供过滤/排序的 FLIP 连续性 -->
<TransitionGroup v-else-if="marketplaceViewMode === 'grid'" name="marketplace-cards" tag="div" class="grid w-full grid-cols-[repeat(auto-fill,minmax(min(100%,19rem),1fr))] gap-3">
<article v-for="listing in sortedMarketplaceListings" :key="listing.key" class="group @container marketplace-card flex min-w-0 min-h-48 flex-col rounded-xl border bg-card p-4 transition-colors hover:border-primary/40">
<div class="flex flex-wrap items-start gap-3">
<button
v-if="batchMode && isBatchSelectableListing(listing.status)"
type="button"
class="mt-1 inline-flex size-4 shrink-0 items-center justify-center rounded border transition-colors"
:class="isListingSelected(listing) ? 'border-primary bg-primary text-primary-foreground' : 'border-muted-foreground/40 bg-background'"
:aria-pressed="isListingSelected(listing)"
:aria-label="listing.name"
:disabled="batchRunning"
@click.stop="toggleListingSelection(listing)"
>
<Check v-if="isListingSelected(listing)" class="size-3" />
</button>
<PluginIcon :plugin-id="listing.plugin.id" :icon="listing.plugin.icon" class="size-11 rounded-xl border bg-background p-1.5" />
<div class="mb-3 flex min-w-0 items-start gap-3">
<PluginIcon :plugin-id="listing.plugin.id" :icon="listing.plugin.icon" class="size-11 shrink-0 rounded-xl border bg-background p-1.5" />
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-1.5">
<span class="truncate text-sm font-semibold">{{ listing.name }}</span>
<div class="truncate text-sm font-semibold" :title="listing.name">{{ listing.name }}</div>
<div class="mt-1 flex min-w-0 items-center">
<Badge variant="outline" class="h-auto min-h-5 min-w-0 max-w-full shrink px-1.5 py-0.5 text-[10px] leading-tight whitespace-normal break-all" :title="`v${listing.plugin.latestVersion}`">v{{ listing.plugin.latestVersion }}</Badge>
</div>
<div class="mt-1 flex min-w-0 items-center gap-1 text-xs text-muted-foreground">
<BadgeCheck v-if="listingRepositoryCanVerify(listing.repository)" class="size-3.5 shrink-0 text-emerald-600 dark:text-emerald-400" :title="t('pluginPlatform.verified')" :aria-label="t('pluginPlatform.verified')" />
<span class="truncate">{{ listing.plugin.publisher }} · {{ listing.repository.name }}</span>
</div>
<!-- 发布信息行(紧凑档专属):日期+版本降为身份块的第三行纯文本,与标签行的
内容/风险元数据分类;纯文本而非徽标,形态本身完成信息类别区分 -->
<div class="mt-0.5 hidden truncate text-[10px] leading-3.5 text-muted-foreground @max-[21rem]:block">
v{{ listing.plugin.latestVersion }}<span v-if="listing.latestVersionReleasedAt"> · {{ formatMarketplaceReleasedDate(listing.latestVersionReleasedAt, appLocale) }}</span>
</div>
</div>
<div class="flex shrink-0 items-center gap-1.5 @max-[21rem]:gap-0.5">
<span class="inline-flex items-center gap-1">
<div class="mt-1 flex min-w-0 flex-wrap items-center gap-1.5 text-muted-foreground">
<button
v-if="listing.plugin.source"
type="button"
class="rounded p-0.5 opacity-60 transition-opacity [will-change:opacity] hover:opacity-100"
class="shrink-0 rounded p-0.5 opacity-60 transition-opacity [will-change:opacity] hover:opacity-100"
:title="t('pluginPlatform.sourceRepository')"
:aria-label="t('pluginPlatform.sourceRepository')"
@click.stop="openExternal(listing.plugin.source)"
@@ -1095,24 +1076,28 @@ onBeforeUnmount(() => {
<button
v-if="marketplaceHomepageUrl(listing.plugin.source, listing.plugin.homepage)"
type="button"
class="rounded p-0.5 opacity-60 transition-opacity [will-change:opacity] hover:opacity-100"
class="shrink-0 rounded p-0.5 opacity-60 transition-opacity [will-change:opacity] hover:opacity-100"
:title="t('pluginPlatform.pluginHomepage')"
:aria-label="t('pluginPlatform.pluginHomepage')"
@click.stop="openExternal(marketplaceHomepageUrl(listing.plugin.source, listing.plugin.homepage))"
>
<Globe class="size-3.5" />
</button>
</span>
<!-- 头部元数据芯片(日期+版本):标准档显示在这里;紧凑档(卡宽 <21rem)隐藏,
由标题块下方的发布信息行接管 —— 纯 CSS 无法跨容器移动元素,双份渲染按档位二选一 -->
<span v-if="listing.latestVersionReleasedAt" class="hidden shrink-0 items-center gap-1 @min-[21rem]:flex">
<span class="shrink-0 text-[10px] text-muted-foreground">{{ formatMarketplaceReleasedDate(listing.latestVersionReleasedAt, appLocale) }}</span>
<Badge variant="outline" class="h-5 shrink-0 px-1.5 text-[10px]">v{{ listing.plugin.latestVersion }}</Badge>
</span>
<span v-else class="hidden shrink-0 items-center @min-[21rem]:flex">
<Badge variant="outline" class="h-5 shrink-0 px-1.5 text-[10px]">v{{ listing.plugin.latestVersion }}</Badge>
</span>
<span v-if="listing.latestVersionReleasedAt" class="shrink-0 text-[10px] text-muted-foreground">{{ formatMarketplaceReleasedDate(listing.latestVersionReleasedAt, appLocale) }}</span>
</div>
</div>
<button
v-if="batchMode && isBatchSelectableListing(listing.status)"
type="button"
class="mt-0.5 inline-flex size-4 shrink-0 items-center justify-center rounded border transition-colors"
:class="isListingSelected(listing) ? 'border-primary bg-primary text-primary-foreground' : 'border-muted-foreground/40 bg-background'"
:aria-pressed="isListingSelected(listing)"
:aria-label="listing.name"
:disabled="batchRunning"
@click.stop="toggleListingSelection(listing)"
>
<Check v-if="isListingSelected(listing)" class="size-3" />
</button>
</div>
<div class="marketplace-tags mt-3 flex min-w-0 flex-wrap items-center gap-1.5 @max-[21rem]:mt-2.5">
<!-- 紧凑档(卡宽 <21rem)只露第 1 个标签 + "+N" 折叠;标准档由下方 t-extra 全显 -->
@@ -1194,7 +1179,7 @@ onBeforeUnmount(() => {
</article>
</TransitionGroup>
<TransitionGroup v-else name="marketplace-cards" tag="div" class="flex w-full flex-col gap-2">
<article v-for="listing in sortedMarketplaceListings" :key="listing.key" class="flex items-center gap-3 rounded-xl border bg-card p-3 transition-colors hover:border-primary/40">
<article v-for="listing in sortedMarketplaceListings" :key="listing.key" class="flex min-w-0 flex-wrap items-center gap-3 rounded-xl border bg-card p-3 transition-colors hover:border-primary/40">
<button
v-if="batchMode && isBatchSelectableListing(listing.status)"
type="button"
@@ -1207,10 +1192,17 @@ onBeforeUnmount(() => {
>
<Check v-if="isListingSelected(listing)" class="size-3" />
</button>
<PluginIcon :plugin-id="listing.plugin.id" :icon="listing.plugin.icon" class="size-10 rounded-lg border bg-background p-1.5" />
<PluginIcon :plugin-id="listing.plugin.id" :icon="listing.plugin.icon" class="size-10 shrink-0 rounded-lg border bg-background p-1.5" />
<div class="min-w-0 flex-1">
<div class="flex min-w-0 items-center gap-2">
<span class="truncate text-sm font-semibold">{{ listing.name }}</span>
<div class="truncate text-sm font-semibold" :title="listing.name">{{ listing.name }}</div>
<div class="mt-1 flex min-w-0 items-center">
<Badge variant="outline" class="h-auto min-h-5 min-w-0 max-w-full shrink px-1.5 py-0.5 text-[10px] leading-tight whitespace-normal break-all" :title="`v${listing.plugin.latestVersion}`">v{{ listing.plugin.latestVersion }}</Badge>
</div>
<div class="mt-1 flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground">
<BadgeCheck v-if="listingRepositoryCanVerify(listing.repository)" class="size-3.5 shrink-0 text-emerald-600 dark:text-emerald-400" :title="t('pluginPlatform.verified')" :aria-label="t('pluginPlatform.verified')" />
<span class="min-w-0 truncate">{{ listing.plugin.publisher }} · {{ listing.repository.name }}</span>
</div>
<div class="mt-1 flex min-w-0 flex-wrap items-center gap-1.5 text-muted-foreground">
<button
v-if="listing.plugin.source"
type="button"
@@ -1231,12 +1223,7 @@ onBeforeUnmount(() => {
>
<Globe class="size-3.5" />
</button>
<span v-if="listing.latestVersionReleasedAt" class="shrink-0 text-[10px] text-muted-foreground @max-[38.75rem]:hidden">{{ formatMarketplaceReleasedDate(listing.latestVersionReleasedAt, appLocale) }}</span>
<Badge variant="outline" class="h-5 shrink-0 px-1.5 text-[10px]">v{{ listing.plugin.latestVersion }}</Badge>
</div>
<div class="mt-0.5 flex min-w-0 items-center gap-1 text-xs text-muted-foreground">
<BadgeCheck v-if="listingRepositoryCanVerify(listing.repository)" class="size-3.5 shrink-0 text-emerald-600 dark:text-emerald-400" :title="t('pluginPlatform.verified')" :aria-label="t('pluginPlatform.verified')" />
<span class="truncate">{{ listing.plugin.publisher }} · {{ listing.repository.name }}</span>
<span v-if="listing.latestVersionReleasedAt" class="shrink-0 text-[10px] text-muted-foreground">{{ formatMarketplaceReleasedDate(listing.latestVersionReleasedAt, appLocale) }}</span>
</div>
<Tooltip :delay-duration="700">
<TooltipTrigger as-child>
@@ -886,24 +886,40 @@ describe("PluginContributionsPanel marketplace sort", () => {
});
describe("PluginContributionsPanel marketplace card layout", () => {
it("wraps the grid card header and pins the version badge so it never clips in a narrow panel", async () => {
it("keeps the three-column grid and shows full names and versions in both views", async () => {
state.batchMode = false;
state.marketplaceViewMode = "grid";
await nextTick();
const longName = "Database Schema Explorer Marketplace Plugin With Deliberately Long Name";
const versionValue = "3.0.0-preview.10483";
const plugin = state.catalogResults[0].catalog!.plugins.find((entry) => entry.id === "a")!;
plugin.name = longName;
plugin.latestVersion = versionValue;
plugin.versions[0].version = versionValue;
await flushUi();
const card = host.querySelector("article");
expect(card, "a marketplace grid card should render").not.toBeNull();
for (const view of ["grid", "list"] as const) {
state.marketplaceViewMode = view;
await nextTick();
// The header row (icon + name + github/globe/date/version cluster) must be allowed to wrap,
// otherwise the non-shrinkable right cluster overflows the narrow column and the version
// badge is clipped (e.g. "v0.1.C") when the plugin center shares width with the AI panel.
const header = card!.querySelector(":scope > div");
expect(header?.classList.contains("flex-wrap"), "grid card header row should wrap").toBe(true);
const cards = [...host.querySelectorAll("article")];
const pluginName = cards.flatMap((card) => [...card.querySelectorAll<HTMLElement>("[title]")]).find((element) => element.title === longName);
expect(pluginName?.textContent?.trim(), `${view}: full plugin name`).toBe(longName);
expect(pluginName?.classList.contains("truncate"), `${view}: long name truncates visually`).toBe(true);
expect(pluginName?.classList.contains("text-sm"), `${view}: name size stays fixed`).toBe(true);
// The version badge must not shrink, so it is never squished even when it stays on one line.
const versionBadge = [...host.querySelectorAll<HTMLElement>("[data-stub='Badge']")].find((element) => element.textContent?.trim() === "v3.0.0");
expect(versionBadge, "version badge should render").toBeDefined();
expect(versionBadge!.classList.contains("shrink-0"), "version badge should be shrink-0").toBe(true);
const card = pluginName?.closest("article");
const versionBadge = [...(card?.querySelectorAll<HTMLElement>("[data-stub='Badge']") ?? [])].find((element) => element.textContent?.trim() === `v${versionValue}`);
expect(versionBadge?.textContent?.trim(), `${view}: full version`).toBe(`v${versionValue}`);
expect(versionBadge?.classList.contains("max-w-full"), `${view}: version fits the available width`).toBe(true);
const details = [...(card?.querySelectorAll<HTMLElement>("span") ?? [])].find((element) => element.textContent?.trim() === "DBX · first");
expect(details, `${view}: publisher and repository details render`).toBeDefined();
expect(versionBadge!.compareDocumentPosition(details!) & Node.DOCUMENT_POSITION_FOLLOWING).not.toBe(0);
if (view === "grid") {
expect(card?.parentElement?.classList.contains("md:grid-cols-3"), "grid must not use the viewport breakpoint removed by #10444").toBe(false);
expect(card?.parentElement?.className, "grid tracks the panel width via auto-fill").toContain("auto-fill");
}
}
});
});