diff --git a/web/src/sections/Store/Browse.tsx b/web/src/sections/Store/Browse.tsx index bfe5379e..43554e51 100644 --- a/web/src/sections/Store/Browse.tsx +++ b/web/src/sections/Store/Browse.tsx @@ -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 = { + 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", )} > - +
@@ -173,7 +195,7 @@ export const StoreCard: FC<{ entry: StoreEntry; onInstall: () => void }> = ({
{entry.platforms.map((p) => ( - {p} + {PLATFORM_LABELS[p] ?? p} ))}
diff --git a/web/src/sections/Store/JobProgress.tsx b/web/src/sections/Store/JobProgress.tsx index 1b5ec08f..c78e0388 100644 --- a/web/src/sections/Store/JobProgress.tsx +++ b/web/src/sections/Store/JobProgress.tsx @@ -66,7 +66,7 @@ export const JobProgressCard: FC<{ className={failed ? "ring-2 ring-destructive/60" : undefined} aria-live="polite" > - +
{running ? ( diff --git a/web/src/stories/Store.stories.tsx b/web/src/stories/Store.stories.tsx new file mode 100644 index 00000000..9d544580 --- /dev/null +++ b/web/src/stories/Store.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +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" }, + }, +};