feat: enhance web app with fuzzy search, syntax highlighting, and pagination

- Expand README with detailed Web App section (English)
- Improve SEO meta tags in index.html
- Add rehype-highlight for code syntax highlighting in skill details
- Implement fuzzy search with scoring (name > category > description)
- Add clear search button and result counter
- Implement Load More pagination (24 items initially) for 950+ skills
- Add rehype-highlight and highlight.js dependencies

Made-with: Cursor
This commit is contained in:
sck_0
2026-02-27 09:05:13 +01:00
parent 1e73502c3d
commit 6b4dae330c
6 changed files with 208 additions and 33 deletions

View File

@@ -351,21 +351,52 @@ We have moved the full skill registry to a dedicated catalog to keep this README
### 🌐 Interactive Skills Web App
You can now easily search, filter, and discover the perfect skills for your agent using our local Web App.
A modern web interface to explore, search, and use the 950+ skills directly from your browser.
To launch the app:
#### ✨ Features
1. Double-click the `START_APP.bat` file in the root directory (Windows) or run it from your terminal.
2. The app will automatically configure everything and open in your default browser.
- 🔍 **Full-text search** Search skills by name, description, or content
- 🏷️ **Category filters** Frontend, Backend, Security, DevOps, etc.
- 📝 **Markdown rendering** View complete documentation with syntax highlighting
- 📋 **Copy buttons** Copy `@skill-name` or full content in 1 click
- 🛠️ **Prompt Builder** Add custom context before copying
- 🌙 **Dark mode** Adaptive light/dark interface
- ⚡ **Auto-update** Automatically syncs with upstream repo
#### 🛠️ New: Interactive Prompt Builder
#### 🚀 Quick Start
The web app is no longer just a static catalog! When you click on any skill, you will see an **Interactive Prompt Builder** box.
Instead of manually copying `@skill-name` and writing your requirements separately in your IDE:
**Windows:**
```bash
# Double-click or terminal
START_APP.bat
```
1. Type your specific project constraints into the text box (e.g., "Use React 19 and Tailwind").
2. Click **Copy Prompt**.
3. Your clipboard now has a fully formatted, ready-to-run prompt combining the skill invocation and your custom context!
**macOS/Linux:**
```bash
# 1. Install dependencies (first time)
cd web-app && npm install
# 2. Setup assets and launch
npm run app:dev
```
**Available npm commands:**
```bash
npm run app:setup # Copy skills to web-app/public/
npm run app:dev # Start dev server
npm run app:build # Production build
npm run app:preview # Preview production build
```
The app automatically opens at `http://localhost:5173` (or alternative port).
#### 🛠️ Prompt Builder
On each skill page you'll find the **Prompt Builder**:
1. Write specific requirements (e.g., "Use React 19, TypeScript and Tailwind")
2. Click **Copy Prompt** copies `@skill-name + context`
3. Or **Copy Full Content** copies the full documentation
4. Paste into your AI assistant (Claude, Cursor, Gemini, etc.)
👉 **[View the Complete Skill Catalog (CATALOG.md)](CATALOG.md)**

View File

@@ -4,7 +4,14 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>web-app</title>
<meta name="description" content="Antigravity Awesome Skills - 950+ agentic skills catalog for Claude Code, Gemini, Cursor, Copilot. Search, filter, and copy prompts instantly." />
<meta name="keywords" content="AI skills, Claude Code, Gemini CLI, Cursor, Copilot, agentic skills, coding assistant" />
<meta name="author" content="Niccolò Abate (@sickn33)" />
<meta property="og:title" content="Antigravity Awesome Skills Catalog" />
<meta property="og:description" content="Browse 950+ battle-tested agentic skills for AI coding assistants" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<title>Antigravity Skills | 950+ AI Agentic Skills Catalog</title>
</head>
<body>
<div id="root"></div>

View File

