diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index 3c21e8a00..3c471f376 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -6,6 +6,8 @@ on: paths: - 'website/**' - 'landingpage/**' + - 'skills/**' + - 'optional-skills/**' - '.github/workflows/deploy-site.yml' workflow_dispatch: @@ -34,6 +36,16 @@ jobs: cache: npm cache-dependency-path: website/package-lock.json + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install PyYAML for skill extraction + run: pip install pyyaml + + - name: Extract skill metadata for dashboard + run: python3 website/scripts/extract-skills.py + - name: Install dependencies run: npm ci working-directory: website diff --git a/.github/workflows/docs-site-checks.yml b/.github/workflows/docs-site-checks.yml index 6e4b966b2..14cdb8f6a 100644 --- a/.github/workflows/docs-site-checks.yml +++ b/.github/workflows/docs-site-checks.yml @@ -27,8 +27,11 @@ jobs: with: python-version: '3.11' - - name: Install ascii-guard - run: python -m pip install ascii-guard + - name: Install Python dependencies + run: python -m pip install ascii-guard pyyaml + + - name: Extract skill metadata for dashboard + run: python3 website/scripts/extract-skills.py - name: Lint docs diagrams run: npm run lint:diagrams diff --git a/website/docs/developer-guide/context-compression-and-caching.md b/website/docs/developer-guide/context-compression-and-caching.md index 65c0911f4..970b89448 100644 --- a/website/docs/developer-guide/context-compression-and-caching.md +++ b/website/docs/developer-guide/context-compression-and-caching.md @@ -99,9 +99,9 @@ outputs (file contents, terminal output, search results). ┌─────────────────────────────────────────────────────────────┐ │ Message list │ │ │ -│ [0..2] ← protect_first_n (system + first exchange) │ -│ [3..N] ← middle turns → SUMMARIZED │ -│ [N..end] ← tail (by token budget OR protect_last_n) │ +│ [0..2] ← protect_first_n (system + first exchange) │ +│ [3..N] ← middle turns → SUMMARIZED │ +│ [N..end] ← tail (by token budget OR protect_last_n) │ │ │ └─────────────────────────────────────────────────────────────┘ ``` diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index bbd7d4ea9..ad3267900 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -84,6 +84,11 @@ const config: Config = { position: 'left', label: 'Docs', }, + { + to: '/skills', + label: 'Skills', + position: 'left', + }, { href: 'https://hermes-agent.nousresearch.com', label: 'Home', diff --git a/website/scripts/extract-skills.py b/website/scripts/extract-skills.py new file mode 100644 index 000000000..30cf52316 --- /dev/null +++ b/website/scripts/extract-skills.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 +"""Extract skill metadata from SKILL.md files and index caches into JSON.""" + +import json +import os +from collections import Counter + +import yaml + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +LOCAL_SKILL_DIRS = [ + ("skills", "built-in"), + ("optional-skills", "optional"), +] +INDEX_CACHE_DIR = os.path.join(REPO_ROOT, "skills", "index-cache") +OUTPUT = os.path.join(REPO_ROOT, "website", "src", "data", "skills.json") + +CATEGORY_LABELS = { + "apple": "Apple", + "autonomous-ai-agents": "AI Agents", + "blockchain": "Blockchain", + "communication": "Communication", + "creative": "Creative", + "data-science": "Data Science", + "devops": "DevOps", + "dogfood": "Dogfood", + "domain": "Domain", + "email": "Email", + "feeds": "Feeds", + "gaming": "Gaming", + "gifs": "GIFs", + "github": "GitHub", + "health": "Health", + "inference-sh": "Inference", + "leisure": "Leisure", + "mcp": "MCP", + "media": "Media", + "migration": "Migration", + "mlops": "MLOps", + "note-taking": "Note-Taking", + "productivity": "Productivity", + "red-teaming": "Red Teaming", + "research": "Research", + "security": "Security", + "smart-home": "Smart Home", + "social-media": "Social Media", + "software-development": "Software Dev", + "translation": "Translation", + "other": "Other", +} + +SOURCE_LABELS = { + "anthropics_skills": "Anthropic", + "openai_skills": "OpenAI", + "claude_marketplace": "Claude Marketplace", + "lobehub": "LobeHub", +} + + +def extract_local_skills(): + skills = [] + + for base_dir, source_label in LOCAL_SKILL_DIRS: + base_path = os.path.join(REPO_ROOT, base_dir) + if not os.path.isdir(base_path): + continue + + for root, _dirs, files in os.walk(base_path): + if "SKILL.md" not in files: + continue + + skill_path = os.path.join(root, "SKILL.md") + with open(skill_path) as f: + content = f.read() + + if not content.startswith("---"): + continue + + parts = content.split("---", 2) + if len(parts) < 3: + continue + + try: + fm = yaml.safe_load(parts[1]) + except yaml.YAMLError: + continue + + if not fm or not isinstance(fm, dict): + continue + + rel = os.path.relpath(root, base_path) + category = rel.split(os.sep)[0] + + tags = [] + metadata = fm.get("metadata") + if isinstance(metadata, dict): + hermes_meta = metadata.get("hermes", {}) + if isinstance(hermes_meta, dict): + tags = hermes_meta.get("tags", []) + if not tags: + tags = fm.get("tags", []) + if isinstance(tags, str): + tags = [tags] + + skills.append({ + "name": fm.get("name", os.path.basename(root)), + "description": fm.get("description", ""), + "category": category, + "categoryLabel": CATEGORY_LABELS.get(category, category.replace("-", " ").title()), + "source": source_label, + "tags": tags or [], + "platforms": fm.get("platforms", []), + "author": fm.get("author", ""), + "version": fm.get("version", ""), + }) + + return skills + + +def extract_cached_index_skills(): + skills = [] + + if not os.path.isdir(INDEX_CACHE_DIR): + return skills + + for filename in os.listdir(INDEX_CACHE_DIR): + if not filename.endswith(".json"): + continue + + filepath = os.path.join(INDEX_CACHE_DIR, filename) + try: + with open(filepath) as f: + data = json.load(f) + except (json.JSONDecodeError, OSError): + continue + + stem = filename.replace(".json", "") + source_label = "community" + for key, label in SOURCE_LABELS.items(): + if key in stem: + source_label = label + break + + if isinstance(data, dict) and "agents" in data: + for agent in data["agents"]: + if not isinstance(agent, dict): + continue + skills.append({ + "name": agent.get("identifier", agent.get("meta", {}).get("title", "unknown")), + "description": (agent.get("meta", {}).get("description", "") or "").split("\n")[0][:200], + "category": _guess_category(agent.get("meta", {}).get("tags", [])), + "categoryLabel": "", # filled below + "source": source_label, + "tags": agent.get("meta", {}).get("tags", []), + "platforms": [], + "author": agent.get("author", ""), + "version": "", + }) + continue + + if isinstance(data, list): + for entry in data: + if not isinstance(entry, dict) or not entry.get("name"): + continue + if "skills" in entry and isinstance(entry["skills"], list): + continue + skills.append({ + "name": entry.get("name", ""), + "description": entry.get("description", ""), + "category": "uncategorized", + "categoryLabel": "", + "source": source_label, + "tags": entry.get("tags", []), + "platforms": [], + "author": "", + "version": "", + }) + + for s in skills: + if not s["categoryLabel"]: + s["categoryLabel"] = CATEGORY_LABELS.get( + s["category"], + s["category"].replace("-", " ").title() if s["category"] else "Uncategorized", + ) + + return skills + + +TAG_TO_CATEGORY = {} +for _cat, _tags in { + "software-development": [ + "programming", "code", "coding", "software-development", + "frontend-development", "backend-development", "web-development", + "react", "python", "typescript", "java", "rust", + ], + "creative": ["writing", "design", "creative", "art", "image-generation"], + "research": ["education", "academic", "research"], + "social-media": ["marketing", "seo", "social-media"], + "productivity": ["productivity", "business"], + "data-science": ["data", "data-science"], + "mlops": ["machine-learning", "deep-learning"], + "devops": ["devops"], + "gaming": ["gaming", "game", "game-development"], + "media": ["music", "media", "video"], + "health": ["health", "fitness"], + "translation": ["translation", "language-learning"], + "security": ["security", "cybersecurity"], +}.items(): + for _t in _tags: + TAG_TO_CATEGORY[_t] = _cat + + +def _guess_category(tags: list) -> str: + if not tags: + return "uncategorized" + for tag in tags: + cat = TAG_TO_CATEGORY.get(tag.lower()) + if cat: + return cat + return tags[0].lower().replace(" ", "-") + + +MIN_CATEGORY_SIZE = 4 + + +def _consolidate_small_categories(skills: list) -> list: + for s in skills: + if s["category"] in ("uncategorized", ""): + s["category"] = "other" + s["categoryLabel"] = "Other" + + counts = Counter(s["category"] for s in skills) + small_cats = {cat for cat, n in counts.items() if n < MIN_CATEGORY_SIZE} + + for s in skills: + if s["category"] in small_cats: + s["category"] = "other" + s["categoryLabel"] = "Other" + + return skills + + +def main(): + local = extract_local_skills() + external = extract_cached_index_skills() + + all_skills = _consolidate_small_categories(local + external) + + source_order = {"built-in": 0, "optional": 1} + all_skills.sort(key=lambda s: ( + source_order.get(s["source"], 2), + 1 if s["category"] == "other" else 0, + s["category"], + s["name"], + )) + + os.makedirs(os.path.dirname(OUTPUT), exist_ok=True) + with open(OUTPUT, "w") as f: + json.dump(all_skills, f, indent=2) + + print(f"Extracted {len(all_skills)} skills to {OUTPUT}") + print(f" {len(local)} local ({sum(1 for s in local if s['source'] == 'built-in')} built-in, " + f"{sum(1 for s in local if s['source'] == 'optional')} optional)") + print(f" {len(external)} from external indexes") + + +if __name__ == "__main__": + main() diff --git a/website/src/pages/skills/index.tsx b/website/src/pages/skills/index.tsx new file mode 100644 index 000000000..7e2311a6c --- /dev/null +++ b/website/src/pages/skills/index.tsx @@ -0,0 +1,582 @@ +import React, { useState, useMemo, useCallback, useRef, useEffect } from "react"; +import Layout from "@theme/Layout"; +import skills from "../../data/skills.json"; +import styles from "./styles.module.css"; + +interface Skill { + name: string; + description: string; + category: string; + categoryLabel: string; + source: string; + tags: string[]; + platforms: string[]; + author: string; + version: string; +} + +const allSkills: Skill[] = skills as Skill[]; + +const CATEGORY_ICONS: Record = { + apple: "\u{f179}", + "autonomous-ai-agents": "\u{1F916}", + blockchain: "\u{26D3}", + communication: "\u{1F4AC}", + creative: "\u{1F3A8}", + "data-science": "\u{1F4CA}", + devops: "\u{2699}", + dogfood: "\u{1F436}", + domain: "\u{1F310}", + email: "\u{2709}", + feeds: "\u{1F4E1}", + gaming: "\u{1F3AE}", + gifs: "\u{1F3AC}", + github: "\u{1F4BB}", + health: "\u{2764}", + "inference-sh": "\u{26A1}", + leisure: "\u{2615}", + mcp: "\u{1F50C}", + media: "\u{1F3B5}", + migration: "\u{1F4E6}", + mlops: "\u{1F9EA}", + "note-taking": "\u{1F4DD}", + productivity: "\u{2705}", + "red-teaming": "\u{1F6E1}", + research: "\u{1F50D}", + security: "\u{1F512}", + "smart-home": "\u{1F3E0}", + "social-media": "\u{1F4F1}", + "software-development": "\u{1F4BB}", + translation: "\u{1F30D}", + other: "\u{1F4E6}", +}; + +const SOURCE_CONFIG: Record< + string, + { label: string; color: string; bg: string; border: string; icon: string } +> = { + "built-in": { + label: "Built-in", + color: "#4ade80", + bg: "rgba(74, 222, 128, 0.08)", + border: "rgba(74, 222, 128, 0.2)", + icon: "\u{2713}", + }, + optional: { + label: "Optional", + color: "#fbbf24", + bg: "rgba(251, 191, 36, 0.08)", + border: "rgba(251, 191, 36, 0.2)", + icon: "\u{2B50}", + }, + Anthropic: { + label: "Anthropic", + color: "#d4845a", + bg: "rgba(212, 132, 90, 0.08)", + border: "rgba(212, 132, 90, 0.2)", + icon: "\u{25C6}", + }, + LobeHub: { + label: "LobeHub", + color: "#60a5fa", + bg: "rgba(96, 165, 250, 0.08)", + border: "rgba(96, 165, 250, 0.2)", + icon: "\u{25CB}", + }, + "Claude Marketplace": { + label: "Marketplace", + color: "#a78bfa", + bg: "rgba(167, 139, 250, 0.08)", + border: "rgba(167, 139, 250, 0.2)", + icon: "\u{25A0}", + }, +}; + +const SOURCE_ORDER = ["all", "built-in", "optional", "Anthropic", "LobeHub", "Claude Marketplace"]; + +function highlightMatch(text: string, query: string): React.ReactNode { + if (!query || !text) return text; + const idx = text.toLowerCase().indexOf(query.toLowerCase()); + if (idx === -1) return text; + return ( + <> + {text.slice(0, idx)} + {text.slice(idx, idx + query.length)} + {text.slice(idx + query.length)} + + ); +} + +function SkillCard({ + skill, + query, + expanded, + onToggle, + onCategoryClick, + onTagClick, + style, +}: { + skill: Skill; + query: string; + expanded: boolean; + onToggle: () => void; + onCategoryClick: (cat: string) => void; + onTagClick: (tag: string) => void; + style?: React.CSSProperties; +}) { + const src = SOURCE_CONFIG[skill.source] || SOURCE_CONFIG["optional"]; + const icon = CATEGORY_ICONS[skill.category] || "\u{1F4E6}"; + + return ( +
+
+ +
+
+ {icon} +
+

+ {highlightMatch(skill.name, query)} +

+ + {src.icon} {src.label} + +
+
+ +

+ {highlightMatch(skill.description || "No description available.", query)} +

+ +
+ + {skill.platforms?.map((p) => ( + + {p === "macos" ? "\u{F8FF} macOS" : p === "linux" ? "\u{1F427} Linux" : p} + + ))} +
+ + {expanded && ( +
+ {skill.tags?.length > 0 && ( +
+ {skill.tags.map((tag) => ( + + ))} +
+ )} + {skill.author && ( +
+ Author + {skill.author} +
+ )} + {skill.version && ( +
+ Version + {skill.version} +
+ )} +
+ hermes skills install {skill.name} +
+
+ )} +
+
+ ); +} + +function StatCard({ value, label, color }: { value: number; label: string; color: string }) { + return ( +
+ + {value} + + {label} +
+ ); +} + +const PAGE_SIZE = 60; + +export default function SkillsDashboard() { + const [search, setSearch] = useState(""); + const [sourceFilter, setSourceFilter] = useState("all"); + const [categoryFilter, setCategoryFilter] = useState("all"); + const [expandedCard, setExpandedCard] = useState(null); + const [visibleCount, setVisibleCount] = useState(PAGE_SIZE); + const [sidebarOpen, setSidebarOpen] = useState(false); + const searchRef = useRef(null); + const gridRef = useRef(null); + + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.key === "/" && document.activeElement?.tagName !== "INPUT") { + e.preventDefault(); + searchRef.current?.focus(); + } + if (e.key === "Escape") { + searchRef.current?.blur(); + setExpandedCard(null); + } + }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, []); + + const sources = useMemo(() => { + const set = new Set(allSkills.map((s) => s.source)); + return SOURCE_ORDER.filter((s) => s === "all" || set.has(s)); + }, []); + + const categoryEntries = useMemo(() => { + const pool = + sourceFilter === "all" + ? allSkills + : allSkills.filter((s) => s.source === sourceFilter); + const map = new Map(); + for (const s of pool) { + const key = s.category || "uncategorized"; + const existing = map.get(key); + if (existing) { + existing.count++; + } else { + map.set(key, { + label: s.categoryLabel || s.category || "Uncategorized", + count: 1, + }); + } + } + return Array.from(map.entries()) + .sort((a, b) => b[1].count - a[1].count) + .map(([key, { label, count }]) => ({ key, label, count })); + }, [sourceFilter]); + + const filtered = useMemo(() => { + const q = search.toLowerCase().trim(); + return allSkills.filter((s) => { + if (sourceFilter !== "all" && s.source !== sourceFilter) return false; + if (categoryFilter !== "all" && s.category !== categoryFilter) return false; + if (q) { + const haystack = [s.name, s.description, s.categoryLabel, s.author, ...(s.tags || [])] + .join(" ") + .toLowerCase(); + return haystack.includes(q); + } + return true; + }); + }, [search, sourceFilter, categoryFilter]); + + useEffect(() => { + setVisibleCount(PAGE_SIZE); + setExpandedCard(null); + }, [search, sourceFilter, categoryFilter]); + + const visible = filtered.slice(0, visibleCount); + const hasMore = visibleCount < filtered.length; + + const handleSourceChange = useCallback( + (src: string) => { + setSourceFilter(src); + setCategoryFilter("all"); + }, + [] + ); + + const handleCategoryClick = useCallback((cat: string) => { + setCategoryFilter(cat); + gridRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }); + setSidebarOpen(false); + }, []); + + const handleTagClick = useCallback((tag: string) => { + setSearch(tag); + searchRef.current?.focus(); + }, []); + + const clearAll = useCallback(() => { + setSearch(""); + setSourceFilter("all"); + setCategoryFilter("all"); + }, []); + + return ( + +
+
+
+
+

Hermes Agent

+

Skills Hub

+

+ Discover, search, and install from{" "} + {allSkills.length} skills + across {sources.length - 1} registries +

+ +
+ s.source === "built-in").length} + label="Built-in" + color="#4ade80" + /> + s.source === "optional").length} + label="Optional" + color="#fbbf24" + /> + s.source !== "built-in" && s.source !== "optional" + ).length + } + label="Community" + color="#60a5fa" + /> + s.category)).size} + label="Categories" + color="#a78bfa" + /> +
+
+
+ +
+
+ + + + setSearch(e.target.value)} + className={styles.searchInput} + /> + {search && ( + + )} +
+ +
+ {sources.map((src) => { + const active = sourceFilter === src; + const conf = SOURCE_CONFIG[src]; + const count = + src === "all" + ? allSkills.length + : allSkills.filter((s) => s.source === src).length; + return ( + + ); + })} +
+
+ +
+ + + + +
+ {(search || sourceFilter !== "all" || categoryFilter !== "all") && ( +
+ + {filtered.length} result{filtered.length !== 1 ? "s" : ""} + + {search && ( + + “{search}” + + + )} + {sourceFilter !== "all" && ( + + {SOURCE_CONFIG[sourceFilter]?.label || sourceFilter} + + + )} + {categoryFilter !== "all" && ( + + {categoryEntries.find((c) => c.key === categoryFilter)?.label || + categoryFilter} + + + )} + +
+ )} + + {visible.length > 0 ? ( + <> +
+ {visible.map((skill, i) => { + const key = `${skill.source}-${skill.name}-${i}`; + return ( + + setExpandedCard(expandedCard === key ? null : key) + } + onCategoryClick={handleCategoryClick} + onTagClick={handleTagClick} + style={{ animationDelay: `${Math.min(i, 20) * 25}ms` }} + /> + ); + })} +
+ {hasMore && ( +
+ +
+ )} + + ) : ( +
+
{"\u{1F50D}"}
+

No skills found

+

+ Try a different search term or clear your filters. +

+ +
+ )} +
+
+
+ + {sidebarOpen && ( +
setSidebarOpen(false)} /> + )} + + ); +} diff --git a/website/src/pages/skills/styles.module.css b/website/src/pages/skills/styles.module.css new file mode 100644 index 000000000..a1bbfd000 --- /dev/null +++ b/website/src/pages/skills/styles.module.css @@ -0,0 +1,819 @@ +@import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"); + +.page { + font-family: "DM Sans", -apple-system, BlinkMacSystemFont, sans-serif; + min-height: 100vh; +} + + +.hero { + position: relative; + overflow: hidden; + padding: 4rem 2rem 2.5rem; + text-align: center; +} + +.heroGlow { + position: absolute; + top: -120px; + left: 50%; + transform: translateX(-50%); + width: 600px; + height: 400px; + background: radial-gradient( + ellipse at center, + rgba(255, 215, 0, 0.07) 0%, + transparent 70% + ); + pointer-events: none; +} + +.heroContent { + position: relative; + z-index: 1; + max-width: 720px; + margin: 0 auto; +} + +.heroEyebrow { + font-family: "JetBrains Mono", monospace; + font-size: 0.75rem; + letter-spacing: 0.15em; + text-transform: uppercase; + color: rgba(255, 215, 0, 0.5); + margin-bottom: 0.75rem; +} + +.heroTitle { + font-size: 3rem; + font-weight: 700; + letter-spacing: -0.04em; + line-height: 1.1; + margin: 0 0 0.75rem; +} + +[data-theme="dark"] .heroTitle { + color: #fafaf6; +} + +.heroSub { + font-size: 1.05rem; + color: var(--ifm-font-color-secondary, #9a968e); + line-height: 1.5; + margin: 0 0 2rem; +} + +.heroAccent { + color: #ffd700; + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +.statsRow { + display: flex; + justify-content: center; + gap: 2.5rem; + flex-wrap: wrap; +} + +.stat { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.2rem; +} + +.statValue { + font-family: "JetBrains Mono", monospace; + font-size: 1.6rem; + font-weight: 700; + line-height: 1; +} + +.statLabel { + font-size: 0.72rem; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--ifm-font-color-secondary, #9a968e); +} + + +.controlsBar { + position: sticky; + top: 60px; /* below Docusaurus navbar */ + z-index: 50; + display: flex; + flex-direction: column; + gap: 0.75rem; + align-items: center; + padding: 1rem 2rem; + backdrop-filter: blur(16px) saturate(1.4); + border-bottom: 1px solid rgba(255, 215, 0, 0.06); +} + +[data-theme="dark"] .controlsBar { + background: rgba(7, 7, 13, 0.85); +} + +.searchWrap { + position: relative; + width: 100%; + max-width: 560px; +} + +.searchIcon { + position: absolute; + left: 0.85rem; + top: 50%; + transform: translateY(-50%); + color: rgba(255, 215, 0, 0.35); + pointer-events: none; +} + +.searchInput { + width: 100%; + padding: 0.7rem 2.5rem 0.7rem 2.6rem; + font-size: 0.95rem; + font-family: "DM Sans", sans-serif; + border: 1px solid rgba(255, 215, 0, 0.12); + border-radius: 10px; + background: rgba(15, 15, 24, 0.6); + color: var(--ifm-font-color-base, #e8e4dc); + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.searchInput:focus { + border-color: rgba(255, 215, 0, 0.4); + box-shadow: 0 0 0 3px rgba(255, 215, 0, 0.06); +} + +.searchInput::placeholder { + color: var(--ifm-font-color-secondary, #9a968e); + opacity: 0.5; +} + +.clearBtn { + position: absolute; + right: 0.6rem; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--ifm-font-color-secondary); + cursor: pointer; + padding: 0.15rem; + display: flex; + opacity: 0.6; + transition: opacity 0.15s; +} + +.clearBtn:hover { + opacity: 1; + color: #ffd700; +} + +.sourcePills { + display: flex; + gap: 0.4rem; + flex-wrap: wrap; + justify-content: center; +} + +.srcPill { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.35rem 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.07); + border-radius: 20px; + background: transparent; + color: var(--ifm-font-color-secondary, #9a968e); + font-family: "DM Sans", sans-serif; + font-size: 0.8rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.srcPill:hover { + border-color: rgba(255, 255, 255, 0.15); + color: var(--ifm-font-color-base); +} + +.srcPillActive { + border-color: var(--pill-border, rgba(255, 215, 0, 0.3)); + background: var(--pill-bg, rgba(255, 215, 0, 0.06)); + color: var(--pill-color, #ffd700); +} + +.srcCount { + font-family: "JetBrains Mono", monospace; + font-size: 0.68rem; + background: rgba(255, 255, 255, 0.05); + padding: 0.05rem 0.35rem; + border-radius: 8px; +} + +.srcPillActive .srcCount { + background: rgba(255, 255, 255, 0.08); +} + + +.layout { + display: grid; + grid-template-columns: 260px 1fr; + gap: 0; + max-width: 1440px; + margin: 0 auto; + min-height: 60vh; +} + + +.sidebar { + position: sticky; + top: 160px; + height: calc(100vh - 160px); + overflow-y: auto; + padding: 1.25rem 1rem 2rem 1.5rem; + border-right: 1px solid rgba(255, 215, 0, 0.05); +} + +.sidebar::-webkit-scrollbar { + width: 4px; +} +.sidebar::-webkit-scrollbar-thumb { + background: rgba(255, 215, 0, 0.1); + border-radius: 2px; +} + +.sidebarHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.75rem; +} + +.sidebarTitle { + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--ifm-font-color-secondary); + margin: 0; +} + +.sidebarClear { + font-family: "DM Sans", sans-serif; + font-size: 0.72rem; + color: rgba(255, 215, 0, 0.6); + background: none; + border: none; + cursor: pointer; + padding: 0; + transition: color 0.15s; +} + +.sidebarClear:hover { + color: #ffd700; +} + +.catList { + display: flex; + flex-direction: column; + gap: 1px; +} + +.catItem { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.45rem 0.6rem; + border: none; + border-radius: 6px; + background: transparent; + color: var(--ifm-font-color-secondary, #9a968e); + font-family: "DM Sans", sans-serif; + font-size: 0.82rem; + cursor: pointer; + transition: all 0.15s; + text-align: left; + width: 100%; +} + +.catItem:hover { + background: rgba(255, 215, 0, 0.04); + color: var(--ifm-font-color-base); +} + +.catItemActive { + background: rgba(255, 215, 0, 0.08); + color: #ffd700; +} + +.catItemIcon { + font-size: 0.9rem; + width: 1.3rem; + text-align: center; + flex-shrink: 0; +} + +.catItemLabel { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.catItemCount { + font-family: "JetBrains Mono", monospace; + font-size: 0.68rem; + color: rgba(255, 215, 0, 0.3); + min-width: 1.5rem; + text-align: right; +} + +.catItemActive .catItemCount { + color: rgba(255, 215, 0, 0.6); +} + +.sidebarToggle { + display: none; +} + + +.main { + padding: 1.25rem 1.5rem 3rem; + min-width: 0; +} + +.filterSummary { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; + margin-bottom: 1rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid rgba(255, 215, 0, 0.05); +} + +.filterCount { + font-size: 0.82rem; + font-weight: 600; + color: var(--ifm-font-color-base); + margin-right: 0.25rem; +} + +.filterChip { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.2rem 0.5rem; + border: 1px solid rgba(255, 215, 0, 0.15); + border-radius: 4px; + background: rgba(255, 215, 0, 0.04); + color: rgba(255, 215, 0, 0.8); + font-size: 0.75rem; +} + +.filterChip button { + background: none; + border: none; + color: inherit; + cursor: pointer; + padding: 0; + font-size: 0.85rem; + line-height: 1; + opacity: 0.6; + transition: opacity 0.15s; +} + +.filterChip button:hover { + opacity: 1; +} + +.clearAllBtn { + font-family: "DM Sans", sans-serif; + font-size: 0.75rem; + color: var(--ifm-font-color-secondary); + background: none; + border: none; + cursor: pointer; + padding: 0; + margin-left: auto; + transition: color 0.15s; +} + +.clearAllBtn:hover { + color: #ffd700; +} + + +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + gap: 0.75rem; +} + + +@keyframes cardIn { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.card { + position: relative; + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 10px; + overflow: hidden; + cursor: pointer; + transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s; + animation: cardIn 0.35s ease both; +} + +[data-theme="dark"] .card { + background: #0c0c16; +} + +.card:hover { + border-color: rgba(255, 215, 0, 0.15); + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 215, 0, 0.05); + transform: translateY(-1px); +} + +.cardExpanded { + border-color: rgba(255, 215, 0, 0.2); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 215, 0, 0.08); +} + +.cardAccent { + position: absolute; + top: 0; + left: 0; + width: 3px; + height: 100%; + opacity: 0.5; + transition: opacity 0.2s; +} + +.card:hover .cardAccent { + opacity: 1; +} + +.cardInner { + padding: 1rem 1rem 0.85rem 1.15rem; +} + +.cardTop { + display: flex; + align-items: flex-start; + gap: 0.6rem; + margin-bottom: 0.5rem; +} + +.cardIcon { + font-size: 1.15rem; + line-height: 1; + flex-shrink: 0; + margin-top: 0.1rem; + opacity: 0.7; +} + +.cardTitleGroup { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0.5rem; + flex: 1; + min-width: 0; +} + +.cardTitle { + font-size: 0.92rem; + font-weight: 600; + line-height: 1.3; + margin: 0; + word-break: break-word; + color: var(--ifm-font-color-base); +} + +.sourcePill { + display: inline-flex; + align-items: center; + gap: 0.25rem; + font-family: "JetBrains Mono", monospace; + font-size: 0.62rem; + font-weight: 500; + padding: 0.15rem 0.45rem; + border-radius: 4px; + border: 1px solid; + white-space: nowrap; + flex-shrink: 0; + margin-top: 0.1rem; +} + +.cardDesc { + font-size: 0.82rem; + line-height: 1.55; + color: var(--ifm-font-color-secondary, #9a968e); + margin: 0 0 0.6rem; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.cardDescFull { + -webkit-line-clamp: unset; +} + +.cardMeta { + display: flex; + align-items: center; + gap: 0.35rem; + flex-wrap: wrap; +} + +.catButton { + font-family: "JetBrains Mono", monospace; + font-size: 0.66rem; + padding: 0.15rem 0.45rem; + border: 1px solid rgba(255, 215, 0, 0.12); + border-radius: 3px; + background: rgba(255, 215, 0, 0.04); + color: rgba(255, 215, 0, 0.7); + cursor: pointer; + transition: all 0.15s; +} + +.catButton:hover { + background: rgba(255, 215, 0, 0.1); + color: #ffd700; + border-color: rgba(255, 215, 0, 0.25); +} + +.platformPill { + font-size: 0.66rem; + padding: 0.12rem 0.4rem; + border-radius: 3px; + background: rgba(96, 165, 250, 0.06); + color: rgba(96, 165, 250, 0.8); + border: 1px solid rgba(96, 165, 250, 0.1); +} + + +.cardDetail { + margin-top: 0.75rem; + padding-top: 0.7rem; + border-top: 1px solid rgba(255, 255, 255, 0.04); + animation: cardIn 0.2s ease both; +} + +.tagRow { + display: flex; + flex-wrap: wrap; + gap: 0.3rem; + margin-bottom: 0.65rem; +} + +.tagPill { + font-family: "DM Sans", sans-serif; + font-size: 0.68rem; + padding: 0.12rem 0.4rem; + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 3px; + background: rgba(255, 255, 255, 0.02); + color: var(--ifm-font-color-secondary); + cursor: pointer; + transition: all 0.15s; +} + +.tagPill:hover { + background: rgba(255, 215, 0, 0.06); + color: rgba(255, 215, 0, 0.8); + border-color: rgba(255, 215, 0, 0.15); +} + +.authorRow { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.3rem; +} + +.authorLabel { + font-family: "JetBrains Mono", monospace; + font-size: 0.62rem; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--ifm-font-color-secondary); + opacity: 0.5; + min-width: 3.5rem; +} + +.authorValue { + font-size: 0.78rem; + color: var(--ifm-font-color-base); +} + +.installHint { + margin-top: 0.65rem; + padding: 0.45rem 0.65rem; + background: rgba(0, 0, 0, 0.25); + border: 1px solid rgba(255, 215, 0, 0.06); + border-radius: 5px; +} + +.installHint code { + font-family: "JetBrains Mono", monospace; + font-size: 0.72rem; + color: rgba(255, 215, 0, 0.7); + background: none; + padding: 0; +} + +.highlight { + background: rgba(255, 215, 0, 0.2); + color: #ffd700; + border-radius: 2px; + padding: 0 1px; +} + + +.loadMoreWrap { + display: flex; + justify-content: center; + margin-top: 1.5rem; +} + +.loadMoreBtn { + font-family: "DM Sans", sans-serif; + font-size: 0.85rem; + font-weight: 500; + padding: 0.6rem 1.5rem; + border: 1px solid rgba(255, 215, 0, 0.2); + border-radius: 8px; + background: rgba(255, 215, 0, 0.04); + color: rgba(255, 215, 0, 0.8); + cursor: pointer; + transition: all 0.2s; +} + +.loadMoreBtn:hover { + background: rgba(255, 215, 0, 0.08); + border-color: rgba(255, 215, 0, 0.35); + color: #ffd700; +} + + +.empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 5rem 2rem; + text-align: center; +} + +.emptyIcon { + font-size: 2.5rem; + margin-bottom: 1rem; + opacity: 0.4; +} + +.emptyTitle { + font-size: 1.1rem; + font-weight: 600; + margin: 0 0 0.5rem; + color: var(--ifm-font-color-base); +} + +.emptyDesc { + font-size: 0.85rem; + color: var(--ifm-font-color-secondary); + margin: 0 0 1.25rem; +} + +.emptyReset { + font-family: "DM Sans", sans-serif; + font-size: 0.85rem; + padding: 0.5rem 1.25rem; + border: 1px solid rgba(255, 215, 0, 0.25); + border-radius: 6px; + background: transparent; + color: #ffd700; + cursor: pointer; + transition: all 0.2s; +} + +.emptyReset:hover { + background: rgba(255, 215, 0, 0.08); +} + + +.backdrop { + display: none; +} + +.activeCatBadge { + font-size: 0.72rem; + padding: 0.1rem 0.4rem; + border-radius: 3px; + background: rgba(255, 215, 0, 0.1); + color: rgba(255, 215, 0, 0.8); +} + + +@media (max-width: 900px) { + .layout { + grid-template-columns: 1fr; + } + + .sidebar { + display: none; + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: 280px; + z-index: 200; + background: #0a0a14; + border-right: 1px solid rgba(255, 215, 0, 0.1); + padding-top: 1.5rem; + height: 100vh; + } + + .sidebarOpen { + display: block; + } + + .backdrop { + display: block; + position: fixed; + inset: 0; + z-index: 190; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4px); + } + + .sidebarToggle { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.5rem 0.85rem; + margin: 0 1rem 0.75rem; + border: 1px solid rgba(255, 215, 0, 0.1); + border-radius: 6px; + background: rgba(255, 215, 0, 0.03); + color: var(--ifm-font-color-secondary); + font-family: "DM Sans", sans-serif; + font-size: 0.82rem; + cursor: pointer; + transition: all 0.15s; + } + + .sidebarToggle:hover { + border-color: rgba(255, 215, 0, 0.2); + color: var(--ifm-font-color-base); + } + + .hero { + padding: 2.5rem 1.25rem 1.75rem; + } + + .heroTitle { + font-size: 2rem; + } + + .statsRow { + gap: 1.5rem; + } + + .statValue { + font-size: 1.25rem; + } + + .controlsBar { + padding: 0.75rem 1rem; + } + + .main { + padding: 0.75rem 1rem 2rem; + } + + .grid { + grid-template-columns: 1fr; + } +} + +@media (min-width: 901px) and (max-width: 1100px) { + .grid { + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + } +}