import { useMemo, useState } from "react"; import type { Meta, StoryObj } from "@storybook/react-vite"; import { DiscoveryGrid, type DiscoveryCard, type DiscoveryCategory } from "@/pages/CompanySkills"; type DiscoveryTab = "all" | "installed" | "catalog" | "bundled"; type DiscoverySort = "agents" | "stars" | "forks" | "recent" | "alphabetical"; const MOCK_CARDS: DiscoveryCard[] = [ { key: "paperclipai/paperclip/paperclip-dev", skillId: "s-dev", catalogRef: null, name: "paperclip-dev", slug: "paperclip-dev", author: "Paperclip", version: "v2.4.1", description: "Develop and operate a local Paperclip instance.", categories: ["devops", "coding"], iconUrl: null, color: null, starCount: 48, agentCount: 12, forkCount: 3, installed: true, required: false, forkedFrom: false, updatedAt: Date.now() - 2 * 86_400_000, sourceBadge: "github", }, { key: "here.now/agent-browser", skillId: "s-browser", catalogRef: null, name: "agent-browser", slug: "agent-browser", author: "here.now", version: "v0.8.2", description: "Browser automation CLI for AI agents.", categories: ["research", "browsers"], iconUrl: null, color: null, starCount: 81, agentCount: 9, forkCount: 11, installed: false, required: false, forkedFrom: false, updatedAt: Date.now() - 5 * 86_400_000, sourceBadge: "skills_sh", }, { key: "paperclipai/paperclip/verify", skillId: "s-verify", catalogRef: null, name: "verify", slug: "verify", author: "Paperclip", version: "v1.0.3", description: "Verify a code change works by running the app.", categories: ["testing"], iconUrl: null, color: null, starCount: 22, agentCount: 7, forkCount: 1, installed: true, required: false, forkedFrom: false, updatedAt: Date.now() - 9 * 86_400_000, sourceBadge: "local", }, { key: "you/hue-prosumer", skillId: "s-hue", catalogRef: null, name: "hue-prosumer", slug: "hue-prosumer", author: "you", version: "v0.1.0", description: "Generate design language skills, prosumer remix.", categories: ["design"], iconUrl: null, color: null, starCount: 4, agentCount: 2, forkCount: 0, installed: true, required: false, forkedFrom: true, updatedAt: Date.now() - 1 * 86_400_000, sourceBadge: "paperclip", }, { key: "anthropic/claude-api", skillId: null, catalogRef: "c-claude-api", name: "claude-api", slug: "claude-api", author: "Anthropic", version: "v1.6.0", description: "Build, debug and optimize Claude API apps.", categories: ["coding", "ai"], iconUrl: null, color: null, starCount: 0, agentCount: 0, forkCount: 0, installed: false, required: false, forkedFrom: false, updatedAt: 0, sourceBadge: "url", }, { key: "paperclipai/paperclip/security-review", skillId: "s-sec", catalogRef: null, name: "security-review", slug: "security-review", author: "Paperclip", version: "v1.1.0", description: "Security review of pending changes on a branch.", categories: ["security"], iconUrl: null, color: null, starCount: 29, agentCount: 5, forkCount: 2, installed: true, required: false, forkedFrom: false, updatedAt: Date.now() - 3 * 86_400_000, sourceBadge: "github", }, { key: "yuki/commit-perfect", skillId: null, catalogRef: "c-commit", name: "commit-perfect", slug: "commit-perfect", author: "Yuki", version: "v3.0.1", description: "Polish commits and PR titles before review.", categories: ["git", "workflow"], iconUrl: null, color: null, starCount: 0, agentCount: 0, forkCount: 0, installed: false, required: false, forkedFrom: false, updatedAt: 0, sourceBadge: "skills_sh", }, { key: "astra/deep-research", skillId: "s-research", catalogRef: null, name: "deep-research", slug: "deep-research", author: "Astra", version: "v2.0.0", description: "Multi-source research with citation-grade synthesis.", categories: ["research"], iconUrl: null, color: null, starCount: 211, agentCount: 31, forkCount: 17, installed: true, required: false, forkedFrom: false, updatedAt: Date.now() - 4 * 86_400_000, sourceBadge: "local", }, { key: "paperclipai/paperclip/paperclip", skillId: "s-core", catalogRef: "c-core", name: "paperclip", slug: "paperclip", author: "Paperclip", version: "core", description: "Control plane API for tasks, routines, and coordination.", categories: ["core"], iconUrl: null, color: null, starCount: 0, agentCount: 12, forkCount: 0, installed: true, required: true, forkedFrom: false, updatedAt: Date.now() - 30 * 86_400_000, sourceBadge: "paperclip", }, { key: "paperclipai/paperclip/diagnose-why-work-stopped", skillId: "s-diag", catalogRef: "c-diag", name: "diagnose-why-work-stopped", slug: "diagnose-why-work-stopped", author: "Paperclip", version: "core", description: "Forensics for stalled or looping work trees.", categories: ["workflow"], iconUrl: null, color: null, starCount: 0, agentCount: 8, forkCount: 0, installed: true, required: true, forkedFrom: false, updatedAt: Date.now() - 28 * 86_400_000, sourceBadge: "url", }, ]; const DISCOVERY_TABS: DiscoveryTab[] = ["all", "installed", "catalog", "bundled"]; function cardsForTab(cards: DiscoveryCard[], tab: DiscoveryTab): DiscoveryCard[] { if (tab === "installed") return cards.filter((c) => c.installed); if (tab === "catalog") return cards.filter((c) => c.catalogRef != null); if (tab === "bundled") return cards.filter((c) => c.required); return cards; } function DiscoveryGridHarness({ initialTab = "all", cards = MOCK_CARDS, }: { initialTab?: DiscoveryTab; cards?: DiscoveryCard[]; }) { const [tab, setTab] = useState(initialTab); const [sort, setSort] = useState("agents"); const [category, setCategory] = useState(null); const [search, setSearch] = useState(""); const tabCards = useMemo(() => cardsForTab(cards, tab), [cards, tab]); const categories = useMemo(() => { const counts = new Map(); for (const card of tabCards) for (const c of card.categories) counts.set(c, (counts.get(c) ?? 0) + 1); return Array.from(counts.entries()) .map(([slug, count]) => ({ slug, count })) .sort((a, b) => b.count - a.count || a.slug.localeCompare(b.slug)); }, [tabCards]); const visible = useMemo(() => { const q = search.trim().toLowerCase(); const filtered = tabCards.filter((card) => { if (category && !card.categories.includes(category)) return false; if (!q) return true; return `${card.name} ${card.author} ${card.categories.join(" ")}`.toLowerCase().includes(q); }); const demote = tab !== "bundled"; return [...filtered].sort((a, b) => { if (demote && a.required !== b.required) return a.required ? 1 : -1; if (sort === "stars") return b.starCount - a.starCount; if (sort === "forks") return b.forkCount - a.forkCount; if (sort === "recent") return b.updatedAt - a.updatedAt; if (sort === "alphabetical") return a.name.localeCompare(b.name); return b.agentCount - a.agentCount; }); }, [tabCards, category, search, sort, tab]); const tabCounts = useMemo( () => ({ all: cards.length, installed: cards.filter((c) => c.installed).length, catalog: cards.filter((c) => c.catalogRef != null).length, bundled: cards.filter((c) => c.required).length, }), [cards], ) as Record; return ( { setTab(next); setCategory(null); }} categories={categories} categoryTotal={tabCards.length} activeCategory={category} onCategoryChange={setCategory} search={search} onSearchChange={setSearch} sort={sort} onSortChange={setSort} cards={visible} onOpenCard={() => {}} loading={false} error={null} totalCount={cards.length} onCreate={() => {}} onImport={() => {}} onBrowseCatalog={() => setTab("catalog")} onScan={() => {}} scanPending={false} scanStatus={null} /> ); } const meta: Meta = { title: "Skills Store/Discovery grid", component: DiscoveryGridHarness, parameters: { layout: "fullscreen" }, }; export default meta; type Story = StoryObj; export const AllSkills: Story = { args: { initialTab: "all" } }; export const InstalledTab: Story = { args: { initialTab: "installed" } }; export const BundledRequiredTab: Story = { args: { initialTab: "bundled" } }; export const EmptyLibrary: Story = { args: { initialTab: "all", cards: [] } };