diff --git a/proof/skill-hero-creator-pr/plugin-detail.png b/proof/skill-hero-creator-pr/plugin-detail.png
new file mode 100644
index 00000000..23fcec1a
Binary files /dev/null and b/proof/skill-hero-creator-pr/plugin-detail.png differ
diff --git a/proof/skill-hero-creator-pr/skill-detail.png b/proof/skill-hero-creator-pr/skill-detail.png
new file mode 100644
index 00000000..a9eaf94f
Binary files /dev/null and b/proof/skill-hero-creator-pr/skill-detail.png differ
diff --git a/src/__tests__/package-detail-route.test.tsx b/src/__tests__/package-detail-route.test.tsx
index b8fe19ad..00ae504e 100644
--- a/src/__tests__/package-detail-route.test.tsx
+++ b/src/__tests__/package-detail-route.test.tsx
@@ -970,10 +970,11 @@ describe("plugin detail route", () => {
const route = await loadRoute();
const Component = route.__config.component as ComponentType;
- render();
+ const { container } = render();
- expect(screen.getAllByText("Official").length).toBeGreaterThan(0);
expect(screen.getAllByLabelText("Official").length).toBeGreaterThan(0);
+ expect(container.querySelector(".skill-hero-creator .official-badge-icon-only")).toBeTruthy();
+ expect(container.querySelector(".skill-hero-title-row .official-tag")).toBeNull();
expect(screen.queryByText("Verified")).toBeNull();
});
@@ -1106,7 +1107,8 @@ describe("plugin detail route", () => {
const typeRow = sidebarRows.find((row) => row.text.includes("Code Plugin"));
expect(downloadsRow?.hasDownload).toBe(false);
expect(downloadOnlyRow).toBeTruthy();
- expect(creatorRow).toBeTruthy();
+ expect(creatorRow).toBeUndefined();
+ expect(document.querySelector(".skill-hero-creator")?.textContent).toContain("Demo Owner");
expect(typeRow).toBeTruthy();
expect(sidebarRows.at(-1)).toEqual(downloadOnlyRow);
expect(screen.getByRole("link", { name: /Download/i }).getAttribute("href")).toBe(
diff --git a/src/__tests__/skill-detail-page.test.tsx b/src/__tests__/skill-detail-page.test.tsx
index daaf60ff..0febdca3 100644
--- a/src/__tests__/skill-detail-page.test.tsx
+++ b/src/__tests__/skill-detail-page.test.tsx
@@ -1128,7 +1128,16 @@ describe("SkillDetailPage", () => {
const securityAuditLabelIndex = sidebarLabels.findIndex((label) =>
label?.startsWith("Security audit"),
);
- expect(securityAuditLabelIndex).toBe(sidebarLabels.indexOf("Creator") + 1);
+ expect(sidebarLabels).not.toContain("Creator");
+ expect(securityAuditLabelIndex).toBeGreaterThanOrEqual(0);
+ const heroCreator = document.querySelector(".skill-hero-creator");
+ expect(heroCreator).toBeTruthy();
+ const summary = document.querySelector(".skill-summary-block");
+ expect(
+ summary &&
+ heroCreator &&
+ summary.compareDocumentPosition(heroCreator) & Node.DOCUMENT_POSITION_FOLLOWING,
+ ).toBeTruthy();
expect(
screen.getByRole("button", {
name: "Security checks across malware telemetry and agentic risk",
diff --git a/src/components/DetailPageShell.tsx b/src/components/DetailPageShell.tsx
index c48851d9..a28030be 100644
--- a/src/components/DetailPageShell.tsx
+++ b/src/components/DetailPageShell.tsx
@@ -1,6 +1,8 @@
import type { ReactNode } from "react";
import { cn } from "../lib/utils";
+export const DETAIL_HERO_TOPIC_LIMIT = 4;
+
type DetailPageShellProps = {
children: ReactNode;
className?: string;
diff --git a/src/components/OfficialBadge.tsx b/src/components/OfficialBadge.tsx
index 9d487575..43c39453 100644
--- a/src/components/OfficialBadge.tsx
+++ b/src/components/OfficialBadge.tsx
@@ -14,14 +14,27 @@ export function OfficialTag({ className }: { className?: string }) {
);
}
-export function OfficialBadge({ className }: { className?: string }) {
+type OfficialBadgeProps = {
+ className?: string;
+ iconOnly?: boolean;
+ size?: number;
+};
+
+export function OfficialBadge({ className, iconOnly = false, size = 12 }: OfficialBadgeProps) {
+ if (iconOnly) {
+ const iconClassName = className
+ ? `official-badge-icon-only ${className}`
+ : "official-badge-icon-only";
+ return ;
+ }
+
return (
-
+
);
}
diff --git a/src/components/SkillHeader.test.tsx b/src/components/SkillHeader.test.tsx
index c1bdc335..c140dfca 100644
--- a/src/components/SkillHeader.test.tsx
+++ b/src/components/SkillHeader.test.tsx
@@ -27,6 +27,10 @@ vi.mock("@tanstack/react-router", () => ({
},
}));
+vi.mock("../lib/useHeroCreatorPublisher", () => ({
+ useHeroCreatorPublisher: ({ owner }: { owner?: PublicPublisher | null }) => owner,
+}));
+
describe("SkillHeader", () => {
function sidebarStatsRoot(container: HTMLElement) {
const node = container.querySelector(".detail-sidebar-stats");
@@ -34,6 +38,22 @@ describe("SkillHeader", () => {
return node as HTMLElement;
}
+ function heroCreatorRoot(container: HTMLElement) {
+ const node = container.querySelector(".skill-hero-creator");
+ if (!node) throw new Error("Missing .skill-hero-creator");
+ return node as HTMLElement;
+ }
+
+ function expectCreatorBelowSummary(container: HTMLElement) {
+ const summary = container.querySelector(".skill-summary-block");
+ const creator = container.querySelector(".skill-hero-creator");
+ expect(summary).toBeTruthy();
+ expect(creator).toBeTruthy();
+ expect(
+ summary!.compareDocumentPosition(creator!) & Node.DOCUMENT_POSITION_FOLLOWING,
+ ).toBeTruthy();
+ }
+
function setViewportWidth(width: number) {
vi.stubGlobal("matchMedia", (query: string) => {
const minWidth = /\(min-width:\s*(\d+)px\)/.exec(query)?.[1];
@@ -156,7 +176,10 @@ describe("SkillHeader", () => {
expect(onRequireSignIn).toHaveBeenCalledTimes(2);
expect(onToggleStar).not.toHaveBeenCalled();
expect(onOpenReport).not.toHaveBeenCalled();
- expect(within(sidebarStatsRoot(container)).getByText("Creator")).toBeTruthy();
+ expectCreatorBelowSummary(container);
+ expect(within(heroCreatorRoot(container)).getByText("Local")).toBeTruthy();
+ expect(within(heroCreatorRoot(container)).getByText("@local")).toBeTruthy();
+ expect(within(sidebarStatsRoot(container)).queryByText("Creator")).toBeNull();
expect(within(sidebarStatsRoot(container)).getByText("Downloads")).toBeTruthy();
expect(within(sidebarStatsRoot(container)).getByText("2")).toBeTruthy();
expect(container.querySelector('a[href="/local"]')).toBeTruthy();
@@ -184,18 +207,22 @@ describe("SkillHeader", () => {
it("keeps desktop-width sidebar details expanded at 1071px", () => {
const { container } = renderHeader();
- expect(within(sidebarStatsRoot(container)).getByText("Creator")).toBeTruthy();
+ expectCreatorBelowSummary(container);
+ expect(within(heroCreatorRoot(container)).getByText("Local")).toBeTruthy();
+ expect(within(sidebarStatsRoot(container)).queryByText("Creator")).toBeNull();
expect(within(sidebarStatsRoot(container)).getByText("Downloads")).toBeTruthy();
expect(container.querySelector(".detail-mobile-master-tab-list")).toBeTruthy();
});
- it("uses mobile master tabs and creator placement below the title below 901px", () => {
+ it("places creator below the summary and keeps it out of the sidebar below 901px", () => {
setViewportWidth(488);
const { container } = renderHeader();
- const creator = container.querySelector(".skill-hero-mobile-creator");
+ const creator = container.querySelector(".skill-hero-creator");
const statsPanel = container.querySelector("#skill-mobile-master-panel-stats");
+ expectCreatorBelowSummary(container);
expect(creator?.textContent).toContain("Local");
+ expect(creator?.textContent).toContain("@local");
expect(statsPanel?.textContent).not.toContain("Creator");
expect(statsPanel?.hasAttribute("hidden")).toBe(true);
@@ -330,7 +357,7 @@ describe("SkillHeader", () => {
expect(container.querySelectorAll(".metric-trend-marker-line")).toHaveLength(1);
});
- it("shows the Official tag in the title for official owner skills", () => {
+ it("shows the Official badge on the creator for official publishers", () => {
const { container } = renderHeader({
owner: {
...owner,
@@ -338,8 +365,9 @@ describe("SkillHeader", () => {
},
});
- expect(screen.getByText("Official")).toBeTruthy();
- expect(container.querySelector(".official-tag")).toBeTruthy();
+ const creator = container.querySelector(".skill-hero-creator");
+ expect(creator?.querySelector(".user-name-row .official-badge-icon-only")).toBeTruthy();
+ expect(container.querySelector(".skill-hero-title-row .official-tag")).toBeNull();
});
it("renders canonical topics in the detail hero", () => {
@@ -388,7 +416,8 @@ describe("SkillHeader", () => {
const { container } = renderHeader({ showArchiveMetadata: false });
expect(within(sidebarStatsRoot(container)).getByText("Downloads")).toBeTruthy();
- expect(within(sidebarStatsRoot(container)).getByText("Creator")).toBeTruthy();
+ expect(within(sidebarStatsRoot(container)).queryByText("Creator")).toBeNull();
+ expect(within(heroCreatorRoot(container)).getByText("Local")).toBeTruthy();
expect(within(sidebarStatsRoot(container)).getByText("Last updated")).toBeTruthy();
expect(screen.queryByText("Current version")).toBeNull();
expect(screen.queryByText("License")).toBeNull();
diff --git a/src/components/SkillHeader.tsx b/src/components/SkillHeader.tsx
index 5b34b5c3..549fc024 100644
--- a/src/components/SkillHeader.tsx
+++ b/src/components/SkillHeader.tsx
@@ -5,18 +5,18 @@ import { Flag, Settings, ShieldCheck, Star, 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 } from "../lib/badges";
+import { getSkillBadges, isSkillOfficial } from "../lib/badges";
import { BrowseCategoryIcon } from "../lib/browseCategoryIcons";
import { buildSkillCategoryBrowseHref, type SkillCategory } from "../lib/categories";
import { formatSkillStatsTriplet } from "../lib/numberFormat";
import { buildPublisherProfileHref } from "../lib/ownerRoute";
import type { PublicPublisher, PublicSkill } from "../lib/publicUser";
import { timeAgo } from "../lib/timeAgo";
+import { useHeroCreatorPublisher } from "../lib/useHeroCreatorPublisher";
import { ActivityMetricLabel } from "./ActivityMetricLabel";
-import { DetailHero } from "./DetailPageShell";
+import { DetailHero, DETAIL_HERO_TOPIC_LIMIT } from "./DetailPageShell";
import { DetailSecuritySummaryLabel } from "./DetailSecuritySummary";
import { useDownloadsSidebarMetricBlock } from "./DownloadsMetricCard";
-import { OfficialTag } from "./OfficialBadge";
import { SidebarMetadata } from "./SidebarMetadata";
import { buildSkillHref } from "./skillDetailUtils";
import { SkillCommandLineCard } from "./SkillInstallSurface";
@@ -48,7 +48,6 @@ type SkillCanonical = {
owner: { handle: string | null; userId: Id<"users"> | null };
};
-const MAX_HEADER_TOPICS = 5;
const SUMMARY_COLLAPSE_THRESHOLD = 220;
type MobileDetailPanel = "content" | "stats";
@@ -173,8 +172,11 @@ export function SkillHeader({
const hasOwnerActions = Boolean(newVersionHref) || Boolean(settingsHref);
const showReportAction = !canManage || isStaff;
const badges = getSkillBadges(skill);
- const isOfficial = badges.includes("Official") || owner?.official === true;
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 =
@@ -182,7 +184,7 @@ export function SkillHeader({
const headerTopics = (skill.topics ?? [])
.map((topic) => topic.trim())
.filter(Boolean)
- .slice(0, MAX_HEADER_TOPICS);
+ .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);
@@ -281,6 +283,7 @@ export function SkillHeader({
securityAuditSummary={securityAuditSummary}
activityTrend={activityTrend}
activityTrendLoading={activityTrendLoading}
+ hideCreator
/>
{renderSidebarActions()}
>
@@ -401,7 +404,6 @@ export function SkillHeader({
) : null}
{skill.displayName}
- {isOfficial ?
: null}
{showTitleBadges ? (
{titleBadges.map((badge) => (
@@ -413,18 +415,29 @@ export function SkillHeader({
) : null}
{nixPlugin ? Plugin bundle (nix) : null}
- {owner || ownerHandle ? (
-
-
+ {showHeroMeta ? (
+
) : null}
@@ -447,41 +460,27 @@ export function SkillHeader({
) : null}
+ {owner || ownerHandle ? (
+
+
+
+ ) : null}
{nixPlugin ? (
Bundles the skill pack, CLI binary, and config requirements in one Nix install.
) : null}
-
- {showHeroMeta ? (
-
- ) : null}
>
}
diff --git a/src/components/UserBadge.test.tsx b/src/components/UserBadge.test.tsx
index a13a6224..b5313149 100644
--- a/src/components/UserBadge.test.tsx
+++ b/src/components/UserBadge.test.tsx
@@ -142,6 +142,29 @@ describe("UserBadge", () => {
expect(container.querySelector(".official-tag")).toBeFalsy();
});
+ it("places the official badge beside the display name in hero creator layout", () => {
+ const { container } = render(
+
+
+ ,
+ );
+
+ const nameRow = container.querySelector(".user-name-row");
+ expect(nameRow?.querySelector(".user-name")?.textContent).toBe("OpenClaw");
+ expect(nameRow?.querySelector(".official-badge-icon-only")).toBeTruthy();
+ expect(nameRow?.querySelector(".official-badge")).toBeFalsy();
+ expect(container.querySelector(".user-badge > .official-badge")).toBeFalsy();
+ });
+
it("falls back to the legacy hover metric during rollout", () => {
expect(
getHoverTotalDownloads({
diff --git a/src/components/UserBadge.tsx b/src/components/UserBadge.tsx
index 0a83e560..4107f8d2 100644
--- a/src/components/UserBadge.tsx
+++ b/src/components/UserBadge.tsx
@@ -30,6 +30,8 @@ type UserBadgeProps = {
showHandle?: boolean;
/** Sidebar creator row: `Display Name / @handle` with muted handle suffix. */
showMutedHandle?: boolean;
+ /** Hero creator row: stack `@handle` below the display name. */
+ stackMutedHandleBelowName?: boolean;
disableTooltip?: boolean;
};
@@ -42,6 +44,7 @@ export function UserBadge({
showName = false,
showHandle = true,
showMutedHandle = false,
+ stackMutedHandleBelowName = false,
disableTooltip = false,
}: UserBadgeProps) {
const userName =
@@ -51,7 +54,10 @@ export function UserBadge({
const href = handle ? buildPublisherProfileHref(handle) : null;
const label = handle ? `@${handle}` : "user";
const image = user?.image ?? null;
- const showInlineMutedHandle = showMutedHandle && Boolean(handle) && Boolean(displayName);
+ const showStackedMutedHandle =
+ stackMutedHandleBelowName && showMutedHandle && Boolean(handle) && Boolean(displayName);
+ const showInlineMutedHandle =
+ !stackMutedHandleBelowName && showMutedHandle && Boolean(handle) && Boolean(displayName);
const resolvedShowHandle = showMutedHandle ? !displayName && Boolean(handle) : showHandle;
const hasUsefulName =
showName &&
@@ -68,6 +74,14 @@ export function UserBadge({
const userId =
user && hasOwnProperty(user, "kind") ? (user.linkedUserId ?? null) : (user?._id ?? null);
+ const officialBadge = isOfficial ? (
+
+ ) : null;
+
const badgeContent = (
<>
{prefix ? {prefix} : null}
@@ -80,7 +94,10 @@ export function UserBadge({
{hasUsefulName ? (
<>
- {displayName}
+
+ {displayName}
+ {officialBadge}
+
{showInlineMutedHandle ? (
<>
@@ -95,8 +112,11 @@ export function UserBadge({
) : null}
>
) : null}
+ {showStackedMutedHandle ? (
+ {label}
+ ) : null}
{resolvedShowHandle ? {label} : null}
- {isOfficial ? : null}
+ {isOfficial && !hasUsefulName ? officialBadge : null}
>
);
diff --git a/src/lib/useHeroCreatorPublisher.test.tsx b/src/lib/useHeroCreatorPublisher.test.tsx
new file mode 100644
index 00000000..f67dd630
--- /dev/null
+++ b/src/lib/useHeroCreatorPublisher.test.tsx
@@ -0,0 +1,44 @@
+/* @vitest-environment jsdom */
+
+import { renderHook } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import type { PublicPublisher } from "./publicUser";
+import { useHeroCreatorPublisher } from "./useHeroCreatorPublisher";
+
+const useQueryMock = vi.fn();
+
+vi.mock("convex/react", () => ({
+ useQuery: (...args: unknown[]) => useQueryMock(...args),
+}));
+
+const owner = {
+ _id: "publishers:openclaw",
+ _creationTime: 1,
+ kind: "org",
+ handle: "openclaw",
+ displayName: "OpenClaw",
+} as PublicPublisher;
+
+describe("useHeroCreatorPublisher", () => {
+ beforeEach(() => {
+ useQueryMock.mockReset();
+ });
+
+ it("enriches detail owners from the canonical publisher lookup", () => {
+ useQueryMock.mockReturnValue({ ...owner, official: true });
+
+ const { result } = renderHook(() => useHeroCreatorPublisher({ owner }));
+
+ expect(useQueryMock.mock.calls[0]?.[1]).toEqual({ handle: "openclaw" });
+ expect(result.current).toEqual({ ...owner, official: true });
+ });
+
+ it("skips the publisher lookup when the item already proves official status", () => {
+ useQueryMock.mockReturnValue(undefined);
+
+ const { result } = renderHook(() => useHeroCreatorPublisher({ owner, skillOfficial: true }));
+
+ expect(useQueryMock.mock.calls[0]?.[1]).toBe("skip");
+ expect(result.current).toEqual({ ...owner, official: true });
+ });
+});
diff --git a/src/lib/useHeroCreatorPublisher.ts b/src/lib/useHeroCreatorPublisher.ts
new file mode 100644
index 00000000..c34d7ebf
--- /dev/null
+++ b/src/lib/useHeroCreatorPublisher.ts
@@ -0,0 +1,41 @@
+import { useQuery } from "convex/react";
+import { api } from "../../convex/_generated/api";
+
+type HeroCreatorPublisher = {
+ _id?: string;
+ _creationTime?: number;
+ kind?: "user" | "org";
+ handle?: string | null;
+ displayName?: string | null;
+ image?: string | null;
+ bio?: string | null;
+ linkedUserId?: string;
+ official?: boolean;
+};
+
+type UseHeroCreatorPublisherArgs = {
+ owner: HeroCreatorPublisher | null | undefined;
+ skillOfficial?: boolean;
+ packageOfficial?: boolean;
+};
+
+export function useHeroCreatorPublisher({
+ owner,
+ skillOfficial = false,
+ packageOfficial = false,
+}: UseHeroCreatorPublisherArgs) {
+ const shouldLookupPublisherOfficial =
+ Boolean(owner?.handle) && owner?.official !== true && !skillOfficial && !packageOfficial;
+ const publisherOfficialLookup = useQuery(
+ api.publishers.getByHandle,
+ shouldLookupPublisherOfficial && owner?.handle ? { handle: owner.handle } : "skip",
+ ) as HeroCreatorPublisher | null | undefined;
+
+ if (!owner) return owner;
+ const showOfficial =
+ owner.official === true ||
+ publisherOfficialLookup?.official === true ||
+ skillOfficial ||
+ packageOfficial;
+ return showOfficial ? { ...owner, official: true as const } : owner;
+}
diff --git a/src/routes/plugins/$name.tsx b/src/routes/plugins/$name.tsx
index 513623cd..72c21d6b 100644
--- a/src/routes/plugins/$name.tsx
+++ b/src/routes/plugins/$name.tsx
@@ -29,7 +29,11 @@ import { useEffect, useMemo, useState, type ReactNode } from "react";
import { toast } from "sonner";
import { api } from "../../../convex/_generated/api";
import { CatalogMetadataEditor } from "../../components/CatalogMetadataEditor";
-import { DetailHero, DetailPageShell } from "../../components/DetailPageShell";
+import {
+ DetailHero,
+ DETAIL_HERO_TOPIC_LIMIT,
+ DetailPageShell,
+} from "../../components/DetailPageShell";
import {
DetailSecuritySummary,
DetailSecuritySummaryLabel,
@@ -39,7 +43,6 @@ import { EmptyState } from "../../components/EmptyState";
import { InstallCopyButton } from "../../components/InstallCopyButton";
import { Container } from "../../components/layout/Container";
import { MarkdownPreview } from "../../components/MarkdownPreview";
-import { OfficialTag } from "../../components/OfficialBadge";
import {
PLUGIN_VERSIONS_PAGE_SIZE,
PluginVersionsPanel,
@@ -94,6 +97,7 @@ import { buildReadmeAssetBaseUrl } from "../../lib/readmeAssetBaseUrl";
import { timeAgo } from "../../lib/timeAgo";
import { useAuthStatus } from "../../lib/useAuthStatus";
import { useDeferredPackageActivityTrend } from "../../lib/useDeferredActivityTrend";
+import { useHeroCreatorPublisher } from "../../lib/useHeroCreatorPublisher";
import { useMediaQuery } from "../../lib/useMediaQuery";
type PluginDetailRateLimitState = {
@@ -1136,10 +1140,14 @@ function PluginDetailPageContent({ name, loaderData }: PluginDetailPageProps) {
const headerTopics = (pkg.topics ?? [])
.map((topic) => topic.trim())
.filter(Boolean)
- .slice(0, 5);
+ .slice(0, DETAIL_HERO_TOPIC_LIMIT);
const headerSummary = pkg.summary ?? "No summary provided.";
const hasSummaryToggle = headerSummary.length > 220;
const owner = detail.owner;
+ const heroCreatorPublisher = useHeroCreatorPublisher({
+ owner,
+ packageOfficial: pkg.isOfficial === true,
+ });
const latestRelease = version?.version ?? null;
const isDownloadBlocked =
pkg.scanStatus === "malicious" || latestRelease?.verification?.scanStatus === "malicious";
@@ -1431,24 +1439,20 @@ function PluginDetailPageContent({ name, loaderData }: PluginDetailPageProps) {
);
})()
: null;
- const ownerMetadataValue = owner ? (
+ const pluginHeroCreator = heroCreatorPublisher ? (
) : null;
- const hasSourceMetadata = Boolean(
- sourceRepoLink || ownerMetadataValue || latestRelease || pkg.latestVersion,
- );
+ const hasSourceMetadata = Boolean(sourceRepoLink || owner || latestRelease || pkg.latestVersion);
const securitySummary = latestRelease ? (
{pkg.displayName}
- {pkg.isOfficial ? (
-
-
-
- ) : null}
{isDownloadBlocked ? (
Download blocked
@@ -1665,6 +1663,9 @@ function PluginDetailPageContent({ name, loaderData }: PluginDetailPageProps) {
) : null}
+ {pluginHeroCreator ? (
+ {pluginHeroCreator}
+ ) : null}
{rateLimited?.scope === "metadata" ? (
diff --git a/src/styles.css b/src/styles.css
index a17c36f9..67fe658c 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -5984,6 +5984,7 @@ code {
.skill-hero-layout.has-sidebar {
grid-template-columns: minmax(0, 1fr) minmax(300px, 360px);
column-gap: clamp(56px, 7vw, 96px);
+ row-gap: 64px;
}
.skill-hero-layout.has-sidebar > .skill-hero-main {
@@ -6221,10 +6222,82 @@ code {
align-items: flex-start;
}
-.skill-hero-mobile-creator {
+.skill-hero-creator {
+ display: inline-flex;
+ min-width: 0;
+ margin-top: 12px;
+}
+
+.skill-hero-title > .skill-summary-block:has(+ .skill-hero-creator) {
+ margin-bottom: 0;
+}
+
+.skill-hero-creator .user-badge,
+.skill-hero-creator .user-badge-link {
+ display: inline-grid;
+ grid-template-columns: auto minmax(0, 1fr);
+ grid-template-rows: auto auto;
+ column-gap: 10px;
+ row-gap: 2px;
+ align-items: center;
+ min-width: 0;
+ color: var(--ink-soft);
+ text-decoration: none;
+}
+
+.skill-hero-creator .user-avatar {
+ grid-column: 1;
+ grid-row: 1 / span 2;
+ align-self: center;
+}
+
+.skill-hero-creator .user-name-row {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ grid-column: 2;
+ grid-row: 1;
+ align-self: center;
+ min-width: 0;
+}
+
+.skill-hero-creator .user-name {
+ color: var(--ink);
+ font-weight: 680;
+ font-size: 0.88rem;
+ line-height: 1.15;
+}
+
+.skill-hero-creator .official-badge-icon-only {
+ flex: 0 0 auto;
+ color: var(--official-fg);
+}
+
+.skill-hero-creator .user-name-sep {
display: none;
}
+.skill-hero-creator .user-handle {
+ grid-column: 2;
+ grid-row: 2;
+ color: var(--ink-soft);
+ font-weight: 560;
+ font-size: 0.8rem;
+ line-height: 1.15;
+}
+
+.skill-hero-creator .user-badge-link:hover,
+.skill-hero-creator .user-badge-link:focus-visible {
+ text-decoration: none;
+}
+
+.skill-hero-creator .user-badge-link:hover .user-name,
+.skill-hero-creator .user-badge-link:focus-visible .user-name,
+.skill-hero-creator .user-badge-link:hover .user-handle,
+.skill-hero-creator .user-badge-link:focus-visible .user-handle {
+ text-decoration: none;
+}
+
.plugin-catalog-empty-alert {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
@@ -6876,7 +6949,7 @@ code {
.detail-mobile-master-tab-list[hidden],
.detail-mobile-master-panel[hidden],
-.skill-hero-mobile-creator[hidden] {
+.skill-hero-creator[hidden] {
display: none !important;
}
@@ -7407,11 +7480,44 @@ code {
display: flex;
flex-wrap: wrap;
align-items: center;
- gap: 4px 8px;
+ gap: 8px 14px;
min-width: 0;
+ margin-top: 2px;
overflow-wrap: anywhere;
}
+.skill-hero-meta-item {
+ display: inline-flex;
+ align-items: baseline;
+ flex-wrap: wrap;
+ gap: 4px;
+ min-width: 0;
+ font-size: 0.82rem;
+ line-height: 1.3;
+}
+
+.skill-hero-meta-label {
+ color: var(--ink-soft);
+ font-weight: 520;
+}
+
+.skill-hero-meta-link {
+ color: var(--accent);
+ font-weight: 560;
+ text-decoration: none;
+}
+
+.skill-hero-meta-link:hover,
+.skill-hero-meta-link:focus-visible {
+ color: var(--accent);
+ text-decoration: underline;
+}
+
+.skill-hero-meta-version {
+ color: var(--ink-soft);
+ font-weight: 520;
+}
+
.skill-hero-badges {
display: flex;
flex-wrap: wrap;
@@ -8246,33 +8352,20 @@ code {
gap: 10px;
}
- .skill-hero-mobile-creator {
- display: inline-flex;
- align-items: center;
+ :is(.skill-detail-page, .plugin-detail-page) .skill-summary-block {
+ margin-top: 4px;
+ }
+
+ :is(.skill-detail-page, .plugin-detail-page) .skill-hero-creator {
+ margin-top: 18px;
+ }
+
+ .skill-detail-page .skill-hero-lower.has-sidebar {
+ order: 3;
+ display: grid;
+ gap: 12px;
+ width: 100%;
min-width: 0;
- color: var(--ink-soft);
- }
-
- .skill-hero-mobile-creator .user-badge {
- min-width: 0;
- }
-
- .skill-hero-mobile-creator .user-badge-md .user-name,
- .skill-hero-mobile-creator .user-badge-md .user-name-sep,
- .skill-hero-mobile-creator .user-badge-md .user-handle {
- font-size: 0.88rem;
- line-height: 1.15;
- }
-
- .skill-hero-mobile-creator .user-badge-md .user-name {
- color: var(--ink);
- font-weight: 680;
- }
-
- .skill-hero-mobile-creator .user-badge-md .user-name-sep,
- .skill-hero-mobile-creator .user-badge-md .user-handle {
- color: var(--ink-soft);
- font-weight: 560;
}
.detail-mobile-master-tabs {