mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat(website): add skills browse and search page to docs (#4500)
Adds a Skills Hub page to the documentation site with browsable/searchable catalog of all skills (built-in, optional, and community from cached hub indexes). - Python extraction script (website/scripts/extract-skills.py) parses SKILL.md frontmatter and hub index caches into skills.json - React page (website/src/pages/skills/) with search, category filtering, source filtering, and expandable skill cards - CI workflow updated to run extraction before Docusaurus build - Deploy trigger expanded to include skills/ and optional-skills/ changes Authored by @IAvecilla
This commit is contained in:
parent
20441cf2c8
commit
b8dd059c40
7 changed files with 1694 additions and 5 deletions
12
.github/workflows/deploy-site.yml
vendored
12
.github/workflows/deploy-site.yml
vendored
|
|
@ -6,6 +6,8 @@ on:
|
||||||
paths:
|
paths:
|
||||||
- 'website/**'
|
- 'website/**'
|
||||||
- 'landingpage/**'
|
- 'landingpage/**'
|
||||||
|
- 'skills/**'
|
||||||
|
- 'optional-skills/**'
|
||||||
- '.github/workflows/deploy-site.yml'
|
- '.github/workflows/deploy-site.yml'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
|
@ -34,6 +36,16 @@ jobs:
|
||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: website/package-lock.json
|
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
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
working-directory: website
|
working-directory: website
|
||||||
|
|
|
||||||
7
.github/workflows/docs-site-checks.yml
vendored
7
.github/workflows/docs-site-checks.yml
vendored
|
|
@ -27,8 +27,11 @@ jobs:
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: '3.11'
|
||||||
|
|
||||||
- name: Install ascii-guard
|
- name: Install Python dependencies
|
||||||
run: python -m pip install ascii-guard
|
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
|
- name: Lint docs diagrams
|
||||||
run: npm run lint:diagrams
|
run: npm run lint:diagrams
|
||||||
|
|
|
||||||
|
|
@ -99,9 +99,9 @@ outputs (file contents, terminal output, search results).
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
│ Message list │
|
│ Message list │
|
||||||
│ │
|
│ │
|
||||||
│ [0..2] ← protect_first_n (system + first exchange) │
|
│ [0..2] ← protect_first_n (system + first exchange) │
|
||||||
│ [3..N] ← middle turns → SUMMARIZED │
|
│ [3..N] ← middle turns → SUMMARIZED │
|
||||||
│ [N..end] ← tail (by token budget OR protect_last_n) │
|
│ [N..end] ← tail (by token budget OR protect_last_n) │
|
||||||
│ │
|
│ │
|
||||||
└─────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,11 @@ const config: Config = {
|
||||||
position: 'left',
|
position: 'left',
|
||||||
label: 'Docs',
|
label: 'Docs',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
to: '/skills',
|
||||||
|
label: 'Skills',
|
||||||
|
position: 'left',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: 'https://hermes-agent.nousresearch.com',
|
href: 'https://hermes-agent.nousresearch.com',
|
||||||
label: 'Home',
|
label: 'Home',
|
||||||
|
|
|
||||||
268
website/scripts/extract-skills.py
Normal file
268
website/scripts/extract-skills.py
Normal file
|
|
@ -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()
|
||||||
582
website/src/pages/skills/index.tsx
Normal file
582
website/src/pages/skills/index.tsx
Normal file
|
|
@ -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<string, string> = {
|
||||||
|
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)}
|
||||||
|
<mark className={styles.highlight}>{text.slice(idx, idx + query.length)}</mark>
|
||||||
|
{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 (
|
||||||
|
<div
|
||||||
|
className={`${styles.card} ${expanded ? styles.cardExpanded : ""}`}
|
||||||
|
onClick={onToggle}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<div className={styles.cardAccent} style={{ background: src.color }} />
|
||||||
|
|
||||||
|
<div className={styles.cardInner}>
|
||||||
|
<div className={styles.cardTop}>
|
||||||
|
<span className={styles.cardIcon}>{icon}</span>
|
||||||
|
<div className={styles.cardTitleGroup}>
|
||||||
|
<h3 className={styles.cardTitle}>
|
||||||
|
{highlightMatch(skill.name, query)}
|
||||||
|
</h3>
|
||||||
|
<span
|
||||||
|
className={styles.sourcePill}
|
||||||
|
style={{
|
||||||
|
color: src.color,
|
||||||
|
background: src.bg,
|
||||||
|
borderColor: src.border,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{src.icon} {src.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className={`${styles.cardDesc} ${expanded ? styles.cardDescFull : ""}`}>
|
||||||
|
{highlightMatch(skill.description || "No description available.", query)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className={styles.cardMeta}>
|
||||||
|
<button
|
||||||
|
className={styles.catButton}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onCategoryClick(skill.category);
|
||||||
|
}}
|
||||||
|
title={`Filter by ${skill.categoryLabel}`}
|
||||||
|
>
|
||||||
|
{skill.categoryLabel || skill.category}
|
||||||
|
</button>
|
||||||
|
{skill.platforms?.map((p) => (
|
||||||
|
<span key={p} className={styles.platformPill}>
|
||||||
|
{p === "macos" ? "\u{F8FF} macOS" : p === "linux" ? "\u{1F427} Linux" : p}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{expanded && (
|
||||||
|
<div className={styles.cardDetail}>
|
||||||
|
{skill.tags?.length > 0 && (
|
||||||
|
<div className={styles.tagRow}>
|
||||||
|
{skill.tags.map((tag) => (
|
||||||
|
<button
|
||||||
|
key={tag}
|
||||||
|
className={styles.tagPill}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onTagClick(tag);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{skill.author && (
|
||||||
|
<div className={styles.authorRow}>
|
||||||
|
<span className={styles.authorLabel}>Author</span>
|
||||||
|
<span className={styles.authorValue}>{skill.author}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{skill.version && (
|
||||||
|
<div className={styles.authorRow}>
|
||||||
|
<span className={styles.authorLabel}>Version</span>
|
||||||
|
<span className={styles.authorValue}>{skill.version}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.installHint}>
|
||||||
|
<code>hermes skills install {skill.name}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatCard({ value, label, color }: { value: number; label: string; color: string }) {
|
||||||
|
return (
|
||||||
|
<div className={styles.stat}>
|
||||||
|
<span className={styles.statValue} style={{ color }}>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
<span className={styles.statLabel}>{label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string | null>(null);
|
||||||
|
const [visibleCount, setVisibleCount] = useState(PAGE_SIZE);
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
const searchRef = useRef<HTMLInputElement>(null);
|
||||||
|
const gridRef = useRef<HTMLDivElement>(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<string, { label: string; count: number }>();
|
||||||
|
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 (
|
||||||
|
<Layout
|
||||||
|
title="Skills Hub"
|
||||||
|
description="Browse all skills and plugins available for Hermes Agent"
|
||||||
|
>
|
||||||
|
<div className={styles.page}>
|
||||||
|
<header className={styles.hero}>
|
||||||
|
<div className={styles.heroGlow} />
|
||||||
|
<div className={styles.heroContent}>
|
||||||
|
<p className={styles.heroEyebrow}>Hermes Agent</p>
|
||||||
|
<h1 className={styles.heroTitle}>Skills Hub</h1>
|
||||||
|
<p className={styles.heroSub}>
|
||||||
|
Discover, search, and install from{" "}
|
||||||
|
<strong className={styles.heroAccent}>{allSkills.length}</strong> skills
|
||||||
|
across {sources.length - 1} registries
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className={styles.statsRow}>
|
||||||
|
<StatCard
|
||||||
|
value={allSkills.filter((s) => s.source === "built-in").length}
|
||||||
|
label="Built-in"
|
||||||
|
color="#4ade80"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
value={allSkills.filter((s) => s.source === "optional").length}
|
||||||
|
label="Optional"
|
||||||
|
color="#fbbf24"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
value={
|
||||||
|
allSkills.filter(
|
||||||
|
(s) => s.source !== "built-in" && s.source !== "optional"
|
||||||
|
).length
|
||||||
|
}
|
||||||
|
label="Community"
|
||||||
|
color="#60a5fa"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
value={new Set(allSkills.map((s) => s.category)).size}
|
||||||
|
label="Categories"
|
||||||
|
color="#a78bfa"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className={styles.controlsBar}>
|
||||||
|
<div className={styles.searchWrap}>
|
||||||
|
<svg className={styles.searchIcon} viewBox="0 0 20 20" fill="currentColor" width="18" height="18">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
ref={searchRef}
|
||||||
|
type="text"
|
||||||
|
placeholder='Search skills... (press "/" to focus)'
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
className={styles.searchInput}
|
||||||
|
/>
|
||||||
|
{search && (
|
||||||
|
<button className={styles.clearBtn} onClick={() => setSearch("")}>
|
||||||
|
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.sourcePills}>
|
||||||
|
{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 (
|
||||||
|
<button
|
||||||
|
key={src}
|
||||||
|
className={`${styles.srcPill} ${active ? styles.srcPillActive : ""}`}
|
||||||
|
onClick={() => handleSourceChange(src)}
|
||||||
|
style={
|
||||||
|
active && conf
|
||||||
|
? ({
|
||||||
|
"--pill-color": conf.color,
|
||||||
|
"--pill-bg": conf.bg,
|
||||||
|
"--pill-border": conf.border,
|
||||||
|
} as React.CSSProperties)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{src === "all" ? "All" : conf?.label || src}
|
||||||
|
<span className={styles.srcCount}>{count}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.layout}>
|
||||||
|
<button
|
||||||
|
className={styles.sidebarToggle}
|
||||||
|
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h6a1 1 0 110 2H4a1 1 0 01-1-1z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Categories
|
||||||
|
{categoryFilter !== "all" && (
|
||||||
|
<span className={styles.activeCatBadge}>
|
||||||
|
{categoryEntries.find((c) => c.key === categoryFilter)?.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<aside className={`${styles.sidebar} ${sidebarOpen ? styles.sidebarOpen : ""}`}>
|
||||||
|
<div className={styles.sidebarHeader}>
|
||||||
|
<h2 className={styles.sidebarTitle}>Categories</h2>
|
||||||
|
{categoryFilter !== "all" && (
|
||||||
|
<button className={styles.sidebarClear} onClick={() => setCategoryFilter("all")}>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<nav className={styles.catList}>
|
||||||
|
<button
|
||||||
|
className={`${styles.catItem} ${categoryFilter === "all" ? styles.catItemActive : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
setCategoryFilter("all");
|
||||||
|
setSidebarOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className={styles.catItemIcon}>{"\u{1F4CB}"}</span>
|
||||||
|
<span className={styles.catItemLabel}>All Skills</span>
|
||||||
|
<span className={styles.catItemCount}>{filtered.length}</span>
|
||||||
|
</button>
|
||||||
|
{categoryEntries.map((cat) => (
|
||||||
|
<button
|
||||||
|
key={cat.key}
|
||||||
|
className={`${styles.catItem} ${categoryFilter === cat.key ? styles.catItemActive : ""}`}
|
||||||
|
onClick={() => handleCategoryClick(cat.key)}
|
||||||
|
>
|
||||||
|
<span className={styles.catItemIcon}>
|
||||||
|
{CATEGORY_ICONS[cat.key] || "\u{1F4E6}"}
|
||||||
|
</span>
|
||||||
|
<span className={styles.catItemLabel}>{cat.label}</span>
|
||||||
|
<span className={styles.catItemCount}>{cat.count}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main className={styles.main} ref={gridRef}>
|
||||||
|
{(search || sourceFilter !== "all" || categoryFilter !== "all") && (
|
||||||
|
<div className={styles.filterSummary}>
|
||||||
|
<span className={styles.filterCount}>
|
||||||
|
{filtered.length} result{filtered.length !== 1 ? "s" : ""}
|
||||||
|
</span>
|
||||||
|
{search && (
|
||||||
|
<span className={styles.filterChip}>
|
||||||
|
“{search}”
|
||||||
|
<button onClick={() => setSearch("")}>×</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{sourceFilter !== "all" && (
|
||||||
|
<span className={styles.filterChip}>
|
||||||
|
{SOURCE_CONFIG[sourceFilter]?.label || sourceFilter}
|
||||||
|
<button onClick={() => setSourceFilter("all")}>×</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{categoryFilter !== "all" && (
|
||||||
|
<span className={styles.filterChip}>
|
||||||
|
{categoryEntries.find((c) => c.key === categoryFilter)?.label ||
|
||||||
|
categoryFilter}
|
||||||
|
<button onClick={() => setCategoryFilter("all")}>×</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button className={styles.clearAllBtn} onClick={clearAll}>
|
||||||
|
Clear all
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{visible.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<div className={styles.grid}>
|
||||||
|
{visible.map((skill, i) => {
|
||||||
|
const key = `${skill.source}-${skill.name}-${i}`;
|
||||||
|
return (
|
||||||
|
<SkillCard
|
||||||
|
key={key}
|
||||||
|
skill={skill}
|
||||||
|
query={search}
|
||||||
|
expanded={expandedCard === key}
|
||||||
|
onToggle={() =>
|
||||||
|
setExpandedCard(expandedCard === key ? null : key)
|
||||||
|
}
|
||||||
|
onCategoryClick={handleCategoryClick}
|
||||||
|
onTagClick={handleTagClick}
|
||||||
|
style={{ animationDelay: `${Math.min(i, 20) * 25}ms` }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{hasMore && (
|
||||||
|
<div className={styles.loadMoreWrap}>
|
||||||
|
<button
|
||||||
|
className={styles.loadMoreBtn}
|
||||||
|
onClick={() => setVisibleCount((v) => v + PAGE_SIZE)}
|
||||||
|
>
|
||||||
|
Show more ({filtered.length - visibleCount} remaining)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className={styles.empty}>
|
||||||
|
<div className={styles.emptyIcon}>{"\u{1F50D}"}</div>
|
||||||
|
<h3 className={styles.emptyTitle}>No skills found</h3>
|
||||||
|
<p className={styles.emptyDesc}>
|
||||||
|
Try a different search term or clear your filters.
|
||||||
|
</p>
|
||||||
|
<button className={styles.emptyReset} onClick={clearAll}>
|
||||||
|
Reset all filters
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sidebarOpen && (
|
||||||
|
<div className={styles.backdrop} onClick={() => setSidebarOpen(false)} />
|
||||||
|
)}
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
819
website/src/pages/skills/styles.module.css
Normal file
819
website/src/pages/skills/styles.module.css
Normal file
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue