@@ -181,18 +173,9 @@ function HomeListingSkillRow({ entry, showStats }: { entry: SkillPageEntry; show
{truncateText(entry.skill.summary || "Agent-ready skill pack.", 80)}
- {showStats ? (
-
-
-
- {formatCompactStat(entry.skill.stats?.stars ?? 0)}
-
-
-
- {formatCompactStat(entry.skill.stats?.downloads ?? 0)}
-
-
- ) : null}
+
+ {formatCompactStat(entry.skill.stats?.downloads ?? 0)}
+
);
}
@@ -203,15 +186,6 @@ function HomeListingPluginRow({ plugin }: { plugin: PackageListItem }) {
return (
-
-
-
@@ -227,10 +201,7 @@ function HomeListingPluginRow({ plugin }: { plugin: PackageListItem }) {
-
-
- {formatCompactStat(plugin.stats?.downloads ?? 0)}
-
+ {formatCompactStat(plugin.stats?.downloads ?? 0)}
);
@@ -307,7 +278,6 @@ export function HomeListingSection({ initialListing = null }: HomeListingSection
const activeItems = kind === "skills" ? skills : plugins;
const activeStatus = status;
const isEmpty = activeStatus === "idle" && activeItems.length === 0;
- const showSkillStats = true;
const showListingMore =
activeStatus === "idle" && (activeItems.length > visibleCount || listingHasMore);
@@ -487,19 +457,11 @@ export function HomeListingSection({ initialListing = null }: HomeListingSection
{activeStatus === "idle" && activeItems.length > 0 ? (
-
-
+
{kind === "skills" ? "Skill" : "Plugin"}
- {kind === "skills" && tab === "trending" ? (
- 24h downloads
- ) : showSkillStats ? (
- Popularity
- ) : null}
+ Downloads
) : null}
@@ -532,7 +494,6 @@ export function HomeListingSection({ initialListing = null }: HomeListingSection
))}
diff --git a/src/components/SkillDetailPage.tsx b/src/components/SkillDetailPage.tsx
index 1b9912bf..8164cbdf 100644
--- a/src/components/SkillDetailPage.tsx
+++ b/src/components/SkillDetailPage.tsx
@@ -31,6 +31,7 @@ import { DetailBody, DetailPageShell } from "./DetailPageShell";
import { DetailSecuritySummary } from "./DetailSecuritySummary";
import { GenericNotFoundPage } from "./GenericNotFoundPage";
import { SkillDetailSkeleton } from "./skeletons/SkillDetailSkeleton";
+import { SkillDetailPageView } from "./SkillDetailPageView";
import { SkillDetailTabs, type DetailTab } from "./SkillDetailTabs";
import {
buildSkillHref,
@@ -39,7 +40,6 @@ import {
formatOsList,
stripFrontmatter,
} from "./skillDetailUtils";
-import { SkillHeader } from "./SkillHeader";
import { buildSkillInstallTabs } from "./SkillInstallCard";
import { SkillOwnershipPanel } from "./SkillOwnershipPanel";
import { SkillPublishSuccessDialog } from "./SkillPublishSuccessDialog";
@@ -882,123 +882,122 @@ export function SkillDetailPage({
}
return (
-
-
- void handleToggleStar()}
- onOpenReport={openReportDialog}
- onRequireSignIn={requireSignIn}
- forkOf={forkOf}
- forkOfLabel={forkOfLabel}
- forkOfHref={forkOfHref}
- forkOfOwnerHandle={forkOfOwnerHandle}
- canonical={canonical}
- canonicalHref={canonicalHref}
- canonicalOwnerHandle={canonicalOwnerHandle}
- staffVisibilityTag={staffVisibilityTag}
- isAutoHidden={isAutoHidden}
- isRemoved={isRemoved}
- nixPlugin={nixPlugin}
- hasPluginBundle={hasPluginBundle}
- configRequirements={configRequirements}
- cliHelp={cliHelp}
- clawdis={clawdis}
- category={relatedCategory}
- categories={relatedCategories}
- staffVisibilityAlert={staffVisibilityAlert}
- securityAuditSummary={securitySummary}
- activityTrend={activityTrend}
- activityTrendLoading={activityTrendLoading}
- newVersionHref={newVersionHref}
- settingsHref={settingsHref}
- showArchiveMetadata={!isGitHubBackedSkill}
- >
- {nixSnippet ? (
-
- Install via Nix
- {nixSnippet}
-
- ) : null}
-
- {configExample ? (
-
- Config example
- {configExample}
-
- ) : null}
-
- setShouldPrefetchCompare(true)}
- readmeContent={readmeContent}
- readmeError={displayedReadmeError}
- skillCardContent={displayedSkillCard}
- skillCardError={displayedSkillCardError}
- hasSkillCard={hasSkillCard}
- latestFiles={latestFiles}
- latestVersionId={latestVersion?._id ?? null}
- latestVersion={latestVersion?.version ?? null}
- canDeleteVersions={canDeleteSkillVersions}
- skill={skill as Doc<"skills">}
- ownerHandle={ownerHandle}
- diffVersions={diffVersions}
- versions={versions}
- nixPlugin={Boolean(nixPlugin)}
- showArchiveTabs={!isGitHubBackedSkill}
- suppressVersionScanResults={suppressVersionScanResults}
- scanResultsSuppressedMessage={scanResultsSuppressedMessage}
- clawdis={clawdis}
- osLabels={osLabels}
- readmeHrefResolver={readmeHrefResolver}
+ void handleToggleStar()}
+ onOpenReport={openReportDialog}
+ onRequireSignIn={requireSignIn}
+ forkOf={forkOf}
+ forkOfLabel={forkOfLabel}
+ forkOfHref={forkOfHref}
+ forkOfOwnerHandle={forkOfOwnerHandle}
+ canonical={canonical}
+ canonicalHref={canonicalHref}
+ canonicalOwnerHandle={canonicalOwnerHandle}
+ staffVisibilityTag={staffVisibilityTag}
+ isAutoHidden={isAutoHidden}
+ isRemoved={isRemoved}
+ nixPlugin={nixPlugin}
+ hasPluginBundle={hasPluginBundle}
+ configRequirements={configRequirements}
+ cliHelp={cliHelp}
+ clawdis={clawdis}
+ category={relatedCategory}
+ categories={relatedCategories}
+ staffVisibilityAlert={staffVisibilityAlert}
+ securityAuditSummary={securitySummary}
+ activityTrend={activityTrend}
+ activityTrendLoading={activityTrendLoading}
+ newVersionHref={newVersionHref}
+ settingsHref={settingsHref}
+ showArchiveMetadata={!isGitHubBackedSkill}
+ pageOverlays={
+ <>
+ void submitReport()}
/>
- undefined)}
/>
-
-
+ >
+ }
+ >
+ {nixSnippet ? (
+
+ Install via Nix
+ {nixSnippet}
+
+ ) : null}
- void submitReport()}
+ {configExample ? (
+
+ Config example
+ {configExample}
+
+ ) : null}
+
+ setShouldPrefetchCompare(true)}
+ readmeContent={readmeContent}
+ readmeError={displayedReadmeError}
+ skillCardContent={displayedSkillCard}
+ skillCardError={displayedSkillCardError}
+ hasSkillCard={hasSkillCard}
+ latestFiles={latestFiles}
+ latestVersionId={latestVersion?._id ?? null}
+ latestVersion={latestVersion?.version ?? null}
+ canDeleteVersions={canDeleteSkillVersions}
+ skill={skill as Doc<"skills">}
+ ownerHandle={ownerHandle}
+ diffVersions={diffVersions}
+ versions={versions}
+ nixPlugin={Boolean(nixPlugin)}
+ showArchiveTabs={!isGitHubBackedSkill}
+ suppressVersionScanResults={suppressVersionScanResults}
+ scanResultsSuppressedMessage={scanResultsSuppressedMessage}
+ clawdis={clawdis}
+ osLabels={osLabels}
+ readmeHrefResolver={readmeHrefResolver}
/>
- undefined)}
+
-
+
);
}
diff --git a/src/components/SkillHeader.test.tsx b/src/components/SkillDetailPageView.interactions.test.tsx
similarity index 98%
rename from src/components/SkillHeader.test.tsx
rename to src/components/SkillDetailPageView.interactions.test.tsx
index 0f993aab..0f9c045a 100644
--- a/src/components/SkillHeader.test.tsx
+++ b/src/components/SkillDetailPageView.interactions.test.tsx
@@ -5,7 +5,7 @@ import type { ReactNode } from "react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { Id } from "../../convex/_generated/dataModel";
import type { PublicPublisher, PublicSkill } from "../lib/publicUser";
-import { SkillHeader } from "./SkillHeader";
+import { SkillDetailPageView } from "./SkillDetailPageView";
import { TooltipProvider } from "./ui/tooltip";
vi.mock("@tanstack/react-router", () => ({
@@ -31,7 +31,7 @@ vi.mock("../lib/useHeroCreatorPublisher", () => ({
useHeroCreatorPublisher: ({ owner }: { owner?: PublicPublisher | null }) => owner,
}));
-describe("SkillHeader", () => {
+describe("SkillDetailPageView", () => {
function sidebarStatsRoot(container: HTMLElement) {
const node = container.querySelector(".detail-sidebar-stats");
if (!node) throw new Error("Missing .detail-sidebar-stats");
@@ -121,8 +121,8 @@ describe("SkillHeader", () => {
linkedUserId: "users:owner" as Id<"users">,
};
- function renderHeader(overrides: Partial
[0]> = {}) {
- const props: Parameters[0] = {
+ function renderHeader(overrides: Partial[0]> = {}) {
+ const props: Parameters[0] = {
skill,
owner,
ownerHandle: "local",
@@ -157,7 +157,7 @@ describe("SkillHeader", () => {
return render(
-
+
,
);
}
@@ -387,7 +387,7 @@ describe("SkillHeader", () => {
isAuthenticated: true,
settingsHref: "/local/demo/settings",
newVersionHref: "/skills/publish?updateSlug=demo&ownerHandle=local",
- } as Partial[0]>);
+ } as Partial[0]>);
const newVersionLink = screen.getByRole("link", { name: "New version" });
const settingsLink = screen.getByRole("link", { name: "Settings" });
@@ -448,7 +448,7 @@ describe("SkillHeader", () => {
isAuthenticated: true,
settingsHref: "/local/demo/settings",
newVersionHref: "/skills/publish?updateSlug=demo&ownerHandle=local",
- } as Partial[0]>);
+ } as Partial[0]>);
expect(screen.queryByRole("button", { name: "Report" })).toBeNull();
});
@@ -460,7 +460,7 @@ describe("SkillHeader", () => {
isStaff: true,
settingsHref: "/local/demo/settings",
newVersionHref: "/skills/publish?updateSlug=demo&ownerHandle=local",
- } as Partial[0]>);
+ } as Partial[0]>);
expect(screen.getByRole("button", { name: "Report" })).toBeTruthy();
});
diff --git a/src/components/SkillDetailPageView.test.tsx b/src/components/SkillDetailPageView.test.tsx
new file mode 100644
index 00000000..5bb0994b
--- /dev/null
+++ b/src/components/SkillDetailPageView.test.tsx
@@ -0,0 +1,160 @@
+/* @vitest-environment jsdom */
+
+import { render, screen } from "@testing-library/react";
+import type { ReactNode } from "react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { SkillDetailPageView, type SkillDetailPageViewProps } from "./SkillDetailPageView";
+import { TooltipProvider } from "./ui/tooltip";
+
+vi.mock("@tanstack/react-router", () => ({
+ Link: ({ children, to }: { children?: ReactNode; to?: string }) => (
+ {children}
+ ),
+}));
+
+vi.mock("../lib/useHeroCreatorPublisher", () => ({
+ useHeroCreatorPublisher: () => null,
+}));
+
+describe("SkillDetailPageView", () => {
+ beforeEach(() => {
+ vi.stubGlobal("matchMedia", () => ({
+ matches: false,
+ media: "",
+ onchange: null,
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ }));
+ });
+
+ it("owns the whole shared detail page while rendering source-specific slots", () => {
+ const props: SkillDetailPageViewProps = {
+ skill: {
+ slug: "demo",
+ displayName: "Demo Skill",
+ summary: "Shared detail page",
+ stats: { downloads: 12, stars: 0 },
+ updatedAt: 1,
+ },
+ owner: null,
+ ownerHandle: "publisher",
+ latestVersion: null,
+ modInfo: null,
+ canManage: false,
+ isAuthenticated: false,
+ isStaff: false,
+ isStarred: false,
+ onToggleStar: vi.fn(),
+ onOpenReport: vi.fn(),
+ onRequireSignIn: vi.fn(),
+ forkOf: null,
+ forkOfLabel: "fork of",
+ forkOfHref: null,
+ forkOfOwnerHandle: null,
+ canonical: null,
+ canonicalHref: null,
+ canonicalOwnerHandle: null,
+ staffVisibilityTag: null,
+ isAutoHidden: false,
+ isRemoved: false,
+ nixPlugin: undefined,
+ hasPluginBundle: false,
+ configRequirements: undefined,
+ cliHelp: undefined,
+ clawdis: undefined,
+ installContent: Source install
,
+ renderSidebarContent: () => Source provenance
,
+ children: Source tabs
,
+ };
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByRole("main").classList.contains("skill-detail-page")).toBe(true);
+ expect(screen.getByRole("heading", { name: "Demo Skill" })).toBeTruthy();
+ expect(screen.getByText("Source install")).toBeTruthy();
+ expect(screen.getByText("Source tabs")).toBeTruthy();
+ expect(screen.getAllByText("Source provenance")).toHaveLength(2);
+ });
+
+ it("omits the fallback title icon and renders source metadata before taxonomy", () => {
+ const { container } = render(
+
+ Synced from skills.sh}
+ categories={[{ slug: "development", label: "Development", icon: "wrench", keywords: [] }]}
+ />
+ ,
+ );
+
+ expect(container.querySelector(".marketplace-icon")).toBeNull();
+ const taxonomy = container.querySelector(".skill-hero-taxonomy-row");
+ expect(taxonomy).toBeTruthy();
+ expect(taxonomy?.textContent).toContain("Synced from skills.sh");
+ expect(taxonomy?.textContent).toContain("Development");
+ expect(taxonomy?.querySelector(".skill-hero-taxonomy-separator")).toBeTruthy();
+ });
+
+ it("renders the supplied title icon", () => {
+ const icon = `/api/v1/skill-icons/${"a".repeat(64)}`;
+ const { container } = render(
+
+
+ ,
+ );
+
+ expect(container.querySelector(".marketplace-icon img")?.getAttribute("src")).toBe(icon);
+ });
+});
+
+function makeMinimalProps(): SkillDetailPageViewProps {
+ return {
+ skill: {
+ slug: "demo",
+ displayName: "Demo Skill",
+ summary: "Shared detail page",
+ icon: null,
+ stats: { downloads: 12, stars: 0 },
+ updatedAt: 1,
+ },
+ owner: null,
+ ownerHandle: "publisher",
+ latestVersion: null,
+ modInfo: null,
+ canManage: false,
+ isAuthenticated: false,
+ isStaff: false,
+ isStarred: false,
+ onToggleStar: vi.fn(),
+ onOpenReport: vi.fn(),
+ onRequireSignIn: vi.fn(),
+ forkOf: null,
+ forkOfLabel: "fork of",
+ forkOfHref: null,
+ forkOfOwnerHandle: null,
+ canonical: null,
+ canonicalHref: null,
+ canonicalOwnerHandle: null,
+ staffVisibilityTag: null,
+ isAutoHidden: false,
+ isRemoved: false,
+ nixPlugin: undefined,
+ hasPluginBundle: false,
+ configRequirements: undefined,
+ cliHelp: undefined,
+ clawdis: undefined,
+ };
+}
diff --git a/src/components/SkillDetailPageView.tsx b/src/components/SkillDetailPageView.tsx
new file mode 100644
index 00000000..d1405bc8
--- /dev/null
+++ b/src/components/SkillDetailPageView.tsx
@@ -0,0 +1,838 @@
+import { Link } from "@tanstack/react-router";
+import type { ClawdisSkillMetadata } from "clawhub-schema";
+import { PLATFORM_SKILL_LICENSE } from "clawhub-schema/licenseConstants";
+import { Bookmark, Flag, Settings, ShieldCheck, Upload } from "lucide-react";
+import { useState, type ReactNode } from "react";
+import type { Doc, Id } from "../../convex/_generated/dataModel";
+import type { ActivityTrend } from "../lib/activityTrend";
+import { getSkillBadges, isSkillOfficial } from "../lib/badges";
+import { BrowseCategoryIcon } from "../lib/browseCategoryIcons";
+import {
+ buildSkillCategoryBrowseHref,
+ buildSkillTopicBrowseHref,
+ formatCatalogTopicLabel,
+ type SkillCategory,
+} from "../lib/categories";
+import { formatSkillStatsTriplet } from "../lib/numberFormat";
+import { buildPublisherProfileHref } from "../lib/ownerRoute";
+import { presentationTitle } from "../lib/presentationTitle";
+import type { PublicPublisher, PublicSkill } from "../lib/publicUser";
+import { timeAgo } from "../lib/timeAgo";
+import { useHeroCreatorPublisher } from "../lib/useHeroCreatorPublisher";
+import { useMediaQuery } from "../lib/useMediaQuery";
+import { cn } from "../lib/utils";
+import { ActivityMetricLabel } from "./ActivityMetricLabel";
+import { DetailHero, DetailPageShell, DETAIL_HERO_TOPIC_LIMIT } from "./DetailPageShell";
+import { DetailSecuritySummaryLabel } from "./DetailSecuritySummary";
+import { useDownloadsSidebarMetricBlock } from "./DownloadsMetricCard";
+import { InlineCodeSummary } from "./InlineCodeSummary";
+import { MarketplaceIcon } from "./MarketplaceIcon";
+import { SidebarMetadata } from "./SidebarMetadata";
+import { buildSkillHref } from "./skillDetailUtils";
+import { SkillCommandLineCard } from "./SkillInstallSurface";
+import { Badge } from "./ui/badge";
+import { Button } from "./ui/button";
+import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
+import { UserBadge } from "./UserBadge";
+
+type SkillModerationInfo = {
+ isPendingScan: boolean;
+ isMalwareBlocked: boolean;
+ isSuspicious: boolean;
+ isHiddenByMod: boolean;
+ isRemoved: boolean;
+ overrideActive?: boolean;
+ verdict?: "clean" | "suspicious" | "malicious";
+ reason?: string;
+};
+
+type SkillFork = {
+ kind: "fork" | "duplicate";
+ version: string | null;
+ skill: { slug: string; displayName: string };
+ owner: { handle: string | null; userId: Id<"users"> | null };
+};
+
+type SkillCanonical = {
+ skill: { slug: string; displayName: string };
+ owner: { handle: string | null; userId: Id<"users"> | null };
+};
+
+const SUMMARY_COLLAPSE_THRESHOLD = 220;
+
+type MobileDetailPanel = "content" | "stats";
+
+function formatHeaderTopic(topic: string) {
+ return formatCatalogTopicLabel(topic);
+}
+
+type SkillDetailLatestVersion =
+ | (Omit, "parsed"> & {
+ parsed?: (Partial["parsed"]> & { description?: string }) | null;
+ })
+ | null;
+
+function getLatestVersionDescription(latestVersion: SkillDetailLatestVersion) {
+ const parsed = latestVersion?.parsed;
+ const description =
+ typeof parsed?.presentation?.summary === "string"
+ ? parsed.presentation.summary
+ : typeof parsed?.description === "string"
+ ? parsed.description
+ : typeof parsed?.frontmatter?.description === "string"
+ ? parsed.frontmatter.description
+ : null;
+ return description?.trim() || null;
+}
+
+export type SkillDetailViewSkill = {
+ _id?: string;
+ slug: string;
+ displayName: string;
+ summary?: string | null;
+ icon?: string | null;
+ ownerUserId?: Id<"users">;
+ ownerPublisherId?: Id<"publishers">;
+ installKind?: PublicSkill["installKind"];
+ githubSourceRepo?: string;
+ githubPath?: string;
+ githubCurrentCommit?: string;
+ githubCurrentStatus?: PublicSkill["githubCurrentStatus"];
+ githubScanStatus?: PublicSkill["githubScanStatus"];
+ githubHasSkillCard?: boolean;
+ categories?: string[];
+ inferredCategories?: string[];
+ latestVersionId?: Id<"skillVersions">;
+ inferredFromVersionId?: Id<"skillVersions">;
+ topics?: string[];
+ badges?: PublicSkill["badges"];
+ stats: {
+ downloads: number;
+ stars: number;
+ installs?: number | null;
+ installsAllTime?: number;
+ installsCurrent?: number;
+ versions?: number;
+ comments?: number;
+ };
+ updatedAt: number;
+};
+
+function getGitHubRepositoryLink(skill: SkillDetailViewSkill) {
+ const repo = "githubSourceRepo" in skill ? skill.githubSourceRepo : undefined;
+ if (skill.installKind !== "github" || !repo) return null;
+
+ return (
+
+ {repo}
+
+ );
+}
+
+export type SkillDetailPageViewProps = {
+ skill: SkillDetailViewSkill;
+ owner: PublicPublisher | null;
+ ownerHandle: string | null;
+ latestVersion: SkillDetailLatestVersion;
+ modInfo: SkillModerationInfo | null;
+ canManage: boolean;
+ isAuthenticated: boolean;
+ isStaff: boolean;
+ isStarred: boolean | undefined;
+ onToggleStar: () => void;
+ onOpenReport: () => void;
+ onRequireSignIn: () => void;
+ forkOf: SkillFork | null;
+ forkOfLabel: string;
+ forkOfHref: string | null;
+ forkOfOwnerHandle: string | null;
+ canonical: SkillCanonical | null;
+ canonicalHref: string | null;
+ canonicalOwnerHandle: string | null;
+ staffVisibilityTag: string | null;
+ isAutoHidden: boolean;
+ isRemoved: boolean;
+ nixPlugin: string | undefined;
+ hasPluginBundle: boolean;
+ configRequirements: ClawdisSkillMetadata["config"] | undefined;
+ cliHelp: string | undefined;
+ clawdis: ClawdisSkillMetadata | undefined;
+ category?: SkillCategory | null;
+ categories?: SkillCategory[] | null;
+ staffVisibilityAlert?: ReactNode;
+ postInstallContent?: ReactNode;
+ securityAuditSummary?: ReactNode;
+ activityTrend?: ActivityTrend | null;
+ activityTrendLoading?: boolean;
+ newVersionHref?: string | null;
+ settingsHref?: string | null;
+ showArchiveMetadata?: boolean;
+ titleAccessory?: ReactNode;
+ breadcrumbOwnerHref?: string | null;
+ breadcrumbOwnerLabel?: ReactNode;
+ breadcrumbSkillHref?: string;
+ creatorContent?: ReactNode;
+ heroNotice?: ReactNode;
+ taxonomyPrefix?: ReactNode;
+ installContent?: ReactNode;
+ renderSidebarContent?: () => ReactNode;
+ showBookmarkAction?: boolean;
+ showReportAction?: boolean;
+ pageClassName?: string;
+ pageOverlays?: ReactNode;
+ children?: ReactNode;
+};
+
+export function SkillDetailPageView({
+ skill,
+ owner,
+ ownerHandle,
+ latestVersion,
+ modInfo,
+ canManage,
+ isAuthenticated,
+ isStaff,
+ isStarred,
+ onToggleStar,
+ onOpenReport,
+ onRequireSignIn,
+ forkOf,
+ forkOfLabel,
+ forkOfHref,
+ forkOfOwnerHandle,
+ canonical,
+ canonicalHref,
+ canonicalOwnerHandle,
+ nixPlugin,
+ hasPluginBundle,
+ configRequirements,
+ cliHelp,
+ clawdis,
+ category,
+ categories,
+ staffVisibilityAlert,
+ postInstallContent,
+ securityAuditSummary,
+ activityTrend,
+ activityTrendLoading = false,
+ newVersionHref,
+ settingsHref,
+ showArchiveMetadata = true,
+ titleAccessory,
+ breadcrumbOwnerHref,
+ breadcrumbOwnerLabel,
+ breadcrumbSkillHref,
+ creatorContent,
+ heroNotice,
+ taxonomyPrefix,
+ installContent,
+ renderSidebarContent,
+ showBookmarkAction = true,
+ showReportAction: showReportActionOverride,
+ pageClassName,
+ pageOverlays,
+ children,
+}: SkillDetailPageViewProps) {
+ const formattedStats = formatSkillStatsTriplet(skill.stats);
+ const installOwnerId = owner?._id ?? skill.ownerPublisherId ?? skill.ownerUserId ?? null;
+ const hasOwnerActions = Boolean(newVersionHref) || Boolean(settingsHref);
+ const showReportAction = showReportActionOverride ?? (!canManage || isStaff);
+ const badges = getSkillBadges(skill);
+ const titleBadges = badges.filter((badge) => badge !== "Official");
+ const heroCreatorPublisher = useHeroCreatorPublisher({
+ owner,
+ skillOfficial: isSkillOfficial(skill),
+ });
+ const showHeroMeta = Boolean((forkOf && forkOfHref) || canonicalHref);
+ const showTitleBadges = titleBadges.length > 0;
+ const headerDescription =
+ getLatestVersionDescription(latestVersion) ?? skill.summary ?? "No summary provided.";
+ const headerTopics = (skill.topics ?? [])
+ .map((topic) => topic.trim())
+ .filter(Boolean)
+ .slice(0, DETAIL_HERO_TOPIC_LIMIT);
+ const headerCategories = (categories ?? (category ? [category] : [])).slice(0, 3);
+ const hasSummaryToggle = headerDescription.length > SUMMARY_COLLAPSE_THRESHOLD;
+ const [isSummaryExpanded, setIsSummaryExpanded] = useState(false);
+ const [mobileDetailPanel, setMobileDetailPanel] = useState("content");
+ const isMobileDetailLayout = useMediaQuery("(max-width: 900px)");
+
+ const renderStarAction = () => (
+
+
+
+ {isAuthenticated && isStarred ? "Unbookmark" : "Bookmark"}
+ {formattedStats.stars}
+
+
+ );
+
+ const renderSidebarActions = () => {
+ if (!showReportAction) return null;
+ return (
+
+
+
+
+ Report
+
+
+
+ );
+ };
+
+ const managementToolbar =
+ hasOwnerActions || isStaff || staffVisibilityAlert ? (
+
+ {staffVisibilityAlert ? (
+
{staffVisibilityAlert}
+ ) : null}
+ {hasOwnerActions || isStaff ? (
+
+ {newVersionHref ? (
+
+
+
+ New version
+
+
+ ) : null}
+ {settingsHref ? (
+
+
+
+ Settings
+
+
+ ) : null}
+ {isStaff ? (
+
+
+
+ Manage
+
+
+ ) : null}
+
+ ) : null}
+
+ ) : null;
+
+ const defaultStatsContent = () => (
+ <>
+
+ {renderSidebarActions()}
+ >
+ );
+ // Match the normal shell's CSS-exclusive desktop/mobile copies so responsive
+ // transitions never move or remount the active sidebar subtree.
+ const desktopStatsContent = renderSidebarContent ? renderSidebarContent() : defaultStatsContent();
+ const mobileStatsContent = renderSidebarContent ? renderSidebarContent() : defaultStatsContent();
+ const resolvedBreadcrumbOwnerHref =
+ breadcrumbOwnerHref === undefined
+ ? ownerHandle
+ ? buildPublisherProfileHref(ownerHandle)
+ : "#"
+ : breadcrumbOwnerHref;
+ const displayName = presentationTitle(skill.displayName, skill.slug);
+
+ return (
+
+
+ {modInfo?.isPendingScan ? (
+
+
+
Security scan in progress
+
+ Your skill is being scanned by VirusTotal. It will be visible to others once the
+ scan completes. This usually takes up to 5 minutes — grab a coffee or exfoliate your
+ shell while you wait.
+
+
+
+ ) : modInfo?.isRemoved ? (
+
+
+
Skill removed by moderator
+
This skill has been removed and is not visible to others.
+
+
+ ) : modInfo?.isHiddenByMod ? (
+
+
+
Skill hidden
+
This skill is currently hidden and not visible to others.
+
+
+ ) : null}
+
+ {managementToolbar}
+
+
+ {!isMobileDetailLayout && showBookmarkAction ? (
+
+ {renderStarAction()}
+
+ ) : null}
+ {desktopStatsContent}
+
+ }
+ main={
+ <>
+