fix(web/store): restore the plugin cards' top padding, and capitalise platform names
ci / web (push) Successful in 1m5s
ci / docs-site (push) Successful in 1m5s
apple / swift (push) Successful in 1m24s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 31s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
ci / bench (push) Successful in 5m34s
apple / screenshots (push) Successful in 6m33s
windows-host / package (push) Successful in 9m46s
deb / build-publish (push) Successful in 9m19s
docker / deploy-docs (push) Successful in 26s
deb / build-publish-host (push) Successful in 9m48s
android / android (push) Successful in 17m6s
arch / build-publish (push) Successful in 18m10s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m30s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 21m38s
ci / rust (push) Successful in 27m42s

Two things looked wrong in the store:

The cards had no space above the icon. `CardContent` zeroes its own top padding
(`pt-0`/`sm:pt-0`) because it normally sits under a `CardHeader` that supplies
it — but these cards have no header, so the top padding simply vanished while
the other three sides kept `p-card`. `p-card` does not undo it: `card` is a
custom `--spacing-*` token, which tailwind-merge does not recognise as a
spacing value and so never dedupes against `pt-0`, leaving the longhand to win.
Both header-less cards in this section now restore it explicitly, at both
breakpoints.

Platform chips read "windows" / "linux". Those are the catalog's platform
IDENTIFIERS (the index validator pins `linux | windows | macos`), which is right
for the wire and wrong on screen; the card now maps them to display names —
Linux, Windows, macOS. Proper nouns, so deliberately not routed through i18n.

Adds Store/StoreCard stories covering the padding, the platform labels, and the
installed / update / incompatible / blocked / external states, so the card can
be eyeballed without a host or a catalog.
This commit is contained in:
2026-07-21 00:00:31 +02:00
parent d783d5445c
commit 402ec90edc
3 changed files with 116 additions and 3 deletions
+24 -2
View File
@@ -127,6 +127,26 @@ export const BrowseTab: FC<{
);
};
/**
* 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<string, string> = {
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,
@@ -145,7 +165,9 @@ export const StoreCard: FC<{ entry: StoreEntry; onInstall: () => void }> = ({
!entry.compatible && !blocked && "opacity-60",
)}
>
<CardContent className="flex flex-1 flex-col gap-3 p-card">
<CardContent
className={cn("flex flex-1 flex-col gap-3", HEADERLESS_CARD_PADDING)}
>
<div className="flex items-start gap-3">
<span className="flex size-10 shrink-0 items-center justify-center rounded-md bg-primary/15">
<Icon className="size-5 text-foreground" />
@@ -173,7 +195,7 @@ export const StoreCard: FC<{ entry: StoreEntry; onInstall: () => void }> = ({
<div className="flex flex-wrap gap-1.5">
{entry.platforms.map((p) => (
<Badge key={p} variant="secondary" className="font-normal">
{p}
{PLATFORM_LABELS[p] ?? p}
</Badge>
))}
</div>
+1 -1
View File
@@ -66,7 +66,7 @@ export const JobProgressCard: FC<{
className={failed ? "ring-2 ring-destructive/60" : undefined}
aria-live="polite"
>
<CardContent className="space-y-3 p-card">
<CardContent className="space-y-3 p-card pt-card sm:pt-card">
<div className="flex items-start gap-3">
{running ? (
<Spinner className="mt-0.5 size-5 shrink-0" />
+91
View File
@@ -0,0 +1,91 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { StoreEntry } from "@/api/store";
import { StoreCard } from "@/sections/Store/Browse";
// The store catalog card, rendered straight from a fixture entry — it fetches nothing, so
// the visuals (padding, platform labels, tier chip, blocked/incompatible states) can be
// checked without a host or a catalog.
const meta = {
title: "Store/StoreCard",
component: StoreCard,
parameters: { layout: "centered" },
args: { onInstall: () => {} },
} satisfies Meta<typeof StoreCard>;
export default meta;
type Story = StoryObj<typeof meta>;
const BASE: StoreEntry = {
id: "rom-manager",
pkg: "@punktfunk/plugin-rom-manager",
title: "ROM Manager",
description:
"Scans your ROM directories, maps them to emulators, fetches box art, and reconciles everything into the host game library as a provider.",
icon: "gamepad-2",
author: "unom",
homepage: "https://git.unom.io/unom/punktfunk-plugin-rom-manager",
license: "MIT OR Apache-2.0",
version: "0.3.2",
source: "unom official",
tier: "verified",
// The catalog stores lowercase platform IDENTIFIERS; the card renders display names.
platforms: ["linux", "windows"],
compatible: true,
update_available: false,
};
export const Default: Story = { args: { entry: BASE } };
export const AllPlatforms: Story = {
args: { entry: { ...BASE, platforms: ["linux", "windows", "macos"] } },
};
export const SinglePlatform: Story = {
args: {
entry: {
...BASE,
id: "playnite",
pkg: "@punktfunk/plugin-playnite",
title: "Playnite",
description:
"Syncs your Playnite library into the host game library — every store and emulator Playnite manages, launched back through Playnite.",
icon: "library-big",
version: "0.2.0",
platforms: ["windows"],
},
},
};
export const Installed: Story = {
args: { entry: { ...BASE, installed_version: "0.3.2" } },
};
export const UpdateAvailable: Story = {
args: {
entry: { ...BASE, installed_version: "0.3.1", update_available: true },
},
};
export const Incompatible: Story = {
args: {
entry: {
...BASE,
compatible: false,
incompatible_reason:
"needs punktfunk host ≥ 0.16.0 (this host is 0.15.0)",
},
},
};
export const Blocked: Story = {
args: {
entry: { ...BASE, blocked: "withdrawn by the author (CVE-2026-1234)" },
},
};
export const External: Story = {
args: {
entry: { ...BASE, tier: "external", source: "community catalog" },
},
};