@@ -11,11 +11,13 @@
"clsx": "^2.1.1",
"framer-motion": "^12.34.2",
"github-markdown-css": "^5.9.0",
"highlight.js": "^11.11.1",
"lucide-react": "^0.574.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.13.0",
"rehype-highlight": "^7.0.2",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
@@ -78,7 +80,6 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -1764,7 +1765,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1818,7 +1818,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1971,7 +1970,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -2332,7 +2330,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -2738,6 +2735,19 @@
"node": ">=8"
}
},
"node_modules/hast-util-is-element": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz",
"integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-to-jsx-runtime": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
@@ -2765,6 +2775,22 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-to-text": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
"integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
"hast-util-is-element": "^3.0.0",
"unist-util-find-after": "^5.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-whitespace": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
@@ -2795,6 +2821,15 @@
"hermes-estree": "0.25.1"
}
},
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/html-url-attributes": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
@@ -3329,6 +3364,21 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/lowlight": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
"integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"devlop": "^1.0.0",
"highlight.js": "~11.11.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -4141,7 +4191,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -4169,7 +4218,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -4221,7 +4269,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4231,7 +4278,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -4314,6 +4360,23 @@
"react-dom": ">=18"
}
},
"node_modules/rehype-highlight": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz",
"integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"hast-util-to-text": "^4.0.0",
"lowlight": "^3.0.0",
"unist-util-visit": "^5.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-parse": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
@@ -4631,6 +4694,20 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-find-after": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz",
"integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-is": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
@@ -4774,7 +4851,6 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -4896,7 +4972,6 @@
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@@ -13,11 +13,13 @@
"clsx": "^2.1.1",
"framer-motion": "^12.34.2",
"github-markdown-css": "^5.9.0",
"highlight.js": "^11.11.1",
"lucide-react": "^0.574.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.13.0",
"rehype-highlight": "^7.0.2",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {

View File

@@ -10,6 +10,7 @@ export function Home() {
const [search, setSearch] = useState('');
const [categoryFilter, setCategoryFilter] = useState('all');
const [loading, setLoading] = useState(true);
const [displayCount, setDisplayCount] = useState(24); // Show 24 initially (6 rows of 4)
useEffect(() => {
fetch('/skills.json')
@@ -25,15 +26,39 @@ export function Home() {
});
}, []);
// Fuzzy search with scoring
const calculateScore = (skill, terms) => {
let score = 0;
const nameLower = skill.name.toLowerCase();
const descLower = (skill.description || '').toLowerCase();
const catLower = (skill.category || '').toLowerCase();
for (const term of terms) {
// Exact name match (highest priority)
if (nameLower === term) score += 100;
// Name starts with term
else if (nameLower.startsWith(term)) score += 50;
// Name contains term
else if (nameLower.includes(term)) score += 30;
// Category match
else if (catLower.includes(term)) score += 20;
// Description contains term
else if (descLower.includes(term)) score += 10;
}
return score;
};
useEffect(() => {
let result = skills;
if (search) {
const lowerSearch = search.toLowerCase();
result = result.filter(skill =>
skill.name.toLowerCase().includes(lowerSearch) ||
skill.description.toLowerCase().includes(lowerSearch)
);
const terms = search.toLowerCase().trim().split(/\s+/).filter(t => t.length > 0);
if (terms.length > 0) {
result = result
.map(skill => ({ ...skill, _score: calculateScore(skill, terms) }))
.filter(skill => skill._score > 0)
.sort((a, b) => b._score - a._score);
}
}
if (categoryFilter !== 'all') {
@@ -43,6 +68,11 @@ export function Home() {
setFilteredSkills(result);
}, [search, categoryFilter, skills]);
// Reset display count when search/filter changes
useEffect(() => {
setDisplayCount(24);
}, [search, categoryFilter]);
const categories = ['all', ...new Set(skills.map(s => s.category).filter(Boolean))];
return (
@@ -50,7 +80,11 @@ export function Home() {
<div className="flex flex-col space-y-4 md:flex-row md:items-center md:justify-between md:space-y-0">
<div>
<h1 className="text-3xl font-bold tracking-tight text-slate-900 dark:text-slate-100 mb-2">Explore Skills</h1>
<p className="text-slate-500 dark:text-slate-400">Discover {skills.length} agentic capabilities for your AI assistant.</p>
<p className="text-slate-500 dark:text-slate-400">
{search || categoryFilter !== 'all'
? `Showing ${filteredSkills.length} of ${skills.length} skills`
: `Discover ${skills.length} agentic capabilities for your AI assistant.`}
</p>
</div>
</div>
@@ -59,11 +93,20 @@ export function Home() {
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-500" />
<input
type="text"
placeholder="Search skills (e.g., 'react', 'security', 'python')..."
className="w-full rounded-md border border-slate-200 bg-slate-50 px-9 py-2 text-sm outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50"
placeholder="Search skills (e.g., 'react', 'security', 'python', 'testing')..."
className="w-full rounded-md border border-slate-200 bg-slate-50 px-9 py-2 text-sm outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50 pr-9"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{search && (
<button
onClick={() => setSearch('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
title="Clear search"
>
×
</button>
)}
</div>
<div className="flex items-center space-x-2 overflow-x-auto pb-2 md:pb-0 scrollbar-hide">
<Filter className="h-4 w-4 text-slate-500 shrink-0" />
@@ -93,7 +136,7 @@ export function Home() {
<p className="mt-2 text-slate-500 dark:text-slate-400">Try adjusting your search or filter.</p>
</div>
) : (
filteredSkills.map((skill) => (
filteredSkills.slice(0, displayCount).map((skill) => (
<motion.div
key={skill.id}
layout
@@ -133,6 +176,21 @@ export function Home() {
))
)}
</AnimatePresence>
{/* Load More Button */}
{!loading && filteredSkills.length > displayCount && (
<div className="col-span-full flex justify-center py-8">
<button
onClick={() => setDisplayCount(prev => prev + 24)}
className="flex items-center space-x-2 px-6 py-3 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors shadow-sm"
>
<span>Load More</span>
<span className="text-sm text-slate-500 dark:text-slate-400">
({filteredSkills.length - displayCount} remaining)
</span>
</button>
</div>
)}
</div>
</div>
);

View File

@@ -2,7 +2,9 @@
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import Markdown from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';
import { ArrowLeft, Copy, Check, FileCode, AlertTriangle } from 'lucide-react';
import 'highlight.js/styles/github-dark.css';
export function SkillDetail() {
const { id } = useParams();
@@ -157,8 +159,8 @@ export function SkillDetail() {
</div>
<div className="p-6 sm:p-8">
<div className="prose prose-slate dark:prose-invert max-w-none">
<Markdown>{content}</Markdown>
<div className="prose prose-slate dark:prose-invert max-w-none prose-code:before:content-none prose-code:after:content-none">
<Markdown rehypePlugins={[rehypeHighlight]}>{content}</Markdown>
</div>
</div>
</div>