mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-20 02:15:12 +00:00
Merge branch 'main' into add-new-skill-clean
This commit is contained in:
82
.github/workflows/deploy-website.yml
vendored
Normal file
82
.github/workflows/deploy-website.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
# GitHub Pages deployment workflow
|
||||
# Builds the Astro website and deploys to GitHub Pages
|
||||
|
||||
name: Deploy Website to GitHub Pages
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the default branch
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "website/**"
|
||||
- "agents/**"
|
||||
- "prompts/**"
|
||||
- "instructions/**"
|
||||
- "skills/**"
|
||||
- "collections/**"
|
||||
- "cookbook/**"
|
||||
- "eng/generate-website-data.mjs"
|
||||
- ".github/workflows/deploy-website.yml"
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Build job
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "npm"
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install website dependencies
|
||||
run: npm ci
|
||||
working-directory: ./website
|
||||
|
||||
- name: Generate website data
|
||||
run: npm run website:data
|
||||
|
||||
- name: Build Astro site
|
||||
run: npm run build
|
||||
working-directory: ./website
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: "./website/dist"
|
||||
|
||||
# Deployment job
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -6,3 +6,8 @@ reports/
|
||||
# macOS system files
|
||||
.DS_Store
|
||||
*.tmp
|
||||
|
||||
# Website build artifacts
|
||||
website/dist/
|
||||
website/.astro/
|
||||
website/public/data/*
|
||||
|
||||
99
.schemas/cookbook.schema.json
Normal file
99
.schemas/cookbook.schema.json
Normal file
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Cookbook Manifest",
|
||||
"description": "Schema for cookbook.yml manifest defining cookbooks and recipes",
|
||||
"type": "object",
|
||||
"required": ["cookbooks"],
|
||||
"properties": {
|
||||
"cookbooks": {
|
||||
"type": "array",
|
||||
"description": "List of cookbooks",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "name", "description", "path", "languages", "recipes"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier for the cookbook",
|
||||
"pattern": "^[a-z0-9-]+$"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Display name for the cookbook"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Brief description of the cookbook"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Relative path to the cookbook folder"
|
||||
},
|
||||
"featured": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this cookbook should be featured",
|
||||
"default": false
|
||||
},
|
||||
"languages": {
|
||||
"type": "array",
|
||||
"description": "Programming languages supported by this cookbook",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Language identifier (folder name)",
|
||||
"pattern": "^[a-z0-9-]+$"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Display name for the language"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"description": "Emoji icon for the language"
|
||||
},
|
||||
"extension": {
|
||||
"type": "string",
|
||||
"description": "File extension for runnable examples",
|
||||
"pattern": "^\\.[a-z]+$"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"recipes": {
|
||||
"type": "array",
|
||||
"description": "List of recipes in this cookbook",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "name", "description"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Recipe identifier (matches markdown filename without extension)",
|
||||
"pattern": "^[a-z0-9-]+$"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Display name for the recipe"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Brief description of what the recipe covers"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"description": "Tags for filtering and categorization",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
151
.schemas/tools.schema.json
Normal file
151
.schemas/tools.schema.json
Normal file
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Tools Catalog",
|
||||
"description": "Schema for the awesome-copilot tools catalog (website/data/tools.yml)",
|
||||
"type": "object",
|
||||
"required": ["tools"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"tools": {
|
||||
"type": "array",
|
||||
"description": "List of tools in the catalog",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "name", "description", "category"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier for the tool",
|
||||
"pattern": "^[a-z0-9-]+$",
|
||||
"minLength": 1,
|
||||
"maxLength": 50
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Display name for the tool",
|
||||
"minLength": 1,
|
||||
"maxLength": 100
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Description of what this tool does",
|
||||
"minLength": 1,
|
||||
"maxLength": 1000
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "Category for grouping tools",
|
||||
"minLength": 1,
|
||||
"maxLength": 50,
|
||||
"examples": ["MCP Servers", "VS Code Extensions", "CLI Tools", "Visual Studio Extensions"]
|
||||
},
|
||||
"featured": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this tool is featured (shown first)",
|
||||
"default": false
|
||||
},
|
||||
"requirements": {
|
||||
"type": "array",
|
||||
"description": "List of requirements to use this tool",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 200
|
||||
},
|
||||
"maxItems": 10
|
||||
},
|
||||
"features": {
|
||||
"type": "array",
|
||||
"description": "List of key features",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 200
|
||||
},
|
||||
"maxItems": 20
|
||||
},
|
||||
"links": {
|
||||
"type": "object",
|
||||
"description": "Links related to this tool",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"blog": {
|
||||
"type": "string",
|
||||
"description": "Link to a blog post about the tool",
|
||||
"format": "uri"
|
||||
},
|
||||
"documentation": {
|
||||
"type": "string",
|
||||
"description": "Link to documentation",
|
||||
"format": "uri"
|
||||
},
|
||||
"github": {
|
||||
"type": "string",
|
||||
"description": "Link to GitHub repository",
|
||||
"format": "uri"
|
||||
},
|
||||
"marketplace": {
|
||||
"type": "string",
|
||||
"description": "Link to VS Code or Visual Studio Marketplace",
|
||||
"format": "uri"
|
||||
},
|
||||
"npm": {
|
||||
"type": "string",
|
||||
"description": "Link to npm package",
|
||||
"format": "uri"
|
||||
},
|
||||
"pypi": {
|
||||
"type": "string",
|
||||
"description": "Link to PyPI package",
|
||||
"format": "uri"
|
||||
},
|
||||
"vscode": {
|
||||
"type": "string",
|
||||
"description": "VS Code install link (vscode: URI or aka.ms link)"
|
||||
},
|
||||
"vscode-insiders": {
|
||||
"type": "string",
|
||||
"description": "VS Code Insiders install link"
|
||||
},
|
||||
"visual-studio": {
|
||||
"type": "string",
|
||||
"description": "Visual Studio install link"
|
||||
}
|
||||
}
|
||||
},
|
||||
"configuration": {
|
||||
"type": "object",
|
||||
"description": "Configuration snippet for the tool",
|
||||
"required": ["type", "content"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Type of configuration (for syntax highlighting)",
|
||||
"enum": ["json", "yaml", "bash", "toml", "ini"]
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "The configuration content"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"description": "Tags for filtering and discovery",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9-]+$",
|
||||
"minLength": 1,
|
||||
"maxLength": 30
|
||||
},
|
||||
"uniqueItems": true,
|
||||
"maxItems": 15
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -15,6 +15,7 @@
|
||||
"*.prompt.md": "prompt"
|
||||
},
|
||||
"yaml.schemas": {
|
||||
"./.schemas/collection.schema.json": "*.collection.yml"
|
||||
"./.schemas/collection.schema.json": "*.collection.yml",
|
||||
"./.schemas/tools.schema.json": "website/data/tools.yml",
|
||||
}
|
||||
}
|
||||
|
||||
63
cookbook/cookbook.yml
Normal file
63
cookbook/cookbook.yml
Normal file
@@ -0,0 +1,63 @@
|
||||
# yaml-language-server: $schema=../.schemas/cookbook.schema.json
|
||||
# Cookbook manifest for the Awesome GitHub Copilot website
|
||||
# This file defines the structure of cookbooks and recipes for the Samples page
|
||||
|
||||
cookbooks:
|
||||
- id: copilot-sdk
|
||||
name: GitHub Copilot SDK
|
||||
description: Ready-to-use recipes for building with the GitHub Copilot SDK across multiple languages
|
||||
path: cookbook/copilot-sdk
|
||||
featured: true
|
||||
languages:
|
||||
- id: nodejs
|
||||
name: Node.js / TypeScript
|
||||
icon: "\uE628"
|
||||
extension: .ts
|
||||
- id: python
|
||||
name: Python
|
||||
icon: "\uE73C"
|
||||
extension: .py
|
||||
- id: dotnet
|
||||
name: .NET (C#)
|
||||
icon: "\uE648"
|
||||
extension: .cs
|
||||
- id: go
|
||||
name: Go
|
||||
icon: "\uE626"
|
||||
extension: .go
|
||||
recipes:
|
||||
- id: error-handling
|
||||
name: Error Handling
|
||||
description: Handle errors gracefully including connection failures, timeouts, and cleanup
|
||||
tags:
|
||||
- errors
|
||||
- basics
|
||||
- reliability
|
||||
- id: multiple-sessions
|
||||
name: Multiple Sessions
|
||||
description: Manage multiple independent conversations simultaneously
|
||||
tags:
|
||||
- sessions
|
||||
- advanced
|
||||
- concurrency
|
||||
- id: managing-local-files
|
||||
name: Managing Local Files
|
||||
description: Organize files by metadata using AI-powered grouping strategies
|
||||
tags:
|
||||
- files
|
||||
- organization
|
||||
- ai-powered
|
||||
- id: pr-visualization
|
||||
name: PR Visualization
|
||||
description: Generate interactive PR age charts using GitHub MCP Server
|
||||
tags:
|
||||
- github
|
||||
- visualization
|
||||
- mcp
|
||||
- id: persisting-sessions
|
||||
name: Persisting Sessions
|
||||
description: Save and resume sessions across restarts
|
||||
tags:
|
||||
- sessions
|
||||
- persistence
|
||||
- state-management
|
||||
@@ -123,6 +123,7 @@ const PROMPTS_DIR = path.join(ROOT_FOLDER, "prompts");
|
||||
const AGENTS_DIR = path.join(ROOT_FOLDER, "agents");
|
||||
const SKILLS_DIR = path.join(ROOT_FOLDER, "skills");
|
||||
const COLLECTIONS_DIR = path.join(ROOT_FOLDER, "collections");
|
||||
const COOKBOOK_DIR = path.join(ROOT_FOLDER, "cookbook");
|
||||
const MAX_COLLECTION_ITEMS = 50;
|
||||
|
||||
// Agent Skills validation constants
|
||||
@@ -145,6 +146,7 @@ export {
|
||||
AGENTS_DIR,
|
||||
SKILLS_DIR,
|
||||
COLLECTIONS_DIR,
|
||||
COOKBOOK_DIR,
|
||||
MAX_COLLECTION_ITEMS,
|
||||
SKILL_NAME_MIN_LENGTH,
|
||||
SKILL_NAME_MAX_LENGTH,
|
||||
|
||||
825
eng/generate-website-data.mjs
Normal file
825
eng/generate-website-data.mjs
Normal file
@@ -0,0 +1,825 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Generate JSON metadata files for the GitHub Pages website.
|
||||
* This script extracts metadata from agents, prompts, instructions, skills, and collections
|
||||
* and writes them to website/data/ for client-side search and display.
|
||||
*/
|
||||
|
||||
import fs from "fs";
|
||||
import path, { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import {
|
||||
AGENTS_DIR,
|
||||
COLLECTIONS_DIR,
|
||||
COOKBOOK_DIR,
|
||||
INSTRUCTIONS_DIR,
|
||||
PROMPTS_DIR,
|
||||
ROOT_FOLDER,
|
||||
SKILLS_DIR,
|
||||
} from "./constants.mjs";
|
||||
import {
|
||||
parseCollectionYaml,
|
||||
parseFrontmatter,
|
||||
parseSkillMetadata,
|
||||
parseYamlFile,
|
||||
} from "./yaml-parser.mjs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const WEBSITE_DATA_DIR = path.join(ROOT_FOLDER, "website", "public", "data");
|
||||
const WEBSITE_SOURCE_DATA_DIR = path.join(ROOT_FOLDER, "website", "data");
|
||||
|
||||
/**
|
||||
* Ensure the output directory exists
|
||||
*/
|
||||
function ensureDataDir() {
|
||||
if (!fs.existsSync(WEBSITE_DATA_DIR)) {
|
||||
fs.mkdirSync(WEBSITE_DATA_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract title from filename or frontmatter
|
||||
*/
|
||||
function extractTitle(filePath, frontmatter) {
|
||||
if (frontmatter?.title) return frontmatter.title;
|
||||
if (frontmatter?.name) {
|
||||
return frontmatter.name
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
// Fallback to filename
|
||||
const basename = path.basename(filePath);
|
||||
const name = basename
|
||||
.replace(/\.(agent|prompt|instructions)\.md$/, "")
|
||||
.replace(/\.md$/, "");
|
||||
return name
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate agents metadata
|
||||
*/
|
||||
function generateAgentsData() {
|
||||
const agents = [];
|
||||
const files = fs
|
||||
.readdirSync(AGENTS_DIR)
|
||||
.filter((f) => f.endsWith(".agent.md"));
|
||||
|
||||
// Track all unique values for filters
|
||||
const allModels = new Set();
|
||||
const allTools = new Set();
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(AGENTS_DIR, file);
|
||||
const frontmatter = parseFrontmatter(filePath);
|
||||
const relativePath = path
|
||||
.relative(ROOT_FOLDER, filePath)
|
||||
.replace(/\\/g, "/");
|
||||
|
||||
const model = frontmatter?.model || null;
|
||||
const tools = frontmatter?.tools || [];
|
||||
const handoffs = frontmatter?.handoffs || [];
|
||||
|
||||
// Track unique values
|
||||
if (model) allModels.add(model);
|
||||
tools.forEach((t) => allTools.add(t));
|
||||
|
||||
agents.push({
|
||||
id: file.replace(".agent.md", ""),
|
||||
title: extractTitle(filePath, frontmatter),
|
||||
description: frontmatter?.description || "",
|
||||
model: model,
|
||||
tools: tools,
|
||||
hasHandoffs: handoffs.length > 0,
|
||||
handoffs: handoffs.map((h) => ({
|
||||
label: h.label || "",
|
||||
agent: h.agent || "",
|
||||
})),
|
||||
mcpServers: frontmatter?.["mcp-servers"]
|
||||
? Object.keys(frontmatter["mcp-servers"])
|
||||
: [],
|
||||
path: relativePath,
|
||||
filename: file,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort and return with filter metadata
|
||||
const sortedAgents = agents.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
return {
|
||||
items: sortedAgents,
|
||||
filters: {
|
||||
models: ["(none)", ...Array.from(allModels).sort()],
|
||||
tools: Array.from(allTools).sort(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate prompts metadata
|
||||
*/
|
||||
function generatePromptsData() {
|
||||
const prompts = [];
|
||||
const files = fs
|
||||
.readdirSync(PROMPTS_DIR)
|
||||
.filter((f) => f.endsWith(".prompt.md"));
|
||||
|
||||
// Track all unique tools for filters
|
||||
const allTools = new Set();
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(PROMPTS_DIR, file);
|
||||
const frontmatter = parseFrontmatter(filePath);
|
||||
const relativePath = path
|
||||
.relative(ROOT_FOLDER, filePath)
|
||||
.replace(/\\/g, "/");
|
||||
|
||||
const tools = frontmatter?.tools || [];
|
||||
tools.forEach((t) => allTools.add(t));
|
||||
|
||||
prompts.push({
|
||||
id: file.replace(".prompt.md", ""),
|
||||
title: extractTitle(filePath, frontmatter),
|
||||
description: frontmatter?.description || "",
|
||||
agent: frontmatter?.agent || null,
|
||||
model: frontmatter?.model || null,
|
||||
tools: tools,
|
||||
path: relativePath,
|
||||
filename: file,
|
||||
});
|
||||
}
|
||||
|
||||
const sortedPrompts = prompts.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
return {
|
||||
items: sortedPrompts,
|
||||
filters: {
|
||||
tools: Array.from(allTools).sort(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse applyTo field into an array of patterns
|
||||
*/
|
||||
function parseApplyToPatterns(applyTo) {
|
||||
if (!applyTo) return [];
|
||||
|
||||
// Handle array format
|
||||
if (Array.isArray(applyTo)) {
|
||||
return applyTo.map((p) => p.trim()).filter((p) => p.length > 0);
|
||||
}
|
||||
|
||||
// Handle string format (comma-separated)
|
||||
if (typeof applyTo === "string") {
|
||||
return applyTo
|
||||
.split(",")
|
||||
.map((p) => p.trim())
|
||||
.filter((p) => p.length > 0);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract file extension from a glob pattern
|
||||
*/
|
||||
function extractExtensionFromPattern(pattern) {
|
||||
// Match patterns like **.ts, **/*.js, *.py, etc.
|
||||
const match = pattern.match(/\*\.(\w+)$/);
|
||||
if (match) return `.${match[1]}`;
|
||||
|
||||
// Match patterns like **/*.{ts,tsx}
|
||||
const braceMatch = pattern.match(/\*\.\{([^}]+)\}$/);
|
||||
if (braceMatch) {
|
||||
return braceMatch[1].split(",").map((ext) => `.${ext.trim()}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate instructions metadata
|
||||
*/
|
||||
function generateInstructionsData() {
|
||||
const instructions = [];
|
||||
const files = fs
|
||||
.readdirSync(INSTRUCTIONS_DIR)
|
||||
.filter((f) => f.endsWith(".instructions.md"));
|
||||
|
||||
// Track all unique patterns and extensions for filters
|
||||
const allPatterns = new Set();
|
||||
const allExtensions = new Set();
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(INSTRUCTIONS_DIR, file);
|
||||
const frontmatter = parseFrontmatter(filePath);
|
||||
const relativePath = path
|
||||
.relative(ROOT_FOLDER, filePath)
|
||||
.replace(/\\/g, "/");
|
||||
|
||||
const applyToRaw = frontmatter?.applyTo || null;
|
||||
const applyToPatterns = parseApplyToPatterns(applyToRaw);
|
||||
|
||||
// Extract extensions from patterns
|
||||
const extensions = [];
|
||||
for (const pattern of applyToPatterns) {
|
||||
allPatterns.add(pattern);
|
||||
const ext = extractExtensionFromPattern(pattern);
|
||||
if (ext) {
|
||||
if (Array.isArray(ext)) {
|
||||
ext.forEach((e) => {
|
||||
extensions.push(e);
|
||||
allExtensions.add(e);
|
||||
});
|
||||
} else {
|
||||
extensions.push(ext);
|
||||
allExtensions.add(ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instructions.push({
|
||||
id: file.replace(".instructions.md", ""),
|
||||
title: extractTitle(filePath, frontmatter),
|
||||
description: frontmatter?.description || "",
|
||||
applyTo: applyToRaw,
|
||||
applyToPatterns: applyToPatterns,
|
||||
extensions: [...new Set(extensions)],
|
||||
path: relativePath,
|
||||
filename: file,
|
||||
});
|
||||
}
|
||||
|
||||
const sortedInstructions = instructions.sort((a, b) =>
|
||||
a.title.localeCompare(b.title)
|
||||
);
|
||||
|
||||
return {
|
||||
items: sortedInstructions,
|
||||
filters: {
|
||||
patterns: Array.from(allPatterns).sort(),
|
||||
extensions: ["(none)", ...Array.from(allExtensions).sort()],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize a skill based on its name and description
|
||||
*/
|
||||
function categorizeSkill(name, description) {
|
||||
const text = `${name} ${description}`.toLowerCase();
|
||||
|
||||
if (text.includes("azure") || text.includes("appinsights")) return "Azure";
|
||||
if (
|
||||
text.includes("github") ||
|
||||
text.includes("gh-cli") ||
|
||||
text.includes("git-commit") ||
|
||||
text.includes("git ")
|
||||
)
|
||||
return "Git & GitHub";
|
||||
if (text.includes("vscode") || text.includes("vs code")) return "VS Code";
|
||||
if (
|
||||
text.includes("test") ||
|
||||
text.includes("qa") ||
|
||||
text.includes("playwright")
|
||||
)
|
||||
return "Testing";
|
||||
if (
|
||||
text.includes("microsoft") ||
|
||||
text.includes("m365") ||
|
||||
text.includes("workiq")
|
||||
)
|
||||
return "Microsoft";
|
||||
if (text.includes("cli") || text.includes("command")) return "CLI Tools";
|
||||
if (
|
||||
text.includes("diagram") ||
|
||||
text.includes("plantuml") ||
|
||||
text.includes("visual")
|
||||
)
|
||||
return "Diagrams";
|
||||
if (
|
||||
text.includes("nuget") ||
|
||||
text.includes("dotnet") ||
|
||||
text.includes(".net")
|
||||
)
|
||||
return ".NET";
|
||||
|
||||
return "Other";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate skills metadata
|
||||
*/
|
||||
function generateSkillsData() {
|
||||
const skills = [];
|
||||
|
||||
if (!fs.existsSync(SKILLS_DIR)) {
|
||||
return { items: [], filters: { categories: [], hasAssets: ["Yes", "No"] } };
|
||||
}
|
||||
|
||||
const folders = fs
|
||||
.readdirSync(SKILLS_DIR)
|
||||
.filter((f) => fs.statSync(path.join(SKILLS_DIR, f)).isDirectory());
|
||||
|
||||
const allCategories = new Set();
|
||||
|
||||
for (const folder of folders) {
|
||||
const skillPath = path.join(SKILLS_DIR, folder);
|
||||
const metadata = parseSkillMetadata(skillPath);
|
||||
|
||||
if (metadata) {
|
||||
const relativePath = path
|
||||
.relative(ROOT_FOLDER, skillPath)
|
||||
.replace(/\\/g, "/");
|
||||
const category = categorizeSkill(metadata.name, metadata.description);
|
||||
allCategories.add(category);
|
||||
|
||||
// Get all files in the skill folder recursively
|
||||
const files = getSkillFiles(skillPath, relativePath);
|
||||
|
||||
skills.push({
|
||||
id: folder,
|
||||
name: metadata.name,
|
||||
title: metadata.name
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" "),
|
||||
description: metadata.description,
|
||||
assets: metadata.assets,
|
||||
hasAssets: metadata.assets.length > 0,
|
||||
assetCount: metadata.assets.length,
|
||||
category: category,
|
||||
path: relativePath,
|
||||
skillFile: `${relativePath}/SKILL.md`,
|
||||
files: files,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const sortedSkills = skills.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
return {
|
||||
items: sortedSkills,
|
||||
filters: {
|
||||
categories: Array.from(allCategories).sort(),
|
||||
hasAssets: ["Yes", "No"],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files in a skill folder recursively
|
||||
*/
|
||||
function getSkillFiles(skillPath, relativePath) {
|
||||
const files = [];
|
||||
|
||||
function walkDir(dir, relDir) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
walkDir(fullPath, relPath);
|
||||
} else {
|
||||
// Get file size
|
||||
const stats = fs.statSync(fullPath);
|
||||
files.push({
|
||||
path: `${relativePath}/${relPath}`,
|
||||
name: relPath,
|
||||
size: stats.size,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walkDir(skillPath, "");
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate collections metadata
|
||||
*/
|
||||
function generateCollectionsData() {
|
||||
const collections = [];
|
||||
|
||||
if (!fs.existsSync(COLLECTIONS_DIR)) {
|
||||
return collections;
|
||||
}
|
||||
|
||||
const files = fs
|
||||
.readdirSync(COLLECTIONS_DIR)
|
||||
.filter((f) => f.endsWith(".collection.yml"));
|
||||
|
||||
// Track all unique tags
|
||||
const allTags = new Set();
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(COLLECTIONS_DIR, file);
|
||||
const data = parseCollectionYaml(filePath);
|
||||
const relativePath = path
|
||||
.relative(ROOT_FOLDER, filePath)
|
||||
.replace(/\\/g, "/");
|
||||
|
||||
if (data) {
|
||||
const tags = data.tags || [];
|
||||
tags.forEach((t) => allTags.add(t));
|
||||
|
||||
// featured can be at top level or nested under display
|
||||
const featured = data.featured || data.display?.featured || false;
|
||||
|
||||
collections.push({
|
||||
id: file.replace(".collection.yml", ""),
|
||||
name: data.name || file.replace(".collection.yml", ""),
|
||||
description: data.description || "",
|
||||
tags: tags,
|
||||
featured: featured,
|
||||
items: (data.items || []).map((item) => ({
|
||||
path: item.path,
|
||||
kind: item.kind,
|
||||
usage: item.usage || null,
|
||||
})),
|
||||
path: relativePath,
|
||||
filename: file,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort with featured first, then alphabetically
|
||||
const sortedCollections = collections.sort((a, b) => {
|
||||
if (a.featured && !b.featured) return -1;
|
||||
if (!a.featured && b.featured) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return {
|
||||
items: sortedCollections,
|
||||
filters: {
|
||||
tags: Array.from(allTags).sort(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate tools metadata from website/data/tools.yml
|
||||
*/
|
||||
function generateToolsData() {
|
||||
const toolsFile = path.join(WEBSITE_SOURCE_DATA_DIR, "tools.yml");
|
||||
|
||||
if (!fs.existsSync(toolsFile)) {
|
||||
console.warn("No tools.yml file found at", toolsFile);
|
||||
return { items: [], filters: { categories: [], tags: [] } };
|
||||
}
|
||||
|
||||
const data = parseYamlFile(toolsFile);
|
||||
|
||||
if (!data || !data.tools) {
|
||||
return { items: [], filters: { categories: [], tags: [] } };
|
||||
}
|
||||
|
||||
const allCategories = new Set();
|
||||
const allTags = new Set();
|
||||
|
||||
const tools = data.tools.map((tool) => {
|
||||
const category = tool.category || "Other";
|
||||
allCategories.add(category);
|
||||
|
||||
const tags = tool.tags || [];
|
||||
tags.forEach((t) => allTags.add(t));
|
||||
|
||||
return {
|
||||
id: tool.id,
|
||||
name: tool.name,
|
||||
description: tool.description || "",
|
||||
category: category,
|
||||
featured: tool.featured || false,
|
||||
requirements: tool.requirements || [],
|
||||
features: tool.features || [],
|
||||
links: tool.links || {},
|
||||
configuration: tool.configuration || null,
|
||||
tags: tags,
|
||||
};
|
||||
});
|
||||
|
||||
// Sort with featured first, then alphabetically
|
||||
const sortedTools = tools.sort((a, b) => {
|
||||
if (a.featured && !b.featured) return -1;
|
||||
if (!a.featured && b.featured) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return {
|
||||
items: sortedTools,
|
||||
filters: {
|
||||
categories: Array.from(allCategories).sort(),
|
||||
tags: Array.from(allTags).sort(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a combined index for search
|
||||
*/
|
||||
function generateSearchIndex(
|
||||
agents,
|
||||
prompts,
|
||||
instructions,
|
||||
skills,
|
||||
collections
|
||||
) {
|
||||
const index = [];
|
||||
|
||||
for (const agent of agents) {
|
||||
index.push({
|
||||
type: "agent",
|
||||
id: agent.id,
|
||||
title: agent.title,
|
||||
description: agent.description,
|
||||
path: agent.path,
|
||||
searchText: `${agent.title} ${agent.description} ${agent.tools.join(
|
||||
" "
|
||||
)}`.toLowerCase(),
|
||||
});
|
||||
}
|
||||
|
||||
for (const prompt of prompts) {
|
||||
index.push({
|
||||
type: "prompt",
|
||||
id: prompt.id,
|
||||
title: prompt.title,
|
||||
description: prompt.description,
|
||||
path: prompt.path,
|
||||
searchText: `${prompt.title} ${prompt.description}`.toLowerCase(),
|
||||
});
|
||||
}
|
||||
|
||||
for (const instruction of instructions) {
|
||||
index.push({
|
||||
type: "instruction",
|
||||
id: instruction.id,
|
||||
title: instruction.title,
|
||||
description: instruction.description,
|
||||
path: instruction.path,
|
||||
searchText: `${instruction.title} ${instruction.description} ${
|
||||
instruction.applyTo || ""
|
||||
}`.toLowerCase(),
|
||||
});
|
||||
}
|
||||
|
||||
for (const skill of skills) {
|
||||
index.push({
|
||||
type: "skill",
|
||||
id: skill.id,
|
||||
title: skill.title,
|
||||
description: skill.description,
|
||||
path: skill.path,
|
||||
searchText: `${skill.title} ${skill.description}`.toLowerCase(),
|
||||
});
|
||||
}
|
||||
|
||||
for (const collection of collections) {
|
||||
index.push({
|
||||
type: "collection",
|
||||
id: collection.id,
|
||||
title: collection.name,
|
||||
description: collection.description,
|
||||
path: collection.path,
|
||||
tags: collection.tags,
|
||||
searchText: `${collection.name} ${
|
||||
collection.description
|
||||
} ${collection.tags.join(" ")}`.toLowerCase(),
|
||||
});
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate samples/cookbook data from cookbook.yml
|
||||
*/
|
||||
function generateSamplesData() {
|
||||
const cookbookYamlPath = path.join(COOKBOOK_DIR, "cookbook.yml");
|
||||
|
||||
if (!fs.existsSync(cookbookYamlPath)) {
|
||||
console.warn(
|
||||
"Warning: cookbook/cookbook.yml not found, skipping samples generation"
|
||||
);
|
||||
return {
|
||||
cookbooks: [],
|
||||
totalRecipes: 0,
|
||||
totalCookbooks: 0,
|
||||
filters: { languages: [], tags: [] },
|
||||
};
|
||||
}
|
||||
|
||||
const cookbookManifest = parseYamlFile(cookbookYamlPath);
|
||||
if (!cookbookManifest || !cookbookManifest.cookbooks) {
|
||||
console.warn("Warning: Invalid cookbook.yml format");
|
||||
return {
|
||||
cookbooks: [],
|
||||
totalRecipes: 0,
|
||||
totalCookbooks: 0,
|
||||
filters: { languages: [], tags: [] },
|
||||
};
|
||||
}
|
||||
|
||||
const allLanguages = new Set();
|
||||
const allTags = new Set();
|
||||
let totalRecipes = 0;
|
||||
|
||||
const cookbooks = cookbookManifest.cookbooks.map((cookbook) => {
|
||||
// Collect languages
|
||||
cookbook.languages.forEach((lang) => allLanguages.add(lang.id));
|
||||
|
||||
// Process recipes and add file paths
|
||||
const recipes = cookbook.recipes.map((recipe) => {
|
||||
// Collect tags
|
||||
if (recipe.tags) {
|
||||
recipe.tags.forEach((tag) => allTags.add(tag));
|
||||
}
|
||||
|
||||
// Build variants with file paths for each language
|
||||
const variants = {};
|
||||
cookbook.languages.forEach((lang) => {
|
||||
const docPath = `${cookbook.path}/${lang.id}/${recipe.id}.md`;
|
||||
const examplePath = `${cookbook.path}/${lang.id}/recipe/${recipe.id}${lang.extension}`;
|
||||
|
||||
// Check if files exist
|
||||
const docFullPath = path.join(ROOT_FOLDER, docPath);
|
||||
const exampleFullPath = path.join(ROOT_FOLDER, examplePath);
|
||||
|
||||
if (fs.existsSync(docFullPath)) {
|
||||
variants[lang.id] = {
|
||||
doc: docPath,
|
||||
example: fs.existsSync(exampleFullPath) ? examplePath : null,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
totalRecipes++;
|
||||
|
||||
return {
|
||||
id: recipe.id,
|
||||
name: recipe.name,
|
||||
description: recipe.description,
|
||||
tags: recipe.tags || [],
|
||||
variants,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: cookbook.id,
|
||||
name: cookbook.name,
|
||||
description: cookbook.description,
|
||||
path: cookbook.path,
|
||||
featured: cookbook.featured || false,
|
||||
languages: cookbook.languages,
|
||||
recipes,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
cookbooks,
|
||||
totalRecipes,
|
||||
totalCookbooks: cookbooks.length,
|
||||
filters: {
|
||||
languages: Array.from(allLanguages).sort(),
|
||||
tags: Array.from(allTags).sort(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
async function main() {
|
||||
console.log("Generating website data...\n");
|
||||
|
||||
ensureDataDir();
|
||||
|
||||
// Generate all data
|
||||
const agentsData = generateAgentsData();
|
||||
const agents = agentsData.items;
|
||||
console.log(
|
||||
`✓ Generated ${agents.length} agents (${agentsData.filters.models.length} models, ${agentsData.filters.tools.length} tools)`
|
||||
);
|
||||
|
||||
const promptsData = generatePromptsData();
|
||||
const prompts = promptsData.items;
|
||||
console.log(
|
||||
`✓ Generated ${prompts.length} prompts (${promptsData.filters.tools.length} tools)`
|
||||
);
|
||||
|
||||
const instructionsData = generateInstructionsData();
|
||||
const instructions = instructionsData.items;
|
||||
console.log(
|
||||
`✓ Generated ${instructions.length} instructions (${instructionsData.filters.extensions.length} extensions)`
|
||||
);
|
||||
|
||||
const skillsData = generateSkillsData();
|
||||
const skills = skillsData.items;
|
||||
console.log(
|
||||
`✓ Generated ${skills.length} skills (${skillsData.filters.categories.length} categories)`
|
||||
);
|
||||
|
||||
const collectionsData = generateCollectionsData();
|
||||
const collections = collectionsData.items;
|
||||
console.log(
|
||||
`✓ Generated ${collections.length} collections (${collectionsData.filters.tags.length} tags)`
|
||||
);
|
||||
|
||||
const toolsData = generateToolsData();
|
||||
const tools = toolsData.items;
|
||||
console.log(
|
||||
`✓ Generated ${tools.length} tools (${toolsData.filters.categories.length} categories)`
|
||||
);
|
||||
|
||||
const samplesData = generateSamplesData();
|
||||
console.log(
|
||||
`✓ Generated ${samplesData.totalRecipes} recipes in ${samplesData.totalCookbooks} cookbooks (${samplesData.filters.languages.length} languages, ${samplesData.filters.tags.length} tags)`
|
||||
);
|
||||
|
||||
const searchIndex = generateSearchIndex(
|
||||
agents,
|
||||
prompts,
|
||||
instructions,
|
||||
skills,
|
||||
collections
|
||||
);
|
||||
console.log(`✓ Generated search index with ${searchIndex.length} items`);
|
||||
|
||||
// Write JSON files
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "agents.json"),
|
||||
JSON.stringify(agentsData, null, 2)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "prompts.json"),
|
||||
JSON.stringify(promptsData, null, 2)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "instructions.json"),
|
||||
JSON.stringify(instructionsData, null, 2)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "skills.json"),
|
||||
JSON.stringify(skillsData, null, 2)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "collections.json"),
|
||||
JSON.stringify(collectionsData, null, 2)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "tools.json"),
|
||||
JSON.stringify(toolsData, null, 2)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "samples.json"),
|
||||
JSON.stringify(samplesData, null, 2)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "search-index.json"),
|
||||
JSON.stringify(searchIndex, null, 2)
|
||||
);
|
||||
|
||||
// Generate a manifest with counts and timestamps
|
||||
const manifest = {
|
||||
generated: new Date().toISOString(),
|
||||
counts: {
|
||||
agents: agents.length,
|
||||
prompts: prompts.length,
|
||||
instructions: instructions.length,
|
||||
skills: skills.length,
|
||||
collections: collections.length,
|
||||
tools: tools.length,
|
||||
samples: samplesData.totalRecipes,
|
||||
total: searchIndex.length,
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "manifest.json"),
|
||||
JSON.stringify(manifest, null, 2)
|
||||
);
|
||||
|
||||
console.log(`\n✓ All data written to website/public/data/`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Error generating website data:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -195,6 +195,22 @@ function parseSkillMetadata(skillPath) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a generic YAML file (used for tools.yml and other config files)
|
||||
* @param {string} filePath - Path to the YAML file
|
||||
* @returns {object|null} Parsed YAML object or null on error
|
||||
*/
|
||||
function parseYamlFile(filePath) {
|
||||
return safeFileOperation(
|
||||
() => {
|
||||
const content = fs.readFileSync(filePath, "utf8");
|
||||
return yaml.load(content, { schema: yaml.JSON_SCHEMA });
|
||||
},
|
||||
filePath,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
parseCollectionYaml,
|
||||
parseFrontmatter,
|
||||
@@ -202,5 +218,6 @@ export {
|
||||
extractMcpServers,
|
||||
extractMcpServerConfigs,
|
||||
parseSkillMetadata,
|
||||
parseYamlFile,
|
||||
safeFileOperation,
|
||||
};
|
||||
|
||||
@@ -14,7 +14,11 @@
|
||||
"collection:validate": "node ./eng/validate-collections.mjs",
|
||||
"collection:create": "node ./eng/create-collection.mjs",
|
||||
"skill:validate": "node ./eng/validate-skills.mjs",
|
||||
"skill:create": "node ./eng/create-skill.mjs"
|
||||
"skill:create": "node ./eng/create-skill.mjs",
|
||||
"website:data": "node ./eng/generate-website-data.mjs",
|
||||
"website:dev": "npm run website:data && npm run --prefix website dev",
|
||||
"website:build": "npm run build && npm run website:data && npm run --prefix website build",
|
||||
"website:preview": "npm run --prefix website preview"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
22
website/astro.config.mjs
Normal file
22
website/astro.config.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
import { defineConfig } from "astro/config";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: "https://github.github.io/awesome-copilot",
|
||||
base: "/awesome-copilot/",
|
||||
output: "static",
|
||||
integrations: [sitemap()],
|
||||
build: {
|
||||
assets: "assets",
|
||||
},
|
||||
trailingSlash: "always",
|
||||
vite: {
|
||||
build: {
|
||||
sourcemap: true,
|
||||
},
|
||||
css: {
|
||||
devSourcemap: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
205
website/data/tools.yml
Normal file
205
website/data/tools.yml
Normal file
@@ -0,0 +1,205 @@
|
||||
# yaml-language-server: $schema=../../.schemas/tools.schema.json
|
||||
# Tools data for the Awesome GitHub Copilot website
|
||||
# Each tool entry provides information for the tools page
|
||||
|
||||
tools:
|
||||
- id: mcp-server
|
||||
name: Awesome Copilot MCP Server
|
||||
description: >-
|
||||
A Model Context Protocol (MCP) Server that provides prompts for searching and installing
|
||||
prompts, instructions, agents, and skills directly from this repository. Makes it easy
|
||||
to discover and add customizations to your editor.
|
||||
category: MCP Servers
|
||||
featured: true
|
||||
requirements:
|
||||
- Docker installed and running
|
||||
links:
|
||||
blog: https://developer.microsoft.com/blog/announcing-awesome-copilot-mcp-server
|
||||
vscode: https://aka.ms/awesome-copilot/mcp/vscode
|
||||
vscode-insiders: https://aka.ms/awesome-copilot/mcp/vscode-insiders
|
||||
visual-studio: https://aka.ms/awesome-copilot/mcp/vs
|
||||
configuration:
|
||||
type: json
|
||||
content: |
|
||||
{
|
||||
"servers": {
|
||||
"awesome-copilot": {
|
||||
"type": "stdio",
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"ghcr.io/microsoft/mcp-dotnet-samples/awesome-copilot:latest"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
tags:
|
||||
- mcp
|
||||
- docker
|
||||
- search
|
||||
- install
|
||||
|
||||
- id: vscode-extension
|
||||
name: Awesome GitHub Copilot Browser
|
||||
description: >-
|
||||
A VS Code extension that allows you to browse, preview, and download GitHub Copilot
|
||||
customizations from the awesome-copilot repository. Features a tree view for exploring
|
||||
agents, prompts, instructions, and skills with smart caching for better performance.
|
||||
category: VS Code Extensions
|
||||
featured: true
|
||||
requirements:
|
||||
- VS Code version 1.103.0 or higher
|
||||
- Internet connection to fetch repository data
|
||||
- A workspace folder open in VS Code (for downloads)
|
||||
links:
|
||||
github: https://github.com/timheuer/vscode-awesome-copilot
|
||||
vscode: vscode:extension/TimHeuer.awesome-copilot
|
||||
vscode-insiders: vscode-insiders:extension/TimHeuer.awesome-copilot
|
||||
marketplace: https://marketplace.visualstudio.com/items?itemName=TimHeuer.awesome-copilot
|
||||
features:
|
||||
- "🔍 Browse: Explore chat modes, instructions, prompts, agents, and skills in a tree view"
|
||||
- "📖 Preview: View file content before downloading"
|
||||
- "⬇️ Download: Save files to appropriate .github/ folders in your workspace"
|
||||
- "🔃 Refresh: Update repository data with manual refresh"
|
||||
- "💾 Caching: Smart caching for better performance"
|
||||
tags:
|
||||
- vscode
|
||||
- extension
|
||||
- browse
|
||||
- preview
|
||||
- download
|
||||
|
||||
- id: workspace-architect
|
||||
name: Workspace Architect
|
||||
description: >-
|
||||
A comprehensive library of specialized AI personas and chat modes for GitHub Copilot.
|
||||
Includes architectural planning, tech stack guidance, and advanced cognitive reasoning
|
||||
models. Install via npm and use the CLI to download personas and prompts.
|
||||
category: CLI Tools
|
||||
featured: false
|
||||
requirements:
|
||||
- Node.js 20 or higher
|
||||
- npm
|
||||
links:
|
||||
github: https://github.com/archubbuck/workspace-architect
|
||||
npm: https://www.npmjs.com/package/workspace-architect
|
||||
features:
|
||||
- "📦 CLI tool: List and download personas, prompts, and chat modes"
|
||||
- "🎭 Rich persona library: Architecture, React, Azure, and more"
|
||||
- "🧠 Cognitive modes: Advanced reasoning and planning personas"
|
||||
- "⚡ Easy install: npm install -g workspace-architect"
|
||||
configuration:
|
||||
type: bash
|
||||
content: |
|
||||
# Install globally
|
||||
npm install -g workspace-architect
|
||||
|
||||
# List available items
|
||||
workspace-architect list
|
||||
|
||||
# Download a specific item
|
||||
workspace-architect download instructions:basic-setup
|
||||
tags:
|
||||
- cli
|
||||
- npm
|
||||
- personas
|
||||
- chat-modes
|
||||
- prompts
|
||||
|
||||
- id: apm
|
||||
name: APM - Agent Package Manager
|
||||
description: >-
|
||||
npm for AI coding agents. The package manager for AGENTS.md, Agent Skills, and MCP servers.
|
||||
One package installs to every AI agent (Copilot, Cursor, Claude, Codex, Gemini) in their
|
||||
native format.
|
||||
category: CLI Tools
|
||||
featured: true
|
||||
requirements:
|
||||
- Python 3.8 or higher (for pip install)
|
||||
- Or use the shell installer
|
||||
links:
|
||||
github: https://github.com/danielmeppiel/apm
|
||||
pypi: https://pypi.org/project/apm-cli/
|
||||
features:
|
||||
- "📦 Universal packages: One install works for Copilot, Cursor, Claude, and more"
|
||||
- "🔧 Skills & Instructions: Install guardrails and capabilities"
|
||||
- "🔌 MCP Server management: Configure and manage MCP servers"
|
||||
- "🏗️ Create packages: Share your standards and workflows"
|
||||
- "🌐 Multi-source: GitHub, GitHub Enterprise, Azure DevOps"
|
||||
configuration:
|
||||
type: bash
|
||||
content: |
|
||||
# Install via shell script
|
||||
curl -sSL https://raw.githubusercontent.com/danielmeppiel/apm/main/install.sh | sh
|
||||
|
||||
# Or install via pip
|
||||
pip install apm-cli
|
||||
|
||||
# Install a skill
|
||||
apm install danielmeppiel/form-builder
|
||||
|
||||
# Compile for your AI tools
|
||||
apm compile
|
||||
tags:
|
||||
- cli
|
||||
- python
|
||||
- package-manager
|
||||
- skills
|
||||
- agents
|
||||
- mcp
|
||||
|
||||
- id: prompt-registry
|
||||
name: Prompt Registry
|
||||
description: >-
|
||||
A visual marketplace for discovering, installing, and managing GitHub Copilot prompt
|
||||
libraries from multiple sources. Browse bundles in a tile-based interface with search,
|
||||
filters, and one-click install. Supports GitHub, local directories, and APM repositories.
|
||||
category: VS Code Extensions
|
||||
featured: false
|
||||
requirements:
|
||||
- VS Code
|
||||
links:
|
||||
github: https://github.com/AmadeusITGroup/prompt-registry
|
||||
vscode: vscode:extension/AmadeusITGroup.prompt-registry
|
||||
vscode-insiders: vscode-insiders:extension/AmadeusITGroup.prompt-registry
|
||||
marketplace: https://marketplace.visualstudio.com/items?itemName=AmadeusITGroup.prompt-registry
|
||||
features:
|
||||
- "🎨 Visual Marketplace: Browse bundles with search, filters, and one-click install"
|
||||
- "🔌 Multi-Source: Connect to GitHub, local directories, APM, or Awesome Copilot"
|
||||
- "📦 Version Management: Track versions and enable automatic updates"
|
||||
- "👥 Profiles & Hubs: Organize bundles by project/team"
|
||||
- "🌍 Cross-Platform: Works on macOS, Linux, and Windows"
|
||||
tags:
|
||||
- vscode
|
||||
- extension
|
||||
- marketplace
|
||||
- prompts
|
||||
- bundles
|
||||
|
||||
- id: github-node-vs
|
||||
name: GitHub Node for Visual Studio
|
||||
description: >-
|
||||
Adds GitHub and MCP Servers nodes to Solution Explorer in Visual Studio. Quickly access
|
||||
and manage GitHub-specific files like workflows, Copilot instructions, and agents, plus
|
||||
MCP server configurations - all without leaving Visual Studio.
|
||||
category: Visual Studio Extensions
|
||||
featured: false
|
||||
requirements:
|
||||
- Visual Studio 2022 or higher
|
||||
links:
|
||||
github: https://github.com/madskristensen/GitHubNode
|
||||
marketplace: https://marketplace.visualstudio.com/items?itemName=MadsKristensen.GitHubNode
|
||||
features:
|
||||
- "📁 GitHub Node: Easy access to .github folder contents in Solution Explorer"
|
||||
- "➕ Quick Create: Add Copilot instructions, agents, prompts, skills, and workflows"
|
||||
- "🔌 MCP Servers Node: Centralized access to MCP configurations"
|
||||
- "🔄 Git Status Icons: See file status directly in the tree view"
|
||||
- "🌐 Open on GitHub: Quick link to view files on GitHub.com"
|
||||
tags:
|
||||
- visual-studio
|
||||
- extension
|
||||
- solution-explorer
|
||||
- github
|
||||
- mcp
|
||||
5191
website/package-lock.json
generated
Normal file
5191
website/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
website/package.json
Normal file
26
website/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "awesome-copilot-website",
|
||||
"version": "1.0.0",
|
||||
"description": "Awesome GitHub Copilot website",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"keywords": [
|
||||
"github",
|
||||
"copilot",
|
||||
"agents",
|
||||
"prompts"
|
||||
],
|
||||
"author": "GitHub",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@astrojs/sitemap": "^3.7.0",
|
||||
"astro": "^5.16.15",
|
||||
"choices.js": "^11.1.0",
|
||||
"jszip": "^3.10.1"
|
||||
}
|
||||
}
|
||||
BIN
website/public/fonts/MonaspaceArgonNF-Regular.woff2
Executable file
BIN
website/public/fonts/MonaspaceArgonNF-Regular.woff2
Executable file
Binary file not shown.
17
website/public/images/Copilot_Icon_Black.svg
Executable file
17
website/public/images/Copilot_Icon_Black.svg
Executable file
@@ -0,0 +1,17 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_731_27410)">
|
||||
<g clip-path="url(#clip1_731_27410)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M95.6877 67.9677C92.2446 73.9488 72.2522 88.0617 48 88.0617C23.7478 88.0617 3.75535 73.9488 0.312312 67.9677C0.0605215 67.5303 -0.0207525 67.031 -0.0207525 66.5263L-0.0207524 55.8787C-0.0207524 55.4373 0.0473833 54.9986 0.210699 54.5885C1.69959 50.8499 5.59895 45.4192 10.6313 43.9621C11.2985 42.2494 12.2867 39.7456 13.2087 37.8974C13.0543 36.4833 13 35.0248 13 33.5512C13 28.2274 14.1285 23.5576 17.5288 20.0801C19.117 18.4559 21.0876 17.21 23.4246 16.2735C29.0217 11.7264 36.992 7.90112 47.9136 7.90112C58.8352 7.90112 66.9783 11.7264 72.5754 16.2735C74.9124 17.21 76.883 18.4559 78.4712 20.0801C81.8715 23.5576 83 28.2274 83 33.5512C83 35.0248 82.9457 36.4833 82.7913 37.8974C83.7133 39.7456 84.7015 42.2494 85.3687 43.9621C90.401 45.4192 94.3004 50.8499 95.7893 54.5885C95.9526 54.9986 96.0208 55.4373 96.0208 55.8787L96.0208 66.5263C96.0208 67.031 95.9395 67.5303 95.6877 67.9677ZM51.253 32.2606C51.0828 30.9345 51.0018 29.747 50.9995 28.686L50.9995 28.6021C51.0048 25.5242 51.6776 23.5215 52.7524 22.2914C54.1172 20.7295 56.9379 19.5328 62.8829 20.1762C68.9059 20.828 72.2725 22.3229 74.1812 24.2749C76.0291 26.1648 76.9999 28.9922 76.9999 33.5512C76.9999 38.3951 76.3018 41.2568 74.767 42.9977C73.3075 44.6531 70.4335 46 64.1386 46C59.2994 46 56.5326 44.4261 54.7637 42.2493C52.8644 39.912 51.7955 36.4878 51.253 32.2606ZM44.747 32.2606C44.9172 30.9344 44.9982 29.747 45.0005 28.686L45.0005 28.6021C44.9952 25.5242 44.3224 23.5214 43.2476 22.2914C41.8828 20.7295 39.0621 19.5328 33.1171 20.1762C27.0941 20.828 23.7275 22.3229 21.8188 24.2749C19.9709 26.1647 19.0001 28.9922 19.0001 33.5512C19.0001 38.3951 19.6982 41.2568 21.233 42.9977C22.6925 44.6531 25.5665 46 31.8614 46C36.7006 46 39.4674 44.4261 41.2363 42.2493C43.1356 39.9119 44.2045 36.4878 44.747 32.2606ZM48.6889 43.9983C48.4592 43.9995 48.9185 43.9983 48.6889 43.9983C48.4594 43.9983 47.5408 43.9995 47.3111 43.9983C46.8877 44.7075 46.4168 45.3882 45.8926 46.0332C42.8139 49.8219 38.2182 52 31.8613 52C24.9615 52 19.9049 50.564 16.7324 46.9657C16.552 46.7611 16.3906 46.5469 16.3906 46.5469L16 46.9657L16 73.3023C21.7392 76.4213 34.0576 82.0184 48 82.0184C61.9424 82.0184 74.2608 76.4213 80 73.3023L80 46.9657L79.6094 46.5469C79.6094 46.5469 79.4772 46.728 79.2676 46.9657C76.0951 50.564 71.0385 52 64.1387 52C57.7818 52 53.1861 49.8219 50.1074 46.0332C49.5832 45.3882 49.1123 44.7075 48.6889 43.9983Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M58 57C60.2091 57 62 58.7909 62 61L62 69C62 71.2092 60.2091 73 58 73C55.7909 73 54 71.2092 54 69L54 61C54 58.7909 55.7909 57 58 57Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M38 57C40.2091 57 42 58.7909 42 61L42 69C42 71.2092 40.2091 73 38 73C35.7909 73 34 71.2092 34 69L34 61C34 58.7909 35.7909 57 38 57Z" fill="black"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_731_27410">
|
||||
<rect width="96" height="96" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_731_27410">
|
||||
<rect width="96" height="96" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
17
website/public/images/Copilot_Icon_White.svg
Executable file
17
website/public/images/Copilot_Icon_White.svg
Executable file
@@ -0,0 +1,17 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_731_27436)">
|
||||
<g clip-path="url(#clip1_731_27436)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M95.6877 67.9677C92.2446 73.9488 72.2522 88.0617 48 88.0617C23.7478 88.0617 3.75535 73.9488 0.312312 67.9677C0.0605215 67.5303 -0.0207525 67.031 -0.0207525 66.5263L-0.0207524 55.8787C-0.0207524 55.4373 0.0473833 54.9986 0.210699 54.5885C1.69959 50.8499 5.59895 45.4192 10.6313 43.9621C11.2985 42.2494 12.2867 39.7456 13.2087 37.8974C13.0543 36.4833 13 35.0248 13 33.5512C13 28.2274 14.1285 23.5576 17.5288 20.0801C19.117 18.4559 21.0876 17.21 23.4246 16.2735C29.0217 11.7264 36.992 7.90112 47.9136 7.90112C58.8352 7.90112 66.9783 11.7264 72.5754 16.2735C74.9124 17.21 76.883 18.4559 78.4712 20.0801C81.8715 23.5576 83 28.2274 83 33.5512C83 35.0248 82.9457 36.4833 82.7913 37.8974C83.7133 39.7456 84.7015 42.2494 85.3687 43.9621C90.401 45.4192 94.3004 50.8499 95.7893 54.5885C95.9526 54.9986 96.0208 55.4373 96.0208 55.8787L96.0208 66.5263C96.0208 67.031 95.9395 67.5303 95.6877 67.9677ZM51.253 32.2606C51.0828 30.9345 51.0018 29.747 50.9995 28.686L50.9995 28.6021C51.0048 25.5242 51.6776 23.5215 52.7524 22.2914C54.1172 20.7295 56.9379 19.5328 62.8829 20.1762C68.9059 20.828 72.2725 22.3229 74.1812 24.2749C76.0291 26.1648 76.9999 28.9922 76.9999 33.5512C76.9999 38.3951 76.3018 41.2568 74.767 42.9977C73.3075 44.6531 70.4335 46 64.1386 46C59.2994 46 56.5326 44.4261 54.7637 42.2493C52.8644 39.912 51.7955 36.4878 51.253 32.2606ZM44.747 32.2606C44.9172 30.9344 44.9982 29.747 45.0005 28.686L45.0005 28.6021C44.9952 25.5242 44.3224 23.5214 43.2476 22.2914C41.8828 20.7295 39.0621 19.5328 33.1171 20.1762C27.0941 20.828 23.7275 22.3229 21.8188 24.2749C19.9709 26.1647 19.0001 28.9922 19.0001 33.5512C19.0001 38.3951 19.6982 41.2568 21.233 42.9977C22.6925 44.6531 25.5665 46 31.8614 46C36.7006 46 39.4674 44.4261 41.2363 42.2493C43.1356 39.9119 44.2045 36.4878 44.747 32.2606ZM48.6889 43.9983C48.4592 43.9995 48.9185 43.9983 48.6889 43.9983C48.4594 43.9983 47.5408 43.9995 47.3111 43.9983C46.8877 44.7075 46.4168 45.3882 45.8926 46.0332C42.8139 49.8219 38.2182 52 31.8613 52C24.9615 52 19.9049 50.564 16.7324 46.9657C16.552 46.7611 16.3906 46.5469 16.3906 46.5469L16 46.9657L16 73.3023C21.7392 76.4213 34.0576 82.0184 48 82.0184C61.9424 82.0184 74.2608 76.4213 80 73.3023L80 46.9657L79.6094 46.5469C79.6094 46.5469 79.4772 46.728 79.2676 46.9657C76.0951 50.564 71.0385 52 64.1387 52C57.7818 52 53.1861 49.8219 50.1074 46.0332C49.5832 45.3882 49.1123 44.7075 48.6889 43.9983Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M58 57C60.2091 57 62 58.7909 62 61L62 69C62 71.2092 60.2091 73 58 73C55.7909 73 54 71.2092 54 69L54 61C54 58.7909 55.7909 57 58 57Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M38 57C40.2091 57 42 58.7909 42 61L42 69C42 71.2092 40.2091 73 38 73C35.7909 73 34 71.2092 34 69L34 61C34 58.7909 35.7909 57 38 57Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_731_27436">
|
||||
<rect width="96" height="96" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_731_27436">
|
||||
<rect width="96" height="96" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
1576
website/public/styles/global.css
Normal file
1576
website/public/styles/global.css
Normal file
File diff suppressed because it is too large
Load Diff
58
website/src/components/Modal.astro
Normal file
58
website/src/components/Modal.astro
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
// Modal component for viewing file contents
|
||||
---
|
||||
|
||||
<div id="file-modal" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="modal-title">File</h3>
|
||||
<div class="modal-actions">
|
||||
<button id="copy-btn" class="btn btn-secondary" aria-label="Copy to clipboard">
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true">
|
||||
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/>
|
||||
<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/>
|
||||
</svg>
|
||||
<span aria-hidden="true">Copy</span>
|
||||
</button>
|
||||
<button id="download-btn" class="btn btn-secondary" aria-label="Download file">
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true">
|
||||
<path d="M7.47 10.78a.75.75 0 0 0 1.06 0l3.75-3.75a.75.75 0 0 0-1.06-1.06L8.75 8.44V1.75a.75.75 0 0 0-1.5 0v6.69L4.78 5.97a.75.75 0 0 0-1.06 1.06l3.75 3.75ZM3.75 13a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z"/>
|
||||
</svg>
|
||||
<span aria-hidden="true">Download</span>
|
||||
</button>
|
||||
<button id="share-btn" class="btn btn-secondary" aria-label="Copy link to clipboard">
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true">
|
||||
<path d="M7.775 3.275a.75.75 0 0 0 1.06 1.06l1.25-1.25a2 2 0 1 1 2.83 2.83l-2.5 2.5a2 2 0 0 1-2.83 0 .75.75 0 0 0-1.06 1.06 3.5 3.5 0 0 0 4.95 0l2.5-2.5a3.5 3.5 0 0 0-4.95-4.95l-1.25 1.25zm-.025 5.45a.75.75 0 0 0-1.06-1.06l-1.25 1.25a2 2 0 1 1-2.83-2.83l2.5-2.5a2 2 0 0 1 2.83 0 .75.75 0 0 0 1.06-1.06 3.5 3.5 0 0 0-4.95 0l-2.5 2.5a3.5 3.5 0 0 0 4.95 4.95l1.25-1.25z"/>
|
||||
</svg>
|
||||
<span aria-hidden="true">Share</span>
|
||||
</button>
|
||||
<div id="install-dropdown" class="install-dropdown" style="display: none;">
|
||||
<a id="install-btn-main" class="btn btn-primary install-btn-main" target="_blank" rel="noopener">
|
||||
Install
|
||||
</a>
|
||||
<button type="button" class="btn btn-primary install-btn-toggle" aria-label="Install options" aria-expanded="false" aria-haspopup="true">
|
||||
<svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor" aria-hidden="true">
|
||||
<path d="M4.427 7.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 7H4.604a.25.25 0 00-.177.427z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="install-dropdown-menu" role="menu">
|
||||
<a id="install-vscode" target="_blank" rel="noopener" role="menuitem">
|
||||
VS Code
|
||||
</a>
|
||||
<a id="install-insiders" target="_blank" rel="noopener" role="menuitem">
|
||||
VS Code Insiders
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<button id="close-modal" class="btn btn-icon" aria-label="Close dialog">
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true">
|
||||
<path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre id="modal-content"><code></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
165
website/src/layouts/BaseLayout.astro
Normal file
165
website/src/layouts/BaseLayout.astro
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
activeNav?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
description = "Community-driven collection of custom agents, prompts, and instructions for GitHub Copilot",
|
||||
activeNav = "",
|
||||
} = Astro.props;
|
||||
const base = import.meta.env.BASE_URL;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{title} - Awesome GitHub Copilot</title>
|
||||
<meta name="description" content={description} />
|
||||
<link rel="stylesheet" href={`${base}styles/global.css`} />
|
||||
<link
|
||||
rel="icon"
|
||||
href={`${base}images/Copilot_Icon_Black.svg`}
|
||||
type="image/svg+xml"
|
||||
/>
|
||||
</head>
|
||||
<body data-base-path={base}>
|
||||
<a href="#main-content" class="skip-link">Skip to main content</a>
|
||||
<header class="site-header">
|
||||
<div class="container">
|
||||
<div class="header-content">
|
||||
<a href={base} class="logo">
|
||||
<img
|
||||
src={`${base}images/Copilot_Icon_White.svg`}
|
||||
alt=""
|
||||
class="logo-icon logo-icon-dark"
|
||||
width="32"
|
||||
height="32"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<img
|
||||
src={`${base}images/Copilot_Icon_Black.svg`}
|
||||
alt=""
|
||||
class="logo-icon logo-icon-light"
|
||||
width="32"
|
||||
height="32"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="logo-text">Awesome Copilot</span>
|
||||
</a>
|
||||
<nav class="main-nav" aria-label="Main navigation">
|
||||
<a
|
||||
href={`${base}agents/`}
|
||||
class:list={[{ active: activeNav === "agents" }]}>Agents</a
|
||||
>
|
||||
<a
|
||||
href={`${base}prompts/`}
|
||||
class:list={[{ active: activeNav === "prompts" }]}>Prompts</a
|
||||
>
|
||||
<a
|
||||
href={`${base}instructions/`}
|
||||
class:list={[{ active: activeNav === "instructions" }]}
|
||||
>Instructions</a
|
||||
>
|
||||
<a
|
||||
href={`${base}skills/`}
|
||||
class:list={[{ active: activeNav === "skills" }]}>Skills</a
|
||||
>
|
||||
<a
|
||||
href={`${base}collections/`}
|
||||
class:list={[{ active: activeNav === "collections" }]}
|
||||
>Collections</a
|
||||
>
|
||||
<a
|
||||
href={`${base}tools/`}
|
||||
class:list={[{ active: activeNav === "tools" }]}>Tools</a
|
||||
>
|
||||
<a
|
||||
href={`${base}samples/`}
|
||||
class:list={[{ active: activeNav === "samples" }]}>Samples</a
|
||||
>
|
||||
</nav>
|
||||
<div class="header-actions">
|
||||
<button
|
||||
id="theme-toggle"
|
||||
class="theme-toggle"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
<svg
|
||||
class="icon-sun"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0V.75A.75.75 0 0 1 8 0zm0 13a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 8 13zM2.343 2.343a.75.75 0 0 1 1.061 0l1.06 1.061a.75.75 0 0 1-1.06 1.06l-1.06-1.06a.75.75 0 0 1 0-1.06zm9.193 9.193a.75.75 0 0 1 1.06 0l1.061 1.06a.75.75 0 0 1-1.06 1.061l-1.061-1.06a.75.75 0 0 1 0-1.061zM0 8a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 8zm13 0a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 0 1.5h-1.5A.75.75 0 0 1 13 8zM2.343 13.657a.75.75 0 0 1 0-1.061l1.06-1.06a.75.75 0 0 1 1.061 1.06l-1.06 1.06a.75.75 0 0 1-1.061 0zm9.193-9.193a.75.75 0 0 1 0-1.06l1.061-1.061a.75.75 0 0 1 1.06 1.06l-1.06 1.061a.75.75 0 0 1-1.061 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg
|
||||
class="icon-moon"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M9.598 1.591a.75.75 0 0 1 .785-.175 7 7 0 1 1-8.967 8.967.75.75 0 0 1 .961-.96 5.5 5.5 0 0 0 7.046-7.046.75.75 0 0 1 .175-.786zm1.616 1.945a7 7 0 0 1-7.678 7.678 5.5 5.5 0 1 0 7.678-7.678z"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<a
|
||||
href="https://github.com/github/awesome-copilot"
|
||||
class="github-link"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
aria-label="View on GitHub"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<slot />
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p>
|
||||
<a
|
||||
href="https://github.com/github/awesome-copilot"
|
||||
target="_blank"
|
||||
rel="noopener">GitHub</a
|
||||
> ·
|
||||
<a
|
||||
href="https://github.com/github/awesome-copilot/blob/main/CONTRIBUTING.md"
|
||||
target="_blank"
|
||||
rel="noopener">Contribute</a
|
||||
> ·
|
||||
<a
|
||||
href="https://github.com/github/awesome-copilot/blob/main/LICENSE"
|
||||
target="_blank"
|
||||
rel="noopener">MIT License</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
import "../scripts/theme";
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
54
website/src/pages/agents.astro
Normal file
54
website/src/pages/agents.astro
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import Modal from '../components/Modal.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="Agents" description="Specialized agents that enhance GitHub Copilot for specific technologies, workflows, and domains" activeNav="agents">
|
||||
<main id="main-content">
|
||||
<div class="page-header">
|
||||
<div class="container">
|
||||
<h1>🤖 Custom Agents</h1>
|
||||
<p>Specialized agents that enhance GitHub Copilot for specific technologies, workflows, and domains</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
<div class="container">
|
||||
<div class="search-bar">
|
||||
<label for="search-input" class="sr-only">Search agents</label>
|
||||
<input type="text" id="search-input" placeholder="Search agents..." autocomplete="off">
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filters-bar" id="filters-bar">
|
||||
<div class="filter-group">
|
||||
<label for="filter-model">Model:</label>
|
||||
<select id="filter-model" multiple aria-label="Filter by model"></select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="filter-tool">Tool:</label>
|
||||
<select id="filter-tool" multiple aria-label="Filter by tool"></select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="filter-handoffs">
|
||||
Has Handoffs
|
||||
</label>
|
||||
</div>
|
||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
||||
</div>
|
||||
|
||||
<div class="results-count" id="results-count" aria-live="polite"></div>
|
||||
<div class="resource-list" id="resource-list" role="list">
|
||||
<div class="loading" aria-live="polite">Loading agents...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Modal />
|
||||
|
||||
<script>
|
||||
import '../scripts/pages/agents';
|
||||
</script>
|
||||
</BaseLayout>
|
||||
49
website/src/pages/collections.astro
Normal file
49
website/src/pages/collections.astro
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import Modal from '../components/Modal.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="Collections" description="Curated collections of prompts, instructions, and agents for specific workflows" activeNav="collections">
|
||||
<main id="main-content">
|
||||
<div class="page-header">
|
||||
<div class="container">
|
||||
<h1>📦 Collections</h1>
|
||||
<p>Curated collections of prompts, instructions, and agents for specific workflows</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
<div class="container">
|
||||
<div class="search-bar">
|
||||
<label for="search-input" class="sr-only">Search collections</label>
|
||||
<input type="text" id="search-input" placeholder="Search collections..." autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="filters-bar" id="filters-bar">
|
||||
<div class="filter-group">
|
||||
<label for="filter-tag">Tag:</label>
|
||||
<select id="filter-tag" multiple aria-label="Filter by tag"></select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="filter-featured">
|
||||
Featured Only
|
||||
</label>
|
||||
</div>
|
||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
||||
</div>
|
||||
|
||||
<div class="results-count" id="results-count" aria-live="polite"></div>
|
||||
<div class="resource-list" id="resource-list" role="list">
|
||||
<div class="loading" aria-live="polite">Loading collections...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Modal />
|
||||
|
||||
<script>
|
||||
import '../scripts/pages/collections';
|
||||
</script>
|
||||
</BaseLayout>
|
||||
120
website/src/pages/index.astro
Normal file
120
website/src/pages/index.astro
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import Modal from '../components/Modal.astro';
|
||||
|
||||
const base = import.meta.env.BASE_URL;
|
||||
---
|
||||
|
||||
<BaseLayout title="Home" activeNav="">
|
||||
<main id="main-content">
|
||||
<!-- Hero Section -->
|
||||
<section class="hero" aria-labelledby="hero-heading">
|
||||
<div class="container">
|
||||
<h1 id="hero-heading">Awesome GitHub Copilot</h1>
|
||||
<p class="hero-subtitle">Community-contributed instructions, prompts, agents, and skills to enhance your GitHub Copilot experience</p>
|
||||
<div class="hero-search">
|
||||
<label for="global-search" class="sr-only">Search all resources</label>
|
||||
<input type="text" id="global-search" placeholder="Search all resources..." autocomplete="off" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="search-results">
|
||||
<div id="search-results" class="search-results hidden" role="listbox" aria-label="Search results"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<section class="quick-links" aria-labelledby="quick-links-heading">
|
||||
<h2 id="quick-links-heading" class="sr-only">Browse Resources</h2>
|
||||
<div class="container">
|
||||
<div class="cards-grid">
|
||||
<a href={`${base}agents/`} class="card card-with-count" id="card-agents">
|
||||
<div class="card-icon" aria-hidden="true">🤖</div>
|
||||
<div class="card-content">
|
||||
<h3>Agents</h3>
|
||||
<p>Custom agents for specialized Copilot experiences</p>
|
||||
</div>
|
||||
<div class="card-count" data-count="agents" aria-label="Agent count">-</div>
|
||||
</a>
|
||||
<a href={`${base}prompts/`} class="card card-with-count" id="card-prompts">
|
||||
<div class="card-icon" aria-hidden="true">🎯</div>
|
||||
<div class="card-content">
|
||||
<h3>Prompts</h3>
|
||||
<p>Ready-to-use prompt templates for development tasks</p>
|
||||
</div>
|
||||
<div class="card-count" data-count="prompts" aria-label="Prompt count">-</div>
|
||||
</a>
|
||||
<a href={`${base}instructions/`} class="card card-with-count" id="card-instructions">
|
||||
<div class="card-icon" aria-hidden="true">📋</div>
|
||||
<div class="card-content">
|
||||
<h3>Instructions</h3>
|
||||
<p>Coding standards and best practices for Copilot</p>
|
||||
</div>
|
||||
<div class="card-count" data-count="instructions" aria-label="Instruction count">-</div>
|
||||
</a>
|
||||
<a href={`${base}skills/`} class="card card-with-count" id="card-skills">
|
||||
<div class="card-icon" aria-hidden="true">⚡</div>
|
||||
<div class="card-content">
|
||||
<h3>Skills</h3>
|
||||
<p>Self-contained folders with instructions and resources</p>
|
||||
</div>
|
||||
<div class="card-count" data-count="skills" aria-label="Skill count">-</div>
|
||||
</a>
|
||||
<a href={`${base}collections/`} class="card card-with-count" id="card-collections">
|
||||
<div class="card-icon" aria-hidden="true">📦</div>
|
||||
<div class="card-content">
|
||||
<h3>Collections</h3>
|
||||
<p>Curated collections organized by themes</p>
|
||||
</div>
|
||||
<div class="card-count" data-count="collections" aria-label="Collection count">-</div>
|
||||
</a>
|
||||
<a href={`${base}tools/`} class="card card-with-count" id="card-tools">
|
||||
<div class="card-icon" aria-hidden="true">🔧</div>
|
||||
<div class="card-content">
|
||||
<h3>Tools</h3>
|
||||
<p>MCP servers and developer tools</p>
|
||||
</div>
|
||||
<div class="card-count" data-count="tools" aria-label="Tool count">-</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Featured Collections -->
|
||||
<section class="featured" aria-labelledby="featured-heading">
|
||||
<div class="container">
|
||||
<h2 id="featured-heading">Featured Collections</h2>
|
||||
<div id="featured-collections" class="cards-grid" aria-live="polite">
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Getting Started -->
|
||||
<section class="getting-started" aria-labelledby="getting-started-heading">
|
||||
<div class="container">
|
||||
<h2 id="getting-started-heading">Getting Started</h2>
|
||||
<div class="steps">
|
||||
<div class="step">
|
||||
<div class="step-number" aria-hidden="true">1</div>
|
||||
<h3>Browse</h3>
|
||||
<p>Explore agents, prompts, instructions, and skills</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-number" aria-hidden="true">2</div>
|
||||
<h3>Preview</h3>
|
||||
<p>Click any item to view its full content</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-number" aria-hidden="true">3</div>
|
||||
<h3>Install</h3>
|
||||
<p>One-click install to VS Code or copy to clipboard</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Modal />
|
||||
|
||||
<script>
|
||||
import '../scripts/pages/index';
|
||||
</script>
|
||||
</BaseLayout>
|
||||
43
website/src/pages/instructions.astro
Normal file
43
website/src/pages/instructions.astro
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import Modal from '../components/Modal.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="Instructions" description="Coding standards and best practices for GitHub Copilot" activeNav="instructions">
|
||||
<main id="main-content">
|
||||
<div class="page-header">
|
||||
<div class="container">
|
||||
<h1>📋 Instructions</h1>
|
||||
<p>Coding standards and best practices for GitHub Copilot</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
<div class="container">
|
||||
<div class="search-bar">
|
||||
<label for="search-input" class="sr-only">Search instructions</label>
|
||||
<input type="text" id="search-input" placeholder="Search instructions..." autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="filters-bar" id="filters-bar">
|
||||
<div class="filter-group">
|
||||
<label for="filter-extension">File Extension:</label>
|
||||
<select id="filter-extension" multiple aria-label="Filter by file extension"></select>
|
||||
</div>
|
||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
||||
</div>
|
||||
|
||||
<div class="results-count" id="results-count" aria-live="polite"></div>
|
||||
<div class="resource-list" id="resource-list" role="list">
|
||||
<div class="loading" aria-live="polite">Loading instructions...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Modal />
|
||||
|
||||
<script>
|
||||
import '../scripts/pages/instructions';
|
||||
</script>
|
||||
</BaseLayout>
|
||||
43
website/src/pages/prompts.astro
Normal file
43
website/src/pages/prompts.astro
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import Modal from '../components/Modal.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="Prompts" description="Ready-to-use prompt templates for development tasks with GitHub Copilot" activeNav="prompts">
|
||||
<main id="main-content">
|
||||
<div class="page-header">
|
||||
<div class="container">
|
||||
<h1>🎯 Prompts</h1>
|
||||
<p>Ready-to-use prompt templates for development tasks with GitHub Copilot</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
<div class="container">
|
||||
<div class="search-bar">
|
||||
<label for="search-input" class="sr-only">Search prompts</label>
|
||||
<input type="text" id="search-input" placeholder="Search prompts..." autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="filters-bar" id="filters-bar">
|
||||
<div class="filter-group">
|
||||
<label for="filter-tool">Tool:</label>
|
||||
<select id="filter-tool" multiple aria-label="Filter by tool"></select>
|
||||
</div>
|
||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
||||
</div>
|
||||
|
||||
<div class="results-count" id="results-count" aria-live="polite"></div>
|
||||
<div class="resource-list" id="resource-list" role="list">
|
||||
<div class="loading" aria-live="polite">Loading prompts...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Modal />
|
||||
|
||||
<script>
|
||||
import '../scripts/pages/prompts';
|
||||
</script>
|
||||
</BaseLayout>
|
||||
248
website/src/pages/samples.astro
Normal file
248
website/src/pages/samples.astro
Normal file
@@ -0,0 +1,248 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import Modal from '../components/Modal.astro';
|
||||
|
||||
const base = import.meta.env.BASE_URL;
|
||||
---
|
||||
|
||||
<BaseLayout title="Samples" description="Code samples, recipes, and examples for building with GitHub Copilot" activeNav="samples">
|
||||
<main id="main-content">
|
||||
<div class="page-header">
|
||||
<div class="container">
|
||||
<h1>📚 Samples & Recipes</h1>
|
||||
<p>Code samples, recipes, and examples for building with GitHub Copilot tools</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
<div class="container">
|
||||
<div class="search-bar">
|
||||
<label for="search-input" class="sr-only">Search recipes</label>
|
||||
<input type="text" id="search-input" placeholder="Search recipes..." autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="filters-bar" id="filters-bar">
|
||||
<div class="filter-group">
|
||||
<label for="filter-language">Language:</label>
|
||||
<select id="filter-language" aria-label="Filter by language">
|
||||
<option value="">All Languages</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="filter-tag">Tags:</label>
|
||||
<select id="filter-tag" multiple aria-label="Filter by tags"></select>
|
||||
</div>
|
||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
||||
</div>
|
||||
|
||||
<div class="results-count" id="results-count" aria-live="polite"></div>
|
||||
|
||||
<div id="samples-list" role="list">
|
||||
<div class="loading" aria-live="polite">Loading samples...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Modal />
|
||||
|
||||
<style is:global>
|
||||
/* Cookbook Section */
|
||||
.cookbook-section {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.cookbook-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.cookbook-info h2 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 24px;
|
||||
color: var(--color-text-emphasis);
|
||||
}
|
||||
|
||||
.cookbook-info p {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.cookbook-languages {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lang-tab {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--color-bg-secondary);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
font-family: 'Monaspace Argon NF', monospace;
|
||||
font-size: 18px;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.lang-tab:hover {
|
||||
border-color: var(--color-link);
|
||||
background: var(--color-bg-tertiary);
|
||||
}
|
||||
|
||||
.lang-tab.active {
|
||||
border-color: var(--color-link);
|
||||
background: var(--color-link);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Recipe Grid */
|
||||
.recipes-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* Recipe Card */
|
||||
.recipe-card {
|
||||
background: var(--color-card-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 24px;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.recipe-card:hover {
|
||||
border-color: var(--color-link);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.recipe-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.recipe-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: var(--color-text-emphasis);
|
||||
}
|
||||
|
||||
.recipe-langs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lang-indicator {
|
||||
font-family: 'Monaspace Argon NF', monospace;
|
||||
font-size: 16px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.recipe-description {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 14px;
|
||||
margin: 0 0 16px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.recipe-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.recipe-tag {
|
||||
background: var(--color-bg-secondary);
|
||||
color: var(--color-text-muted);
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.recipe-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.recipe-actions .btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.recipe-actions .btn svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.no-variant {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 64px 24px;
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius-lg);
|
||||
border: 1px dashed var(--color-border);
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Search highlight */
|
||||
.search-highlight {
|
||||
background-color: var(--color-warning);
|
||||
color: var(--color-text-emphasis);
|
||||
padding: 0 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.cookbook-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.recipes-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.recipe-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.recipe-actions .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import '../scripts/pages/samples';
|
||||
</script>
|
||||
</BaseLayout>
|
||||
49
website/src/pages/skills.astro
Normal file
49
website/src/pages/skills.astro
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import Modal from '../components/Modal.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="Skills" description="Self-contained agent skills with instructions and bundled resources" activeNav="skills">
|
||||
<main id="main-content">
|
||||
<div class="page-header">
|
||||
<div class="container">
|
||||
<h1>⚡ Skills</h1>
|
||||
<p>Self-contained agent skills with instructions and bundled resources</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
<div class="container">
|
||||
<div class="search-bar">
|
||||
<label for="search-input" class="sr-only">Search skills</label>
|
||||
<input type="text" id="search-input" placeholder="Search skills..." autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="filters-bar" id="filters-bar">
|
||||
<div class="filter-group">
|
||||
<label for="filter-category">Category:</label>
|
||||
<select id="filter-category" multiple aria-label="Filter by category"></select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="filter-has-assets">
|
||||
Has Bundled Assets
|
||||
</label>
|
||||
</div>
|
||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
||||
</div>
|
||||
|
||||
<div class="results-count" id="results-count" aria-live="polite"></div>
|
||||
<div class="resource-list" id="resource-list" role="list">
|
||||
<div class="loading" aria-live="polite">Loading skills...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Modal />
|
||||
|
||||
<script>
|
||||
import '../scripts/pages/skills';
|
||||
</script>
|
||||
</BaseLayout>
|
||||
311
website/src/pages/tools.astro
Normal file
311
website/src/pages/tools.astro
Normal file
@@ -0,0 +1,311 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Tools"
|
||||
description="MCP servers and developer tools for GitHub Copilot"
|
||||
activeNav="tools"
|
||||
>
|
||||
<main id="main-content">
|
||||
<div class="page-header">
|
||||
<div class="container">
|
||||
<h1>🔧 Tools</h1>
|
||||
<p>MCP servers and developer tools for GitHub Copilot</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
<div class="container">
|
||||
<div class="search-section">
|
||||
<div class="search-bar">
|
||||
<label for="search-input" class="sr-only">Search tools</label>
|
||||
<input
|
||||
type="text"
|
||||
id="search-input"
|
||||
placeholder="Search tools..."
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="filters">
|
||||
<label for="filter-category" class="sr-only">Filter by category</label>
|
||||
<select id="filter-category" class="filter-select" aria-label="Filter by category">
|
||||
<option value="">All Categories</option>
|
||||
</select>
|
||||
<button id="clear-filters" class="btn btn-secondary btn-small"
|
||||
>Clear</button
|
||||
>
|
||||
</div>
|
||||
<div id="results-count" class="results-count" aria-live="polite"></div>
|
||||
</div>
|
||||
|
||||
<div id="tools-list" role="list"></div>
|
||||
|
||||
<div class="coming-soon">
|
||||
<h2>More Tools Coming Soon</h2>
|
||||
<p>
|
||||
We're working on additional tools to enhance your GitHub Copilot
|
||||
experience. Check back soon!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style is:global>
|
||||
.search-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-card-bg);
|
||||
color: var(--color-text-primary);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px rgba(133, 52, 243, 0.1);
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-card-bg);
|
||||
color: var(--color-text-primary);
|
||||
font-size: 14px;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.results-count {
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 48px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 48px;
|
||||
background-color: var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius-lg);
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
background-color: var(--color-card-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 32px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.tool-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tool-header h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: var(--color-text-emphasis);
|
||||
}
|
||||
|
||||
.tool-badges {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tool-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tool-badge.featured {
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tool-badge.category {
|
||||
background-color: var(--color-purple-light);
|
||||
color: var(--color-purple-dark);
|
||||
}
|
||||
|
||||
.tool-description {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.tool-section {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.tool-section h3 {
|
||||
margin-bottom: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-emphasis);
|
||||
}
|
||||
|
||||
.tool-section ul {
|
||||
margin: 0;
|
||||
padding-left: 24px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.tool-section li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tool-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.tool-tag {
|
||||
background-color: var(--color-bg-secondary);
|
||||
color: var(--color-text-muted);
|
||||
padding: 4px 12px;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.tool-config {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.tool-config h3 {
|
||||
margin-bottom: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-emphasis);
|
||||
}
|
||||
|
||||
.tool-config-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tool-config pre {
|
||||
background-color: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tool-config code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
color: var(--color-text-primary);
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.tool-actions {
|
||||
margin-top: 24px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.coming-soon {
|
||||
text-align: center;
|
||||
padding: 48px;
|
||||
background-color: var(--color-bg-secondary);
|
||||
border: 1px dashed var(--color-border);
|
||||
border-radius: var(--border-radius-lg);
|
||||
}
|
||||
|
||||
.coming-soon h2 {
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.coming-soon p {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.copy-config-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
background-color: var(--color-bg-primary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
margin-top: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.copy-config-btn:hover {
|
||||
background-color: var(--color-bg-secondary);
|
||||
color: var(--color-text-primary);
|
||||
border-color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.copy-config-btn.copied {
|
||||
background-color: var(--color-success);
|
||||
color: white;
|
||||
border-color: var(--color-success);
|
||||
}
|
||||
|
||||
/* Search highlight */
|
||||
.search-highlight {
|
||||
background-color: var(--color-warning);
|
||||
color: var(--color-text-emphasis);
|
||||
padding: 0 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { initToolsPage } from "../scripts/pages/tools";
|
||||
initToolsPage();
|
||||
</script>
|
||||
</BaseLayout>
|
||||
34
website/src/scripts/choices.ts
Normal file
34
website/src/scripts/choices.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Choices.js wrapper with sensible defaults
|
||||
*/
|
||||
import Choices from 'choices.js';
|
||||
import 'choices.js/public/assets/styles/choices.min.css';
|
||||
|
||||
export type { Choices };
|
||||
|
||||
/**
|
||||
* Get selected values from a Choices instance
|
||||
*/
|
||||
export function getChoicesValues(choices: Choices): string[] {
|
||||
const val = choices.getValue(true);
|
||||
return Array.isArray(val) ? val : (val ? [val] : []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Choices instance with sensible defaults
|
||||
*/
|
||||
export function createChoices(selector: string | HTMLSelectElement, options: Partial<Choices['config']> = {}): Choices {
|
||||
return new Choices(selector, {
|
||||
removeItemButton: true,
|
||||
searchPlaceholderValue: 'Search...',
|
||||
noResultsText: 'No results found',
|
||||
noChoicesText: 'No options available',
|
||||
itemSelectText: '',
|
||||
shouldSort: false,
|
||||
searchResultLimit: 100,
|
||||
resetScrollPosition: false,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export { Choices };
|
||||
7
website/src/scripts/jszip.ts
Normal file
7
website/src/scripts/jszip.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* JSZip entry point for bundling
|
||||
*/
|
||||
import JSZip from 'jszip';
|
||||
|
||||
export { JSZip };
|
||||
export default JSZip;
|
||||
546
website/src/scripts/modal.ts
Normal file
546
website/src/scripts/modal.ts
Normal file
@@ -0,0 +1,546 @@
|
||||
/**
|
||||
* Modal functionality for file viewing
|
||||
*/
|
||||
|
||||
import {
|
||||
fetchFileContent,
|
||||
fetchData,
|
||||
getVSCodeInstallUrl,
|
||||
copyToClipboard,
|
||||
showToast,
|
||||
downloadFile,
|
||||
shareFile,
|
||||
getResourceType,
|
||||
escapeHtml,
|
||||
getResourceIcon,
|
||||
} from "./utils";
|
||||
|
||||
// Modal state
|
||||
let currentFilePath: string | null = null;
|
||||
let currentFileContent: string | null = null;
|
||||
let currentFileType: string | null = null;
|
||||
let triggerElement: HTMLElement | null = null;
|
||||
|
||||
// Collection data cache
|
||||
interface CollectionItem {
|
||||
path: string;
|
||||
kind: string;
|
||||
usage?: string | null;
|
||||
}
|
||||
|
||||
interface Collection {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
path: string;
|
||||
items: CollectionItem[];
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
interface CollectionsData {
|
||||
items: Collection[];
|
||||
}
|
||||
|
||||
let collectionsCache: CollectionsData | null = null;
|
||||
|
||||
/**
|
||||
* Get all focusable elements within a container
|
||||
*/
|
||||
function getFocusableElements(container: HTMLElement): HTMLElement[] {
|
||||
const focusableSelectors = [
|
||||
"button:not([disabled])",
|
||||
"a[href]",
|
||||
"input:not([disabled])",
|
||||
"select:not([disabled])",
|
||||
"textarea:not([disabled])",
|
||||
'[tabindex]:not([tabindex="-1"])',
|
||||
].join(", ");
|
||||
|
||||
return Array.from(
|
||||
container.querySelectorAll<HTMLElement>(focusableSelectors)
|
||||
).filter((el) => el.offsetParent !== null); // Filter out hidden elements
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle keyboard navigation within modal (focus trap)
|
||||
*/
|
||||
function handleModalKeydown(e: KeyboardEvent, modal: HTMLElement): void {
|
||||
if (e.key === "Tab") {
|
||||
const focusableElements = getFocusableElements(modal);
|
||||
if (focusableElements.length === 0) return;
|
||||
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
|
||||
if (e.shiftKey) {
|
||||
// Shift+Tab: if on first element, wrap to last
|
||||
if (document.activeElement === firstElement) {
|
||||
e.preventDefault();
|
||||
lastElement.focus();
|
||||
}
|
||||
} else {
|
||||
// Tab: if on last element, wrap to first
|
||||
if (document.activeElement === lastElement) {
|
||||
e.preventDefault();
|
||||
firstElement.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup modal functionality
|
||||
*/
|
||||
export function setupModal(): void {
|
||||
const modal = document.getElementById("file-modal");
|
||||
const closeBtn = document.getElementById("close-modal");
|
||||
const copyBtn = document.getElementById("copy-btn");
|
||||
const downloadBtn = document.getElementById("download-btn");
|
||||
const shareBtn = document.getElementById("share-btn");
|
||||
|
||||
if (!modal) return;
|
||||
|
||||
closeBtn?.addEventListener("click", closeModal);
|
||||
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target === modal) closeModal();
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (!modal.classList.contains("hidden")) {
|
||||
if (e.key === "Escape") {
|
||||
closeModal();
|
||||
} else {
|
||||
handleModalKeydown(e, modal);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
copyBtn?.addEventListener("click", async () => {
|
||||
if (currentFileContent) {
|
||||
const success = await copyToClipboard(currentFileContent);
|
||||
showToast(
|
||||
success ? "Copied to clipboard!" : "Failed to copy",
|
||||
success ? "success" : "error"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
downloadBtn?.addEventListener("click", async () => {
|
||||
if (currentFilePath) {
|
||||
const success = await downloadFile(currentFilePath);
|
||||
showToast(
|
||||
success ? "Download started!" : "Download failed",
|
||||
success ? "success" : "error"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
shareBtn?.addEventListener("click", async () => {
|
||||
if (currentFilePath) {
|
||||
const success = await shareFile(currentFilePath);
|
||||
showToast(
|
||||
success ? "Link copied to clipboard!" : "Failed to copy link",
|
||||
success ? "success" : "error"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Setup install dropdown toggle
|
||||
setupInstallDropdown("install-dropdown");
|
||||
|
||||
// Handle browser back/forward navigation
|
||||
window.addEventListener("hashchange", handleHashChange);
|
||||
|
||||
// Check for deep link on initial load
|
||||
handleHashChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle hash changes for deep linking
|
||||
*/
|
||||
function handleHashChange(): void {
|
||||
const hash = window.location.hash;
|
||||
|
||||
if (hash && hash.startsWith("#file=")) {
|
||||
const filePath = decodeURIComponent(hash.slice(6));
|
||||
if (filePath && filePath !== currentFilePath) {
|
||||
const type = getResourceType(filePath);
|
||||
openFileModal(filePath, type, false); // Don't update hash since we're responding to it
|
||||
}
|
||||
} else if (!hash || hash === "#") {
|
||||
// No hash or empty hash - close modal if open
|
||||
if (currentFilePath) {
|
||||
closeModal(false); // Don't update hash since we're responding to it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update URL hash for deep linking
|
||||
*/
|
||||
function updateHash(filePath: string | null): void {
|
||||
if (filePath) {
|
||||
const newHash = `#file=${encodeURIComponent(filePath)}`;
|
||||
if (window.location.hash !== newHash) {
|
||||
history.pushState(null, "", newHash);
|
||||
}
|
||||
} else {
|
||||
if (window.location.hash) {
|
||||
history.pushState(
|
||||
null,
|
||||
"",
|
||||
window.location.pathname + window.location.search
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup install dropdown toggle functionality
|
||||
*/
|
||||
export function setupInstallDropdown(containerId: string): void {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
const toggle = container.querySelector<HTMLButtonElement>(
|
||||
".install-btn-toggle"
|
||||
);
|
||||
const menuItems = container.querySelectorAll<HTMLAnchorElement>(
|
||||
".install-dropdown-menu a"
|
||||
);
|
||||
|
||||
toggle?.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const isOpen = container.classList.toggle("open");
|
||||
toggle.setAttribute("aria-expanded", String(isOpen));
|
||||
|
||||
// Focus first menu item when opening
|
||||
if (isOpen && menuItems.length > 0) {
|
||||
menuItems[0].focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard navigation for dropdown
|
||||
toggle?.addEventListener("keydown", (e) => {
|
||||
if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
container.classList.add("open");
|
||||
toggle.setAttribute("aria-expanded", "true");
|
||||
if (menuItems.length > 0) {
|
||||
menuItems[0].focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard navigation within menu
|
||||
menuItems.forEach((item, index) => {
|
||||
item.addEventListener("keydown", (e) => {
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
e.preventDefault();
|
||||
if (index < menuItems.length - 1) {
|
||||
menuItems[index + 1].focus();
|
||||
}
|
||||
break;
|
||||
case "ArrowUp":
|
||||
e.preventDefault();
|
||||
if (index > 0) {
|
||||
menuItems[index - 1].focus();
|
||||
} else {
|
||||
toggle?.focus();
|
||||
}
|
||||
break;
|
||||
case "Escape":
|
||||
e.preventDefault();
|
||||
container.classList.remove("open");
|
||||
toggle?.setAttribute("aria-expanded", "false");
|
||||
toggle?.focus();
|
||||
break;
|
||||
case "Tab":
|
||||
// Close menu on tab out
|
||||
container.classList.remove("open");
|
||||
toggle?.setAttribute("aria-expanded", "false");
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!container.contains(e.target as Node)) {
|
||||
container.classList.remove("open");
|
||||
toggle?.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
});
|
||||
|
||||
// Close dropdown when clicking a menu item
|
||||
container.querySelectorAll(".install-dropdown-menu a").forEach((link) => {
|
||||
link.addEventListener("click", () => {
|
||||
container.classList.remove("open");
|
||||
toggle?.setAttribute("aria-expanded", "false");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open file viewer modal
|
||||
* @param filePath - Path to the file
|
||||
* @param type - Resource type (agent, prompt, instruction, etc.)
|
||||
* @param updateUrl - Whether to update the URL hash (default: true)
|
||||
* @param trigger - The element that triggered the modal (for focus return)
|
||||
*/
|
||||
export async function openFileModal(
|
||||
filePath: string,
|
||||
type: string,
|
||||
updateUrl = true,
|
||||
trigger?: HTMLElement
|
||||
): Promise<void> {
|
||||
const modal = document.getElementById("file-modal");
|
||||
const title = document.getElementById("modal-title");
|
||||
const modalContent = document.getElementById("modal-content");
|
||||
const contentEl = modalContent?.querySelector("code");
|
||||
const installDropdown = document.getElementById("install-dropdown");
|
||||
const installBtnMain = document.getElementById(
|
||||
"install-btn-main"
|
||||
) as HTMLAnchorElement | null;
|
||||
const installVscode = document.getElementById(
|
||||
"install-vscode"
|
||||
) as HTMLAnchorElement | null;
|
||||
const installInsiders = document.getElementById(
|
||||
"install-insiders"
|
||||
) as HTMLAnchorElement | null;
|
||||
const copyBtn = document.getElementById("copy-btn");
|
||||
const downloadBtn = document.getElementById("download-btn");
|
||||
const closeBtn = document.getElementById("close-modal");
|
||||
|
||||
if (!modal || !title || !modalContent) return;
|
||||
|
||||
currentFilePath = filePath;
|
||||
currentFileType = type;
|
||||
|
||||
// Track trigger element for focus return
|
||||
triggerElement = trigger || (document.activeElement as HTMLElement);
|
||||
|
||||
// Update URL for deep linking
|
||||
if (updateUrl) {
|
||||
updateHash(filePath);
|
||||
}
|
||||
|
||||
// Show modal with loading state
|
||||
title.textContent = filePath.split("/").pop() || filePath;
|
||||
modal.classList.remove("hidden");
|
||||
|
||||
// Set focus to close button for accessibility
|
||||
setTimeout(() => {
|
||||
closeBtn?.focus();
|
||||
}, 0);
|
||||
|
||||
// Handle collections differently - show as item list
|
||||
if (type === "collection") {
|
||||
await openCollectionModal(
|
||||
filePath,
|
||||
title,
|
||||
modalContent,
|
||||
installDropdown,
|
||||
copyBtn,
|
||||
downloadBtn
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular file modal
|
||||
if (contentEl) {
|
||||
contentEl.textContent = "Loading...";
|
||||
}
|
||||
|
||||
// Show copy/download buttons for regular files
|
||||
if (copyBtn) copyBtn.style.display = "inline-flex";
|
||||
if (downloadBtn) downloadBtn.style.display = "inline-flex";
|
||||
|
||||
// Restore pre/code structure if it was replaced by collection view
|
||||
if (!modalContent.querySelector("pre")) {
|
||||
modalContent.innerHTML = '<pre id="modal-content"><code></code></pre>';
|
||||
}
|
||||
const codeEl = modalContent.querySelector("code");
|
||||
|
||||
// Setup install dropdown
|
||||
const vscodeUrl = getVSCodeInstallUrl(type, filePath, false);
|
||||
const insidersUrl = getVSCodeInstallUrl(type, filePath, true);
|
||||
|
||||
if (vscodeUrl && installDropdown) {
|
||||
installDropdown.style.display = "inline-flex";
|
||||
installDropdown.classList.remove("open");
|
||||
if (installBtnMain) installBtnMain.href = vscodeUrl;
|
||||
if (installVscode) installVscode.href = vscodeUrl;
|
||||
if (installInsiders) installInsiders.href = insidersUrl || "#";
|
||||
} else if (installDropdown) {
|
||||
installDropdown.style.display = "none";
|
||||
}
|
||||
|
||||
// Fetch and display content
|
||||
const fileContent = await fetchFileContent(filePath);
|
||||
currentFileContent = fileContent;
|
||||
|
||||
if (fileContent && codeEl) {
|
||||
codeEl.textContent = fileContent;
|
||||
} else if (codeEl) {
|
||||
codeEl.textContent =
|
||||
"Failed to load file content. Click the button below to view on GitHub.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open collection modal with item list
|
||||
*/
|
||||
async function openCollectionModal(
|
||||
filePath: string,
|
||||
title: HTMLElement,
|
||||
modalContent: HTMLElement,
|
||||
installDropdown: HTMLElement | null,
|
||||
copyBtn: HTMLElement | null,
|
||||
downloadBtn: HTMLElement | null
|
||||
): Promise<void> {
|
||||
// Hide install dropdown and copy/download for collections
|
||||
if (installDropdown) installDropdown.style.display = "none";
|
||||
if (copyBtn) copyBtn.style.display = "none";
|
||||
if (downloadBtn) downloadBtn.style.display = "none";
|
||||
|
||||
// Show loading
|
||||
modalContent.innerHTML =
|
||||
'<div class="collection-loading">Loading collection...</div>';
|
||||
|
||||
// Load collections data if not cached
|
||||
if (!collectionsCache) {
|
||||
collectionsCache = await fetchData<CollectionsData>("collections.json");
|
||||
}
|
||||
|
||||
if (!collectionsCache) {
|
||||
modalContent.innerHTML =
|
||||
'<div class="collection-error">Failed to load collection data.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the collection
|
||||
const collection = collectionsCache.items.find((c) => c.path === filePath);
|
||||
if (!collection) {
|
||||
modalContent.innerHTML =
|
||||
'<div class="collection-error">Collection not found.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Update title
|
||||
title.textContent = collection.name;
|
||||
|
||||
// Render collection view
|
||||
modalContent.innerHTML = `
|
||||
<div class="collection-view">
|
||||
<div class="collection-description">${escapeHtml(
|
||||
collection.description || ""
|
||||
)}</div>
|
||||
${
|
||||
collection.tags && collection.tags.length > 0
|
||||
? `
|
||||
<div class="collection-tags">
|
||||
${collection.tags
|
||||
.map((t) => `<span class="resource-tag">${escapeHtml(t)}</span>`)
|
||||
.join("")}
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
<div class="collection-items-header">
|
||||
<strong>${collection.items.length} items in this collection</strong>
|
||||
</div>
|
||||
<div class="collection-items-list">
|
||||
${collection.items
|
||||
.map(
|
||||
(item) => `
|
||||
<div class="collection-item" data-path="${escapeHtml(
|
||||
item.path
|
||||
)}" data-type="${escapeHtml(item.kind)}">
|
||||
<span class="collection-item-icon">${getResourceIcon(
|
||||
item.kind
|
||||
)}</span>
|
||||
<div class="collection-item-info">
|
||||
<div class="collection-item-name">${escapeHtml(
|
||||
item.path.split("/").pop() || item.path
|
||||
)}</div>
|
||||
${
|
||||
item.usage
|
||||
? `<div class="collection-item-usage">${escapeHtml(
|
||||
item.usage
|
||||
)}</div>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
<span class="collection-item-type">${escapeHtml(item.kind)}</span>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add click handlers to collection items
|
||||
modalContent.querySelectorAll(".collection-item").forEach((el) => {
|
||||
el.addEventListener("click", () => {
|
||||
const path = (el as HTMLElement).dataset.path;
|
||||
const itemType = (el as HTMLElement).dataset.type;
|
||||
if (path && itemType) {
|
||||
openFileModal(path, itemType);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal
|
||||
* @param updateUrl - Whether to update the URL hash (default: true)
|
||||
*/
|
||||
export function closeModal(updateUrl = true): void {
|
||||
const modal = document.getElementById("file-modal");
|
||||
const installDropdown = document.getElementById("install-dropdown");
|
||||
|
||||
if (modal) {
|
||||
modal.classList.add("hidden");
|
||||
}
|
||||
if (installDropdown) {
|
||||
installDropdown.classList.remove("open");
|
||||
}
|
||||
|
||||
// Update URL for deep linking
|
||||
if (updateUrl) {
|
||||
updateHash(null);
|
||||
}
|
||||
|
||||
// Return focus to trigger element
|
||||
if (
|
||||
triggerElement &&
|
||||
triggerElement.isConnected &&
|
||||
typeof triggerElement.focus === "function"
|
||||
) {
|
||||
triggerElement.focus();
|
||||
}
|
||||
|
||||
currentFilePath = null;
|
||||
currentFileContent = null;
|
||||
currentFileType = null;
|
||||
triggerElement = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current file path (for external use)
|
||||
*/
|
||||
export function getCurrentFilePath(): string | null {
|
||||
return currentFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current file content (for external use)
|
||||
*/
|
||||
export function getCurrentFileContent(): string | null {
|
||||
return currentFileContent;
|
||||
}
|
||||
176
website/src/scripts/pages/agents.ts
Normal file
176
website/src/scripts/pages/agents.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* Agents page functionality
|
||||
*/
|
||||
import { createChoices, getChoicesValues, type Choices } from '../choices';
|
||||
import { FuzzySearch, SearchItem } from '../search';
|
||||
import { fetchData, debounce, escapeHtml, getGitHubUrl, getInstallDropdownHtml, setupDropdownCloseHandlers, getActionButtonsHtml, setupActionHandlers } from '../utils';
|
||||
import { setupModal, openFileModal } from '../modal';
|
||||
|
||||
interface Agent extends SearchItem {
|
||||
path: string;
|
||||
model?: string;
|
||||
tools?: string[];
|
||||
hasHandoffs?: boolean;
|
||||
}
|
||||
|
||||
interface AgentsData {
|
||||
items: Agent[];
|
||||
filters: {
|
||||
models: string[];
|
||||
tools: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const resourceType = 'agent';
|
||||
let allItems: Agent[] = [];
|
||||
let search = new FuzzySearch<Agent>();
|
||||
let modelSelect: Choices;
|
||||
let toolSelect: Choices;
|
||||
|
||||
let currentFilters = {
|
||||
models: [] as string[],
|
||||
tools: [] as string[],
|
||||
hasHandoffs: false,
|
||||
};
|
||||
|
||||
function applyFiltersAndRender(): void {
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
const countEl = document.getElementById('results-count');
|
||||
const query = searchInput?.value || '';
|
||||
|
||||
let results = query ? search.search(query) : [...allItems];
|
||||
|
||||
if (currentFilters.models.length > 0) {
|
||||
results = results.filter(item => {
|
||||
if (currentFilters.models.includes('(none)') && !item.model) {
|
||||
return true;
|
||||
}
|
||||
return item.model && currentFilters.models.includes(item.model);
|
||||
});
|
||||
}
|
||||
|
||||
if (currentFilters.tools.length > 0) {
|
||||
results = results.filter(item =>
|
||||
item.tools?.some(tool => currentFilters.tools.includes(tool))
|
||||
);
|
||||
}
|
||||
|
||||
if (currentFilters.hasHandoffs) {
|
||||
results = results.filter(item => item.hasHandoffs);
|
||||
}
|
||||
|
||||
renderItems(results, query);
|
||||
|
||||
const activeFilters: string[] = [];
|
||||
if (currentFilters.models.length > 0) activeFilters.push(`models: ${currentFilters.models.length}`);
|
||||
if (currentFilters.tools.length > 0) activeFilters.push(`tools: ${currentFilters.tools.length}`);
|
||||
if (currentFilters.hasHandoffs) activeFilters.push('has handoffs');
|
||||
|
||||
let countText = `${results.length} of ${allItems.length} agents`;
|
||||
if (activeFilters.length > 0) {
|
||||
countText += ` (filtered by ${activeFilters.join(', ')})`;
|
||||
}
|
||||
if (countEl) countEl.textContent = countText;
|
||||
}
|
||||
|
||||
function renderItems(items: Agent[], query = ''): void {
|
||||
const list = document.getElementById('resource-list');
|
||||
if (!list) return;
|
||||
|
||||
if (items.length === 0) {
|
||||
list.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<h3>No agents found</h3>
|
||||
<p>Try a different search term or adjust filters</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = items.map(item => `
|
||||
<div class="resource-item" data-path="${escapeHtml(item.path)}">
|
||||
<div class="resource-info">
|
||||
<div class="resource-title">${query ? search.highlight(item.title, query) : escapeHtml(item.title)}</div>
|
||||
<div class="resource-description">${escapeHtml(item.description || 'No description')}</div>
|
||||
<div class="resource-meta">
|
||||
${item.model ? `<span class="resource-tag tag-model">${escapeHtml(item.model)}</span>` : ''}
|
||||
${item.tools?.slice(0, 3).map(t => `<span class="resource-tag">${escapeHtml(t)}</span>`).join('') || ''}
|
||||
${item.tools && item.tools.length > 3 ? `<span class="resource-tag">+${item.tools.length - 3} more</span>` : ''}
|
||||
${item.hasHandoffs ? `<span class="resource-tag tag-handoffs">handoffs</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-actions">
|
||||
${getInstallDropdownHtml(resourceType, item.path, true)}
|
||||
${getActionButtonsHtml(item.path, true)}
|
||||
<a href="${getGitHubUrl(item.path)}" class="btn btn-secondary btn-small" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add click handlers
|
||||
list.querySelectorAll('.resource-item').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const path = (el as HTMLElement).dataset.path;
|
||||
if (path) openFileModal(path, resourceType);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function initAgentsPage(): Promise<void> {
|
||||
const list = document.getElementById('resource-list');
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
const handoffsCheckbox = document.getElementById('filter-handoffs') as HTMLInputElement;
|
||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
||||
|
||||
const data = await fetchData<AgentsData>('agents.json');
|
||||
if (!data || !data.items) {
|
||||
if (list) list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
allItems = data.items;
|
||||
search.setItems(allItems);
|
||||
|
||||
// Initialize Choices.js for model filter
|
||||
modelSelect = createChoices('#filter-model', { placeholderValue: 'All Models' });
|
||||
modelSelect.setChoices(data.filters.models.map(m => ({ value: m, label: m })), 'value', 'label', true);
|
||||
document.getElementById('filter-model')?.addEventListener('change', () => {
|
||||
currentFilters.models = getChoicesValues(modelSelect);
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
// Initialize Choices.js for tool filter
|
||||
toolSelect = createChoices('#filter-tool', { placeholderValue: 'All Tools' });
|
||||
toolSelect.setChoices(data.filters.tools.map(t => ({ value: t, label: t })), 'value', 'label', true);
|
||||
document.getElementById('filter-tool')?.addEventListener('change', () => {
|
||||
currentFilters.tools = getChoicesValues(toolSelect);
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
|
||||
searchInput?.addEventListener('input', debounce(() => applyFiltersAndRender(), 200));
|
||||
|
||||
handoffsCheckbox?.addEventListener('change', () => {
|
||||
currentFilters.hasHandoffs = handoffsCheckbox.checked;
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
clearFiltersBtn?.addEventListener('click', () => {
|
||||
currentFilters = { models: [], tools: [], hasHandoffs: false };
|
||||
modelSelect.removeActiveItems();
|
||||
toolSelect.removeActiveItems();
|
||||
if (handoffsCheckbox) handoffsCheckbox.checked = false;
|
||||
if (searchInput) searchInput.value = '';
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
setupModal();
|
||||
setupDropdownCloseHandlers();
|
||||
setupActionHandlers();
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initAgentsPage);
|
||||
143
website/src/scripts/pages/collections.ts
Normal file
143
website/src/scripts/pages/collections.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Collections page functionality
|
||||
*/
|
||||
import { createChoices, getChoicesValues, type Choices } from '../choices';
|
||||
import { FuzzySearch, SearchItem } from '../search';
|
||||
import { fetchData, debounce, escapeHtml, getGitHubUrl } from '../utils';
|
||||
import { setupModal, openFileModal } from '../modal';
|
||||
|
||||
interface Collection extends SearchItem {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
tags?: string[];
|
||||
featured?: boolean;
|
||||
itemCount: number;
|
||||
}
|
||||
|
||||
interface CollectionsData {
|
||||
items: Collection[];
|
||||
filters: {
|
||||
tags: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const resourceType = 'collection';
|
||||
let allItems: Collection[] = [];
|
||||
let search = new FuzzySearch<Collection>();
|
||||
let tagSelect: Choices;
|
||||
let currentFilters = {
|
||||
tags: [] as string[],
|
||||
featured: false
|
||||
};
|
||||
|
||||
function applyFiltersAndRender(): void {
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
const countEl = document.getElementById('results-count');
|
||||
const query = searchInput?.value || '';
|
||||
|
||||
let results = query ? search.search(query) : [...allItems];
|
||||
|
||||
if (currentFilters.tags.length > 0) {
|
||||
results = results.filter(item => item.tags?.some(tag => currentFilters.tags.includes(tag)));
|
||||
}
|
||||
if (currentFilters.featured) {
|
||||
results = results.filter(item => item.featured);
|
||||
}
|
||||
|
||||
renderItems(results, query);
|
||||
const activeFilters: string[] = [];
|
||||
if (currentFilters.tags.length > 0) activeFilters.push(`${currentFilters.tags.length} tag${currentFilters.tags.length > 1 ? 's' : ''}`);
|
||||
if (currentFilters.featured) activeFilters.push('featured');
|
||||
let countText = `${results.length} of ${allItems.length} collections`;
|
||||
if (activeFilters.length > 0) {
|
||||
countText += ` (filtered by ${activeFilters.join(', ')})`;
|
||||
}
|
||||
if (countEl) countEl.textContent = countText;
|
||||
}
|
||||
|
||||
function renderItems(items: Collection[], query = ''): void {
|
||||
const list = document.getElementById('resource-list');
|
||||
if (!list) return;
|
||||
|
||||
if (items.length === 0) {
|
||||
list.innerHTML = '<div class="empty-state"><h3>No collections found</h3><p>Try a different search term or adjust filters</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = items.map(item => `
|
||||
<div class="resource-item" data-path="${escapeHtml(item.path)}">
|
||||
<div class="resource-info">
|
||||
<div class="resource-title">${item.featured ? '⭐ ' : ''}${query ? search.highlight(item.name, query) : escapeHtml(item.name)}</div>
|
||||
<div class="resource-description">${escapeHtml(item.description || 'No description')}</div>
|
||||
<div class="resource-meta">
|
||||
<span class="resource-tag">${item.itemCount} items</span>
|
||||
${item.tags?.slice(0, 4).map(t => `<span class="resource-tag">${escapeHtml(t)}</span>`).join('') || ''}
|
||||
${item.tags && item.tags.length > 4 ? `<span class="resource-tag">+${item.tags.length - 4} more</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-actions">
|
||||
<a href="${getGitHubUrl(item.path)}" class="btn btn-secondary" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add click handlers
|
||||
list.querySelectorAll('.resource-item').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const path = (el as HTMLElement).dataset.path;
|
||||
if (path) openFileModal(path, resourceType);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function initCollectionsPage(): Promise<void> {
|
||||
const list = document.getElementById('resource-list');
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
const featuredCheckbox = document.getElementById('filter-featured') as HTMLInputElement;
|
||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
||||
|
||||
const data = await fetchData<CollectionsData>('collections.json');
|
||||
if (!data || !data.items) {
|
||||
if (list) list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
allItems = data.items;
|
||||
|
||||
// Map collection items to search items
|
||||
const searchItems = allItems.map(item => ({
|
||||
...item,
|
||||
title: item.name,
|
||||
searchText: `${item.name} ${item.description} ${item.tags?.join(' ') || ''}`.toLowerCase()
|
||||
}));
|
||||
search.setItems(searchItems);
|
||||
|
||||
tagSelect = createChoices('#filter-tag', { placeholderValue: 'All Tags' });
|
||||
tagSelect.setChoices(data.filters.tags.map(t => ({ value: t, label: t })), 'value', 'label', true);
|
||||
document.getElementById('filter-tag')?.addEventListener('change', () => {
|
||||
currentFilters.tags = getChoicesValues(tagSelect);
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
searchInput?.addEventListener('input', debounce(() => applyFiltersAndRender(), 200));
|
||||
|
||||
featuredCheckbox?.addEventListener('change', () => {
|
||||
currentFilters.featured = featuredCheckbox.checked;
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
clearFiltersBtn?.addEventListener('click', () => {
|
||||
currentFilters = { tags: [], featured: false };
|
||||
tagSelect.removeActiveItems();
|
||||
if (featuredCheckbox) featuredCheckbox.checked = false;
|
||||
if (searchInput) searchInput.value = '';
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
setupModal();
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initCollectionsPage);
|
||||
135
website/src/scripts/pages/index.ts
Normal file
135
website/src/scripts/pages/index.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Homepage functionality
|
||||
*/
|
||||
import { FuzzySearch, type SearchItem } from '../search';
|
||||
import { fetchData, debounce, escapeHtml, truncate, getResourceIcon } from '../utils';
|
||||
import { setupModal, openFileModal } from '../modal';
|
||||
|
||||
interface Manifest {
|
||||
counts: {
|
||||
agents: number;
|
||||
prompts: number;
|
||||
instructions: number;
|
||||
skills: number;
|
||||
collections: number;
|
||||
tools: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface Collection {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
path: string;
|
||||
tags?: string[];
|
||||
featured?: boolean;
|
||||
itemCount: number;
|
||||
}
|
||||
|
||||
interface CollectionsData {
|
||||
items: Collection[];
|
||||
}
|
||||
|
||||
export async function initHomepage(): Promise<void> {
|
||||
// Load manifest for stats
|
||||
const manifest = await fetchData<Manifest>('manifest.json');
|
||||
if (manifest && manifest.counts) {
|
||||
// Populate counts in cards
|
||||
const countKeys = ['agents', 'prompts', 'instructions', 'skills', 'collections', 'tools'] as const;
|
||||
countKeys.forEach(key => {
|
||||
const countEl = document.querySelector(`.card-count[data-count="${key}"]`);
|
||||
if (countEl && manifest.counts[key] !== undefined) {
|
||||
countEl.textContent = manifest.counts[key].toString();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load search index
|
||||
const searchIndex = await fetchData<SearchItem[]>('search-index.json');
|
||||
if (searchIndex) {
|
||||
const search = new FuzzySearch<SearchItem>();
|
||||
search.setItems(searchIndex);
|
||||
|
||||
const searchInput = document.getElementById('global-search') as HTMLInputElement;
|
||||
const resultsDiv = document.getElementById('search-results');
|
||||
|
||||
if (searchInput && resultsDiv) {
|
||||
searchInput.addEventListener('input', debounce(() => {
|
||||
const query = searchInput.value.trim();
|
||||
if (query.length < 2) {
|
||||
resultsDiv.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
const results = search.search(query).slice(0, 10);
|
||||
if (results.length === 0) {
|
||||
resultsDiv.innerHTML = '<div class="search-result-empty">No results found</div>';
|
||||
} else {
|
||||
resultsDiv.innerHTML = results.map(item => `
|
||||
<div class="search-result" data-path="${escapeHtml(item.path)}" data-type="${escapeHtml(item.type)}">
|
||||
<span class="search-result-type">${getResourceIcon(item.type)}</span>
|
||||
<div>
|
||||
<div class="search-result-title">${search.highlight(item.title, query)}</div>
|
||||
<div class="search-result-description">${truncate(item.description, 60)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add click handlers
|
||||
resultsDiv.querySelectorAll('.search-result').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const path = (el as HTMLElement).dataset.path;
|
||||
const type = (el as HTMLElement).dataset.type;
|
||||
if (path && type) openFileModal(path, type);
|
||||
});
|
||||
});
|
||||
}
|
||||
resultsDiv.classList.remove('hidden');
|
||||
}, 200));
|
||||
|
||||
// Close results when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!searchInput.contains(e.target as Node) && !resultsDiv.contains(e.target as Node)) {
|
||||
resultsDiv.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Load featured collections
|
||||
const collectionsData = await fetchData<CollectionsData>('collections.json');
|
||||
if (collectionsData && collectionsData.items) {
|
||||
const featured = collectionsData.items.filter(c => c.featured).slice(0, 6);
|
||||
const featuredEl = document.getElementById('featured-collections');
|
||||
if (featuredEl) {
|
||||
if (featured.length > 0) {
|
||||
featuredEl.innerHTML = featured.map(c => `
|
||||
<div class="card" data-path="${escapeHtml(c.path)}">
|
||||
<h3>${escapeHtml(c.name)}</h3>
|
||||
<p>${escapeHtml(truncate(c.description, 80))}</p>
|
||||
<div class="resource-meta">
|
||||
<span class="resource-tag">${c.itemCount} items</span>
|
||||
${c.tags?.slice(0, 3).map(t => `<span class="resource-tag">${escapeHtml(t)}</span>`).join('') || ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add click handlers
|
||||
featuredEl.querySelectorAll('.card').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const path = (el as HTMLElement).dataset.path;
|
||||
if (path) openFileModal(path, 'collection');
|
||||
});
|
||||
});
|
||||
} else {
|
||||
featuredEl.innerHTML = '<p style="text-align: center; color: var(--color-text-muted);">No featured collections yet</p>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Setup modal
|
||||
setupModal();
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initHomepage);
|
||||
128
website/src/scripts/pages/instructions.ts
Normal file
128
website/src/scripts/pages/instructions.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Instructions page functionality
|
||||
*/
|
||||
import { createChoices, getChoicesValues, type Choices } from '../choices';
|
||||
import { FuzzySearch, SearchItem } from '../search';
|
||||
import { fetchData, debounce, escapeHtml, getGitHubUrl, getInstallDropdownHtml, setupDropdownCloseHandlers, getActionButtonsHtml, setupActionHandlers } from '../utils';
|
||||
import { setupModal, openFileModal } from '../modal';
|
||||
|
||||
interface Instruction extends SearchItem {
|
||||
path: string;
|
||||
applyTo?: string;
|
||||
extensions?: string[];
|
||||
}
|
||||
|
||||
interface InstructionsData {
|
||||
items: Instruction[];
|
||||
filters: {
|
||||
extensions: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const resourceType = 'instruction';
|
||||
let allItems: Instruction[] = [];
|
||||
let search = new FuzzySearch<Instruction>();
|
||||
let extensionSelect: Choices;
|
||||
let currentFilters = { extensions: [] as string[] };
|
||||
|
||||
function applyFiltersAndRender(): void {
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
const countEl = document.getElementById('results-count');
|
||||
const query = searchInput?.value || '';
|
||||
|
||||
let results = query ? search.search(query) : [...allItems];
|
||||
|
||||
if (currentFilters.extensions.length > 0) {
|
||||
results = results.filter(item => {
|
||||
if (currentFilters.extensions.includes('(none)') && (!item.extensions || item.extensions.length === 0)) {
|
||||
return true;
|
||||
}
|
||||
return item.extensions?.some(ext => currentFilters.extensions.includes(ext));
|
||||
});
|
||||
}
|
||||
|
||||
renderItems(results, query);
|
||||
let countText = `${results.length} of ${allItems.length} instructions`;
|
||||
if (currentFilters.extensions.length > 0) {
|
||||
countText += ` (filtered by ${currentFilters.extensions.length} extension${currentFilters.extensions.length > 1 ? 's' : ''})`;
|
||||
}
|
||||
if (countEl) countEl.textContent = countText;
|
||||
}
|
||||
|
||||
function renderItems(items: Instruction[], query = ''): void {
|
||||
const list = document.getElementById('resource-list');
|
||||
if (!list) return;
|
||||
|
||||
if (items.length === 0) {
|
||||
list.innerHTML = '<div class="empty-state"><h3>No instructions found</h3><p>Try a different search term or adjust filters</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = items.map(item => `
|
||||
<div class="resource-item" data-path="${escapeHtml(item.path)}">
|
||||
<div class="resource-info">
|
||||
<div class="resource-title">${query ? search.highlight(item.title, query) : escapeHtml(item.title)}</div>
|
||||
<div class="resource-description">${escapeHtml(item.description || 'No description')}</div>
|
||||
<div class="resource-meta">
|
||||
${item.applyTo ? `<span class="resource-tag">applies to: ${escapeHtml(item.applyTo)}</span>` : ''}
|
||||
${item.extensions?.slice(0, 4).map(e => `<span class="resource-tag tag-extension">${escapeHtml(e)}</span>`).join('') || ''}
|
||||
${item.extensions && item.extensions.length > 4 ? `<span class="resource-tag">+${item.extensions.length - 4} more</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-actions">
|
||||
${getInstallDropdownHtml('instructions', item.path, true)}
|
||||
${getActionButtonsHtml(item.path, true)}
|
||||
<a href="${getGitHubUrl(item.path)}" class="btn btn-secondary btn-small" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add click handlers
|
||||
list.querySelectorAll('.resource-item').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const path = (el as HTMLElement).dataset.path;
|
||||
if (path) openFileModal(path, resourceType);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function initInstructionsPage(): Promise<void> {
|
||||
const list = document.getElementById('resource-list');
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
||||
|
||||
const data = await fetchData<InstructionsData>('instructions.json');
|
||||
if (!data || !data.items) {
|
||||
if (list) list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
allItems = data.items;
|
||||
search.setItems(allItems);
|
||||
|
||||
extensionSelect = createChoices('#filter-extension', { placeholderValue: 'All Extensions' });
|
||||
extensionSelect.setChoices(data.filters.extensions.map(e => ({ value: e, label: e })), 'value', 'label', true);
|
||||
document.getElementById('filter-extension')?.addEventListener('change', () => {
|
||||
currentFilters.extensions = getChoicesValues(extensionSelect);
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
searchInput?.addEventListener('input', debounce(() => applyFiltersAndRender(), 200));
|
||||
|
||||
clearFiltersBtn?.addEventListener('click', () => {
|
||||
currentFilters = { extensions: [] };
|
||||
extensionSelect.removeActiveItems();
|
||||
if (searchInput) searchInput.value = '';
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
setupModal();
|
||||
setupDropdownCloseHandlers();
|
||||
setupActionHandlers();
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initInstructionsPage);
|
||||
123
website/src/scripts/pages/prompts.ts
Normal file
123
website/src/scripts/pages/prompts.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Prompts page functionality
|
||||
*/
|
||||
import { createChoices, getChoicesValues, type Choices } from '../choices';
|
||||
import { FuzzySearch, SearchItem } from '../search';
|
||||
import { fetchData, debounce, escapeHtml, getGitHubUrl, getInstallDropdownHtml, setupDropdownCloseHandlers, getActionButtonsHtml, setupActionHandlers } from '../utils';
|
||||
import { setupModal, openFileModal } from '../modal';
|
||||
|
||||
interface Prompt extends SearchItem {
|
||||
path: string;
|
||||
tools?: string[];
|
||||
}
|
||||
|
||||
interface PromptsData {
|
||||
items: Prompt[];
|
||||
filters: {
|
||||
tools: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const resourceType = 'prompt';
|
||||
let allItems: Prompt[] = [];
|
||||
let search = new FuzzySearch<Prompt>();
|
||||
let toolSelect: Choices;
|
||||
let currentFilters = { tools: [] as string[] };
|
||||
|
||||
function applyFiltersAndRender(): void {
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
const countEl = document.getElementById('results-count');
|
||||
const query = searchInput?.value || '';
|
||||
|
||||
let results = query ? search.search(query) : [...allItems];
|
||||
|
||||
if (currentFilters.tools.length > 0) {
|
||||
results = results.filter(item =>
|
||||
item.tools?.some(tool => currentFilters.tools.includes(tool))
|
||||
);
|
||||
}
|
||||
|
||||
renderItems(results, query);
|
||||
let countText = `${results.length} of ${allItems.length} prompts`;
|
||||
if (currentFilters.tools.length > 0) {
|
||||
countText += ` (filtered by ${currentFilters.tools.length} tool${currentFilters.tools.length > 1 ? 's' : ''})`;
|
||||
}
|
||||
if (countEl) countEl.textContent = countText;
|
||||
}
|
||||
|
||||
function renderItems(items: Prompt[], query = ''): void {
|
||||
const list = document.getElementById('resource-list');
|
||||
if (!list) return;
|
||||
|
||||
if (items.length === 0) {
|
||||
list.innerHTML = '<div class="empty-state"><h3>No prompts found</h3><p>Try a different search term or adjust filters</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = items.map(item => `
|
||||
<div class="resource-item" data-path="${escapeHtml(item.path)}">
|
||||
<div class="resource-info">
|
||||
<div class="resource-title">${query ? search.highlight(item.title, query) : escapeHtml(item.title)}</div>
|
||||
<div class="resource-description">${escapeHtml(item.description || 'No description')}</div>
|
||||
<div class="resource-meta">
|
||||
${item.tools?.slice(0, 4).map(t => `<span class="resource-tag">${escapeHtml(t)}</span>`).join('') || ''}
|
||||
${item.tools && item.tools.length > 4 ? `<span class="resource-tag">+${item.tools.length - 4} more</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-actions">
|
||||
${getInstallDropdownHtml(resourceType, item.path, true)}
|
||||
${getActionButtonsHtml(item.path, true)}
|
||||
<a href="${getGitHubUrl(item.path)}" class="btn btn-secondary btn-small" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add click handlers
|
||||
list.querySelectorAll('.resource-item').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const path = (el as HTMLElement).dataset.path;
|
||||
if (path) openFileModal(path, resourceType);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function initPromptsPage(): Promise<void> {
|
||||
const list = document.getElementById('resource-list');
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
||||
|
||||
const data = await fetchData<PromptsData>('prompts.json');
|
||||
if (!data || !data.items) {
|
||||
if (list) list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
allItems = data.items;
|
||||
search.setItems(allItems);
|
||||
|
||||
toolSelect = createChoices('#filter-tool', { placeholderValue: 'All Tools' });
|
||||
toolSelect.setChoices(data.filters.tools.map(t => ({ value: t, label: t })), 'value', 'label', true);
|
||||
document.getElementById('filter-tool')?.addEventListener('change', () => {
|
||||
currentFilters.tools = getChoicesValues(toolSelect);
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
searchInput?.addEventListener('input', debounce(() => applyFiltersAndRender(), 200));
|
||||
|
||||
clearFiltersBtn?.addEventListener('click', () => {
|
||||
currentFilters = { tools: [] };
|
||||
toolSelect.removeActiveItems();
|
||||
if (searchInput) searchInput.value = '';
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
setupModal();
|
||||
setupDropdownCloseHandlers();
|
||||
setupActionHandlers();
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initPromptsPage);
|
||||
522
website/src/scripts/pages/samples.ts
Normal file
522
website/src/scripts/pages/samples.ts
Normal file
@@ -0,0 +1,522 @@
|
||||
/**
|
||||
* Samples/Cookbook page functionality
|
||||
*/
|
||||
|
||||
import { FuzzySearch, type SearchableItem } from "../search";
|
||||
import { fetchData, escapeHtml } from "../utils";
|
||||
import { createChoices, getChoicesValues, type Choices } from "../choices";
|
||||
|
||||
// Types
|
||||
interface Language {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
extension: string;
|
||||
}
|
||||
|
||||
interface RecipeVariant {
|
||||
doc: string;
|
||||
example: string | null;
|
||||
}
|
||||
|
||||
interface Recipe {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
variants: Record<string, RecipeVariant>;
|
||||
}
|
||||
|
||||
interface Cookbook {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
path: string;
|
||||
featured: boolean;
|
||||
languages: Language[];
|
||||
recipes: Recipe[];
|
||||
}
|
||||
|
||||
interface SamplesData {
|
||||
cookbooks: Cookbook[];
|
||||
totalRecipes: number;
|
||||
totalCookbooks: number;
|
||||
filters: {
|
||||
languages: string[];
|
||||
tags: string[];
|
||||
};
|
||||
}
|
||||
|
||||
// State
|
||||
let samplesData: SamplesData | null = null;
|
||||
let search: FuzzySearch<SearchableItem> | null = null;
|
||||
let selectedLanguage: string | null = null;
|
||||
let selectedTags: string[] = [];
|
||||
let expandedRecipes: Set<string> = new Set();
|
||||
let tagChoices: Choices | null = null;
|
||||
|
||||
/**
|
||||
* Initialize the samples page
|
||||
*/
|
||||
export async function initSamplesPage(): Promise<void> {
|
||||
try {
|
||||
// Load samples data
|
||||
samplesData = await fetchData<SamplesData>("samples.json");
|
||||
|
||||
if (!samplesData || samplesData.cookbooks.length === 0) {
|
||||
showEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize search with all recipes
|
||||
const allRecipes = samplesData.cookbooks.flatMap((cookbook) =>
|
||||
cookbook.recipes.map(
|
||||
(recipe) =>
|
||||
({
|
||||
...recipe,
|
||||
title: recipe.name,
|
||||
cookbookId: cookbook.id,
|
||||
} as SearchableItem & { cookbookId: string })
|
||||
)
|
||||
);
|
||||
search = new FuzzySearch(allRecipes);
|
||||
|
||||
// Setup UI
|
||||
setupFilters();
|
||||
setupSearch();
|
||||
renderCookbooks();
|
||||
updateResultsCount();
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize samples page:", error);
|
||||
showEmptyState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show empty state when no cookbooks are available
|
||||
*/
|
||||
function showEmptyState(): void {
|
||||
const container = document.getElementById("samples-list");
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<h3>No Samples Available</h3>
|
||||
<p>Check back soon for code samples and recipes.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Hide filters
|
||||
const filtersBar = document.getElementById("filters-bar");
|
||||
if (filtersBar) filtersBar.style.display = "none";
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup language and tag filters
|
||||
*/
|
||||
function setupFilters(): void {
|
||||
if (!samplesData) return;
|
||||
|
||||
// Language filter
|
||||
const languageSelect = document.getElementById(
|
||||
"filter-language"
|
||||
) as HTMLSelectElement;
|
||||
if (languageSelect) {
|
||||
// Get unique languages across all cookbooks
|
||||
const languages = new Map<string, Language>();
|
||||
samplesData.cookbooks.forEach((cookbook) => {
|
||||
cookbook.languages.forEach((lang) => {
|
||||
if (!languages.has(lang.id)) {
|
||||
languages.set(lang.id, lang);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
languageSelect.innerHTML = '<option value="">All Languages</option>';
|
||||
languages.forEach((lang, id) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = id;
|
||||
option.textContent = `${lang.icon} ${lang.name}`;
|
||||
languageSelect.appendChild(option);
|
||||
});
|
||||
|
||||
languageSelect.addEventListener("change", () => {
|
||||
selectedLanguage = languageSelect.value || null;
|
||||
renderCookbooks();
|
||||
updateResultsCount();
|
||||
});
|
||||
}
|
||||
|
||||
// Tag filter (multi-select with Choices.js)
|
||||
const tagSelect = document.getElementById("filter-tag") as HTMLSelectElement;
|
||||
if (tagSelect && samplesData.filters.tags.length > 0) {
|
||||
// Initialize Choices.js
|
||||
tagChoices = createChoices("#filter-tag", { placeholderValue: "All Tags" });
|
||||
tagChoices.setChoices(
|
||||
samplesData.filters.tags.map((tag) => ({ value: tag, label: tag })),
|
||||
"value",
|
||||
"label",
|
||||
true
|
||||
);
|
||||
|
||||
tagSelect.addEventListener("change", () => {
|
||||
selectedTags = getChoicesValues(tagChoices!);
|
||||
renderCookbooks();
|
||||
updateResultsCount();
|
||||
});
|
||||
}
|
||||
|
||||
// Clear filters button
|
||||
const clearBtn = document.getElementById("clear-filters");
|
||||
clearBtn?.addEventListener("click", clearFilters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup search functionality
|
||||
*/
|
||||
function setupSearch(): void {
|
||||
const searchInput = document.getElementById(
|
||||
"search-input"
|
||||
) as HTMLInputElement;
|
||||
if (!searchInput) return;
|
||||
|
||||
let debounceTimer: number;
|
||||
searchInput.addEventListener("input", () => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = window.setTimeout(() => {
|
||||
renderCookbooks();
|
||||
updateResultsCount();
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
function clearFilters(): void {
|
||||
selectedLanguage = null;
|
||||
selectedTags = [];
|
||||
|
||||
const languageSelect = document.getElementById(
|
||||
"filter-language"
|
||||
) as HTMLSelectElement;
|
||||
if (languageSelect) languageSelect.value = "";
|
||||
|
||||
// Clear Choices.js selection
|
||||
if (tagChoices) {
|
||||
tagChoices.removeActiveItems();
|
||||
}
|
||||
|
||||
const searchInput = document.getElementById(
|
||||
"search-input"
|
||||
) as HTMLInputElement;
|
||||
if (searchInput) searchInput.value = "";
|
||||
|
||||
renderCookbooks();
|
||||
updateResultsCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filtered recipes
|
||||
*/
|
||||
function getFilteredRecipes(): {
|
||||
cookbook: Cookbook;
|
||||
recipe: Recipe;
|
||||
highlighted?: string;
|
||||
}[] {
|
||||
if (!samplesData || !search) return [];
|
||||
|
||||
const searchInput = document.getElementById(
|
||||
"search-input"
|
||||
) as HTMLInputElement;
|
||||
const query = searchInput?.value.trim() || "";
|
||||
|
||||
let results: { cookbook: Cookbook; recipe: Recipe; highlighted?: string }[] =
|
||||
[];
|
||||
|
||||
if (query) {
|
||||
// Use fuzzy search - returns SearchableItem[] directly
|
||||
const searchResults = search.search(query);
|
||||
results = searchResults.map((item) => {
|
||||
const recipe = item as SearchableItem & { cookbookId: string };
|
||||
const cookbook = samplesData!.cookbooks.find(
|
||||
(c) => c.id === recipe.cookbookId
|
||||
)!;
|
||||
return {
|
||||
cookbook,
|
||||
recipe: recipe as unknown as Recipe,
|
||||
highlighted: search!.highlight(recipe.title, query),
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// No search query - return all recipes
|
||||
results = samplesData.cookbooks.flatMap((cookbook) =>
|
||||
cookbook.recipes.map((recipe) => ({ cookbook, recipe }))
|
||||
);
|
||||
}
|
||||
|
||||
// Apply language filter
|
||||
if (selectedLanguage) {
|
||||
results = results.filter(
|
||||
({ recipe }) => recipe.variants[selectedLanguage!]
|
||||
);
|
||||
}
|
||||
|
||||
// Apply tag filter
|
||||
if (selectedTags.length > 0) {
|
||||
results = results.filter(({ recipe }) =>
|
||||
selectedTags.some((tag) => recipe.tags.includes(tag))
|
||||
);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render cookbooks and recipes
|
||||
*/
|
||||
function renderCookbooks(): void {
|
||||
const container = document.getElementById("samples-list");
|
||||
if (!container || !samplesData) return;
|
||||
|
||||
const filteredResults = getFilteredRecipes();
|
||||
|
||||
if (filteredResults.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<h3>No Results Found</h3>
|
||||
<p>Try adjusting your search or filters.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by cookbook
|
||||
const byCookbook = new Map<
|
||||
string,
|
||||
{ cookbook: Cookbook; recipes: { recipe: Recipe; highlighted?: string }[] }
|
||||
>();
|
||||
filteredResults.forEach(({ cookbook, recipe, highlighted }) => {
|
||||
if (!byCookbook.has(cookbook.id)) {
|
||||
byCookbook.set(cookbook.id, { cookbook, recipes: [] });
|
||||
}
|
||||
byCookbook.get(cookbook.id)!.recipes.push({ recipe, highlighted });
|
||||
});
|
||||
|
||||
let html = "";
|
||||
byCookbook.forEach(({ cookbook, recipes }) => {
|
||||
html += renderCookbookSection(cookbook, recipes);
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
// Setup event listeners
|
||||
setupRecipeListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a cookbook section
|
||||
*/
|
||||
function renderCookbookSection(
|
||||
cookbook: Cookbook,
|
||||
recipes: { recipe: Recipe; highlighted?: string }[]
|
||||
): string {
|
||||
const languageTabs = cookbook.languages
|
||||
.map(
|
||||
(lang) => `
|
||||
<button class="lang-tab${selectedLanguage === lang.id ? " active" : ""}"
|
||||
data-lang="${lang.id}"
|
||||
title="${lang.name}">
|
||||
${lang.icon}
|
||||
</button>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
const recipeCards = recipes
|
||||
.map(({ recipe, highlighted }) =>
|
||||
renderRecipeCard(cookbook, recipe, highlighted)
|
||||
)
|
||||
.join("");
|
||||
|
||||
return `
|
||||
<div class="cookbook-section" data-cookbook="${cookbook.id}">
|
||||
<div class="cookbook-header">
|
||||
<div class="cookbook-info">
|
||||
<h2>${escapeHtml(cookbook.name)}</h2>
|
||||
<p>${escapeHtml(cookbook.description)}</p>
|
||||
</div>
|
||||
<div class="cookbook-languages">
|
||||
${languageTabs}
|
||||
</div>
|
||||
</div>
|
||||
<div class="recipes-grid">
|
||||
${recipeCards}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a recipe card
|
||||
*/
|
||||
function renderRecipeCard(
|
||||
cookbook: Cookbook,
|
||||
recipe: Recipe,
|
||||
highlightedName?: string
|
||||
): string {
|
||||
const recipeKey = `${cookbook.id}-${recipe.id}`;
|
||||
const isExpanded = expandedRecipes.has(recipeKey);
|
||||
|
||||
// Determine which language to show
|
||||
const displayLang = selectedLanguage || cookbook.languages[0]?.id || "nodejs";
|
||||
const variant = recipe.variants[displayLang];
|
||||
|
||||
const tags = recipe.tags
|
||||
.map((tag) => `<span class="recipe-tag">${escapeHtml(tag)}</span>`)
|
||||
.join("");
|
||||
|
||||
const langIndicators = cookbook.languages
|
||||
.filter((lang) => recipe.variants[lang.id])
|
||||
.map(
|
||||
(lang) =>
|
||||
`<span class="lang-indicator" title="${lang.name}">${lang.icon}</span>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
return `
|
||||
<div class="recipe-card${
|
||||
isExpanded ? " expanded" : ""
|
||||
}" data-recipe="${recipeKey}" data-cookbook="${
|
||||
cookbook.id
|
||||
}" data-recipe-id="${recipe.id}">
|
||||
<div class="recipe-header">
|
||||
<h3>${highlightedName || escapeHtml(recipe.name)}</h3>
|
||||
<div class="recipe-langs">${langIndicators}</div>
|
||||
</div>
|
||||
<p class="recipe-description">${escapeHtml(recipe.description)}</p>
|
||||
<div class="recipe-tags">${tags}</div>
|
||||
<div class="recipe-actions">
|
||||
${
|
||||
variant
|
||||
? `
|
||||
<button class="btn btn-secondary btn-small view-recipe-btn" data-doc="${
|
||||
variant.doc
|
||||
}">
|
||||
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">
|
||||
<path d="M1 2.75A.75.75 0 0 1 1.75 2h12.5a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 2.75zm0 5A.75.75 0 0 1 1.75 7h12.5a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 7.75zM1.75 12h12.5a.75.75 0 0 1 0 1.5H1.75a.75.75 0 0 1 0-1.5z"/>
|
||||
</svg>
|
||||
View Recipe
|
||||
</button>
|
||||
${
|
||||
variant.example
|
||||
? `
|
||||
<button class="btn btn-secondary btn-small view-example-btn" data-example="${variant.example}">
|
||||
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">
|
||||
<path d="M4.72 3.22a.75.75 0 0 1 1.06 0l3.5 3.5a.75.75 0 0 1 0 1.06l-3.5 3.5a.75.75 0 0 1-1.06-1.06L7.69 7.5 4.72 4.28a.75.75 0 0 1 0-1.06zm6.25 1.06L10.22 5l.75.75-2.25 2.25 2.25 2.25-.75.75-.75-.72L11.97 7.5z"/>
|
||||
</svg>
|
||||
View Example
|
||||
</button>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
<a href="https://github.com/github/awesome-copilot/blob/main/${
|
||||
variant.doc
|
||||
}"
|
||||
class="btn btn-secondary btn-small" target="_blank" rel="noopener">
|
||||
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
||||
</svg>
|
||||
GitHub
|
||||
</a>
|
||||
`
|
||||
: '<span class="no-variant">Not available for selected language</span>'
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners for recipe interactions
|
||||
*/
|
||||
function setupRecipeListeners(): void {
|
||||
// View recipe buttons
|
||||
document.querySelectorAll(".view-recipe-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
const docPath = (btn as HTMLElement).dataset.doc;
|
||||
if (docPath) {
|
||||
await showRecipeContent(docPath, "recipe");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// View example buttons
|
||||
document.querySelectorAll(".view-example-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
const examplePath = (btn as HTMLElement).dataset.example;
|
||||
if (examplePath) {
|
||||
await showRecipeContent(examplePath, "example");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Language tab clicks
|
||||
document.querySelectorAll(".lang-tab").forEach((tab) => {
|
||||
tab.addEventListener("click", (e) => {
|
||||
const langId = (tab as HTMLElement).dataset.lang;
|
||||
if (langId) {
|
||||
selectedLanguage = langId;
|
||||
// Update language filter select
|
||||
const languageSelect = document.getElementById(
|
||||
"filter-language"
|
||||
) as HTMLSelectElement;
|
||||
if (languageSelect) languageSelect.value = langId;
|
||||
renderCookbooks();
|
||||
updateResultsCount();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show recipe/example content in modal
|
||||
*/
|
||||
async function showRecipeContent(
|
||||
filePath: string,
|
||||
type: "recipe" | "example"
|
||||
): Promise<void> {
|
||||
// Use existing modal infrastructure
|
||||
const { openFileModal } = await import("../modal");
|
||||
await openFileModal(filePath, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update results count display
|
||||
*/
|
||||
function updateResultsCount(): void {
|
||||
const resultsCount = document.getElementById("results-count");
|
||||
if (!resultsCount || !samplesData) return;
|
||||
|
||||
const filtered = getFilteredRecipes();
|
||||
const total = samplesData.totalRecipes;
|
||||
|
||||
if (filtered.length === total) {
|
||||
resultsCount.textContent = `${total} recipe${total !== 1 ? "s" : ""}`;
|
||||
} else {
|
||||
resultsCount.textContent = `${filtered.length} of ${total} recipe${
|
||||
total !== 1 ? "s" : ""
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
if (typeof document !== "undefined") {
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => initSamplesPage());
|
||||
} else {
|
||||
initSamplesPage();
|
||||
}
|
||||
}
|
||||
288
website/src/scripts/pages/skills.ts
Normal file
288
website/src/scripts/pages/skills.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* Skills page functionality
|
||||
*/
|
||||
import { createChoices, getChoicesValues, type Choices } from "../choices";
|
||||
import { FuzzySearch, SearchItem } from "../search";
|
||||
import {
|
||||
fetchData,
|
||||
debounce,
|
||||
escapeHtml,
|
||||
getGitHubUrl,
|
||||
getRawGitHubUrl,
|
||||
showToast,
|
||||
} from "../utils";
|
||||
import { setupModal, openFileModal } from "../modal";
|
||||
import JSZip from "../jszip";
|
||||
|
||||
interface SkillFile {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface Skill extends SearchItem {
|
||||
id: string;
|
||||
path: string;
|
||||
skillFile: string;
|
||||
category: string;
|
||||
hasAssets: boolean;
|
||||
assetCount: number;
|
||||
files: SkillFile[];
|
||||
}
|
||||
|
||||
interface SkillsData {
|
||||
items: Skill[];
|
||||
filters: {
|
||||
categories: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const resourceType = "skill";
|
||||
let allItems: Skill[] = [];
|
||||
let search = new FuzzySearch<Skill>();
|
||||
let categorySelect: Choices;
|
||||
let currentFilters = {
|
||||
categories: [] as string[],
|
||||
hasAssets: false,
|
||||
};
|
||||
|
||||
function applyFiltersAndRender(): void {
|
||||
const searchInput = document.getElementById(
|
||||
"search-input"
|
||||
) as HTMLInputElement;
|
||||
const countEl = document.getElementById("results-count");
|
||||
const query = searchInput?.value || "";
|
||||
|
||||
let results = query ? search.search(query) : [...allItems];
|
||||
|
||||
if (currentFilters.categories.length > 0) {
|
||||
results = results.filter((item) =>
|
||||
currentFilters.categories.includes(item.category)
|
||||
);
|
||||
}
|
||||
if (currentFilters.hasAssets) {
|
||||
results = results.filter((item) => item.hasAssets);
|
||||
}
|
||||
|
||||
renderItems(results, query);
|
||||
const activeFilters: string[] = [];
|
||||
if (currentFilters.categories.length > 0)
|
||||
activeFilters.push(
|
||||
`${currentFilters.categories.length} categor${
|
||||
currentFilters.categories.length > 1 ? "ies" : "y"
|
||||
}`
|
||||
);
|
||||
if (currentFilters.hasAssets) activeFilters.push("has assets");
|
||||
let countText = `${results.length} of ${allItems.length} skills`;
|
||||
if (activeFilters.length > 0) {
|
||||
countText += ` (filtered by ${activeFilters.join(", ")})`;
|
||||
}
|
||||
if (countEl) countEl.textContent = countText;
|
||||
}
|
||||
|
||||
function renderItems(items: Skill[], query = ""): void {
|
||||
const list = document.getElementById("resource-list");
|
||||
if (!list) return;
|
||||
|
||||
if (items.length === 0) {
|
||||
list.innerHTML =
|
||||
'<div class="empty-state"><h3>No skills found</h3><p>Try a different search term or adjust filters</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = items
|
||||
.map(
|
||||
(item) => `
|
||||
<div class="resource-item" data-path="${escapeHtml(
|
||||
item.skillFile
|
||||
)}" data-skill-id="${escapeHtml(item.id)}">
|
||||
<div class="resource-info">
|
||||
<div class="resource-title">${
|
||||
query ? search.highlight(item.title, query) : escapeHtml(item.title)
|
||||
}</div>
|
||||
<div class="resource-description">${escapeHtml(
|
||||
item.description || "No description"
|
||||
)}</div>
|
||||
<div class="resource-meta">
|
||||
<span class="resource-tag tag-category">${escapeHtml(
|
||||
item.category
|
||||
)}</span>
|
||||
${
|
||||
item.hasAssets
|
||||
? `<span class="resource-tag tag-assets">${
|
||||
item.assetCount
|
||||
} asset${item.assetCount === 1 ? "" : "s"}</span>`
|
||||
: ""
|
||||
}
|
||||
<span class="resource-tag">${item.files.length} file${
|
||||
item.files.length === 1 ? "" : "s"
|
||||
}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-actions">
|
||||
<button class="btn btn-primary download-skill-btn" data-skill-id="${escapeHtml(
|
||||
item.id
|
||||
)}" title="Download as ZIP">
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
||||
<path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"/>
|
||||
<path d="M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06l1.97 1.969Z"/>
|
||||
</svg>
|
||||
Download
|
||||
</button>
|
||||
<a href="${getGitHubUrl(
|
||||
item.path
|
||||
)}" class="btn btn-secondary" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
// Add click handlers for opening modal
|
||||
list.querySelectorAll(".resource-item").forEach((el) => {
|
||||
el.addEventListener("click", (e) => {
|
||||
// Don't trigger modal if clicking download button or github link
|
||||
if ((e.target as HTMLElement).closest(".resource-actions")) return;
|
||||
const path = (el as HTMLElement).dataset.path;
|
||||
if (path) openFileModal(path, resourceType);
|
||||
});
|
||||
});
|
||||
|
||||
// Add download handlers
|
||||
list.querySelectorAll(".download-skill-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const skillId = (btn as HTMLElement).dataset.skillId;
|
||||
if (skillId) downloadSkill(skillId, btn as HTMLButtonElement);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadSkill(
|
||||
skillId: string,
|
||||
btn: HTMLButtonElement
|
||||
): Promise<void> {
|
||||
const skill = allItems.find((item) => item.id === skillId);
|
||||
if (!skill || !skill.files || skill.files.length === 0) {
|
||||
showToast("No files found for this skill.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const originalContent = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML =
|
||||
'<svg class="spinner" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0a8 8 0 1 0 8 8h-1.5A6.5 6.5 0 1 1 8 1.5V0z"/></svg> Preparing...';
|
||||
|
||||
try {
|
||||
const zip = new JSZip();
|
||||
const folder = zip.folder(skill.id);
|
||||
|
||||
const fetchPromises = skill.files.map(async (file) => {
|
||||
const url = getRawGitHubUrl(file.path);
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) return null;
|
||||
const content = await response.text();
|
||||
return { name: file.name, content };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(fetchPromises);
|
||||
let addedFiles = 0;
|
||||
for (const result of results) {
|
||||
if (result && folder) {
|
||||
folder.file(result.name, result.content);
|
||||
addedFiles++;
|
||||
}
|
||||
}
|
||||
|
||||
if (addedFiles === 0) throw new Error("Failed to fetch any files");
|
||||
|
||||
const blob = await zip.generateAsync({ type: "blob" });
|
||||
const downloadUrl = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = downloadUrl;
|
||||
link.download = `${skill.id}.zip`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
|
||||
btn.innerHTML =
|
||||
'<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0z"/></svg> Downloaded!';
|
||||
setTimeout(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalContent;
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Download failed.";
|
||||
showToast(message, "error");
|
||||
btn.innerHTML =
|
||||
'<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.75.75 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.75.75 0 0 1-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 0 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06z"/></svg> Failed';
|
||||
setTimeout(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalContent;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
export async function initSkillsPage(): Promise<void> {
|
||||
const list = document.getElementById("resource-list");
|
||||
const searchInput = document.getElementById(
|
||||
"search-input"
|
||||
) as HTMLInputElement;
|
||||
const hasAssetsCheckbox = document.getElementById(
|
||||
"filter-has-assets"
|
||||
) as HTMLInputElement;
|
||||
const clearFiltersBtn = document.getElementById("clear-filters");
|
||||
|
||||
const data = await fetchData<SkillsData>("skills.json");
|
||||
if (!data || !data.items) {
|
||||
if (list)
|
||||
list.innerHTML =
|
||||
'<div class="empty-state"><h3>Failed to load data</h3></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
allItems = data.items;
|
||||
search.setItems(allItems);
|
||||
|
||||
categorySelect = createChoices("#filter-category", {
|
||||
placeholderValue: "All Categories",
|
||||
});
|
||||
categorySelect.setChoices(
|
||||
data.filters.categories.map((c) => ({ value: c, label: c })),
|
||||
"value",
|
||||
"label",
|
||||
true
|
||||
);
|
||||
document.getElementById("filter-category")?.addEventListener("change", () => {
|
||||
currentFilters.categories = getChoicesValues(categorySelect);
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
searchInput?.addEventListener(
|
||||
"input",
|
||||
debounce(() => applyFiltersAndRender(), 200)
|
||||
);
|
||||
|
||||
hasAssetsCheckbox?.addEventListener("change", () => {
|
||||
currentFilters.hasAssets = hasAssetsCheckbox.checked;
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
clearFiltersBtn?.addEventListener("click", () => {
|
||||
currentFilters = { categories: [], hasAssets: false };
|
||||
categorySelect.removeActiveItems();
|
||||
if (hasAssetsCheckbox) hasAssetsCheckbox.checked = false;
|
||||
if (searchInput) searchInput.value = "";
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
setupModal();
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener("DOMContentLoaded", initSkillsPage);
|
||||
322
website/src/scripts/pages/tools.ts
Normal file
322
website/src/scripts/pages/tools.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
/**
|
||||
* Tools page functionality
|
||||
*/
|
||||
import { FuzzySearch, type SearchableItem } from "../search";
|
||||
import { fetchData, debounce, escapeHtml } from "../utils";
|
||||
|
||||
export interface Tool extends SearchableItem {
|
||||
id: string;
|
||||
name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category: string;
|
||||
featured: boolean;
|
||||
requirements: string[];
|
||||
features: string[];
|
||||
links: {
|
||||
blog?: string;
|
||||
vscode?: string;
|
||||
"vscode-insiders"?: string;
|
||||
"visual-studio"?: string;
|
||||
github?: string;
|
||||
documentation?: string;
|
||||
marketplace?: string;
|
||||
npm?: string;
|
||||
pypi?: string;
|
||||
};
|
||||
configuration?: {
|
||||
type: string;
|
||||
content: string;
|
||||
};
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
interface ToolsData {
|
||||
items: Tool[];
|
||||
filters: {
|
||||
categories: string[];
|
||||
tags: string[];
|
||||
};
|
||||
}
|
||||
|
||||
let allItems: Tool[] = [];
|
||||
let search: FuzzySearch<Tool>;
|
||||
let currentFilters = {
|
||||
categories: [] as string[],
|
||||
query: "",
|
||||
};
|
||||
|
||||
function formatMultilineText(text: string): string {
|
||||
return escapeHtml(text).replace(/\r?\n/g, "<br>");
|
||||
}
|
||||
|
||||
function applyFiltersAndRender(): void {
|
||||
const searchInput = document.getElementById(
|
||||
"search-input"
|
||||
) as HTMLInputElement;
|
||||
const countEl = document.getElementById("results-count");
|
||||
const query = searchInput?.value || "";
|
||||
currentFilters.query = query;
|
||||
|
||||
let results = query ? search.search(query) : [...allItems];
|
||||
|
||||
if (currentFilters.categories.length > 0) {
|
||||
results = results.filter((item) =>
|
||||
currentFilters.categories.includes(item.category)
|
||||
);
|
||||
}
|
||||
|
||||
renderTools(results, query);
|
||||
|
||||
let countText = `${results.length} of ${allItems.length} tools`;
|
||||
if (currentFilters.categories.length > 0) {
|
||||
countText += ` (filtered by ${currentFilters.categories.length} categories)`;
|
||||
}
|
||||
if (countEl) countEl.textContent = countText;
|
||||
}
|
||||
|
||||
function renderTools(tools: Tool[], query = ""): void {
|
||||
const container = document.getElementById("tools-list");
|
||||
if (!container) return;
|
||||
|
||||
if (tools.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<h3>No tools found</h3>
|
||||
<p>Try a different search term or adjust filters</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = tools
|
||||
.map((tool) => {
|
||||
const badges: string[] = [];
|
||||
if (tool.featured) {
|
||||
badges.push('<span class="tool-badge featured">Featured</span>');
|
||||
}
|
||||
badges.push(
|
||||
`<span class="tool-badge category">${escapeHtml(tool.category)}</span>`
|
||||
);
|
||||
|
||||
const features =
|
||||
tool.features && tool.features.length > 0
|
||||
? `<div class="tool-section">
|
||||
<h3>Features</h3>
|
||||
<ul>${tool.features
|
||||
.map((f) => `<li>${escapeHtml(f)}</li>`)
|
||||
.join("")}</ul>
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
const requirements =
|
||||
tool.requirements && tool.requirements.length > 0
|
||||
? `<div class="tool-section">
|
||||
<h3>Requirements</h3>
|
||||
<ul>${tool.requirements
|
||||
.map((r) => `<li>${escapeHtml(r)}</li>`)
|
||||
.join("")}</ul>
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
const tags =
|
||||
tool.tags && tool.tags.length > 0
|
||||
? `<div class="tool-tags">
|
||||
${tool.tags
|
||||
.map((t) => `<span class="tool-tag">${escapeHtml(t)}</span>`)
|
||||
.join("")}
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
const config = tool.configuration
|
||||
? `<div class="tool-config">
|
||||
<h3>Configuration</h3>
|
||||
<div class="tool-config-wrapper">
|
||||
<pre><code>${escapeHtml(tool.configuration.content)}</code></pre>
|
||||
</div>
|
||||
<button class="copy-config-btn" data-config="${encodeURIComponent(
|
||||
tool.configuration.content
|
||||
)}">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/>
|
||||
<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/>
|
||||
</svg>
|
||||
Copy Configuration
|
||||
</button>
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
const actions: string[] = [];
|
||||
if (tool.links.blog) {
|
||||
actions.push(
|
||||
`<a href="${tool.links.blog}" class="btn btn-secondary" target="_blank" rel="noopener">📖 Blog</a>`
|
||||
);
|
||||
}
|
||||
if (tool.links.marketplace) {
|
||||
actions.push(
|
||||
`<a href="${tool.links.marketplace}" class="btn btn-secondary" target="_blank" rel="noopener">🏪 Marketplace</a>`
|
||||
);
|
||||
}
|
||||
if (tool.links.npm) {
|
||||
actions.push(
|
||||
`<a href="${tool.links.npm}" class="btn btn-secondary" target="_blank" rel="noopener">📦 npm</a>`
|
||||
);
|
||||
}
|
||||
if (tool.links.pypi) {
|
||||
actions.push(
|
||||
`<a href="${tool.links.pypi}" class="btn btn-secondary" target="_blank" rel="noopener">🐍 PyPI</a>`
|
||||
);
|
||||
}
|
||||
if (tool.links.documentation) {
|
||||
actions.push(
|
||||
`<a href="${tool.links.documentation}" class="btn btn-secondary" target="_blank" rel="noopener">📚 Docs</a>`
|
||||
);
|
||||
}
|
||||
if (tool.links.github) {
|
||||
actions.push(
|
||||
`<a href="${tool.links.github}" class="btn btn-secondary" target="_blank" rel="noopener">GitHub</a>`
|
||||
);
|
||||
}
|
||||
if (tool.links.vscode) {
|
||||
actions.push(
|
||||
`<a href="${tool.links.vscode}" class="btn btn-primary" target="_blank" rel="noopener">Install in VS Code</a>`
|
||||
);
|
||||
}
|
||||
if (tool.links["vscode-insiders"]) {
|
||||
actions.push(
|
||||
`<a href="${tool.links["vscode-insiders"]}" class="btn btn-outline" target="_blank" rel="noopener">VS Code Insiders</a>`
|
||||
);
|
||||
}
|
||||
if (tool.links["visual-studio"]) {
|
||||
actions.push(
|
||||
`<a href="${tool.links["visual-studio"]}" class="btn btn-outline" target="_blank" rel="noopener">Visual Studio</a>`
|
||||
);
|
||||
}
|
||||
|
||||
const actionsHtml =
|
||||
actions.length > 0
|
||||
? `<div class="tool-actions">${actions.join("")}</div>`
|
||||
: "";
|
||||
|
||||
const titleHtml = query
|
||||
? search.highlight(tool.name, query)
|
||||
: escapeHtml(tool.name);
|
||||
const descriptionHtml = formatMultilineText(tool.description);
|
||||
|
||||
return `
|
||||
<div class="tool-card">
|
||||
<div class="tool-header">
|
||||
<h2>${titleHtml}</h2>
|
||||
<div class="tool-badges">
|
||||
${badges.join("")}
|
||||
</div>
|
||||
</div>
|
||||
<p class="tool-description">${descriptionHtml}</p>
|
||||
${features}
|
||||
${requirements}
|
||||
${config}
|
||||
${tags}
|
||||
${actionsHtml}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
setupCopyConfigHandlers();
|
||||
}
|
||||
|
||||
function setupCopyConfigHandlers(): void {
|
||||
document.querySelectorAll(".copy-config-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
const button = e.currentTarget as HTMLButtonElement;
|
||||
const config = decodeURIComponent(button.dataset.config || "");
|
||||
try {
|
||||
await navigator.clipboard.writeText(config);
|
||||
button.classList.add("copied");
|
||||
const originalHtml = button.innerHTML;
|
||||
button.innerHTML = `
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/>
|
||||
</svg>
|
||||
Copied!
|
||||
`;
|
||||
setTimeout(() => {
|
||||
button.classList.remove("copied");
|
||||
button.innerHTML = originalHtml;
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy:", err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function initToolsPage(): Promise<void> {
|
||||
const container = document.getElementById("tools-list");
|
||||
const searchInput = document.getElementById(
|
||||
"search-input"
|
||||
) as HTMLInputElement;
|
||||
const categoryFilter = document.getElementById(
|
||||
"filter-category"
|
||||
) as HTMLSelectElement;
|
||||
const clearFiltersBtn = document.getElementById("clear-filters");
|
||||
|
||||
if (container) {
|
||||
container.innerHTML = '<div class="loading">Loading tools...</div>';
|
||||
}
|
||||
|
||||
const data = await fetchData<ToolsData>("tools.json");
|
||||
if (!data || !data.items) {
|
||||
if (container)
|
||||
container.innerHTML =
|
||||
'<div class="empty-state"><h3>Failed to load tools</h3></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Map items to include title for FuzzySearch
|
||||
allItems = data.items.map((item) => ({
|
||||
...item,
|
||||
title: item.name, // FuzzySearch uses title
|
||||
}));
|
||||
|
||||
search = new FuzzySearch<Tool>();
|
||||
search.setItems(allItems);
|
||||
|
||||
// Populate category filter
|
||||
if (categoryFilter && data.filters.categories) {
|
||||
categoryFilter.innerHTML =
|
||||
'<option value="">All Categories</option>' +
|
||||
data.filters.categories
|
||||
.map(
|
||||
(c) => `<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
categoryFilter.addEventListener("change", () => {
|
||||
currentFilters.categories = categoryFilter.value
|
||||
? [categoryFilter.value]
|
||||
: [];
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
}
|
||||
|
||||
// Search input handler
|
||||
searchInput?.addEventListener(
|
||||
"input",
|
||||
debounce(() => applyFiltersAndRender(), 200)
|
||||
);
|
||||
|
||||
// Clear filters
|
||||
clearFiltersBtn?.addEventListener("click", () => {
|
||||
currentFilters = { categories: [], query: "" };
|
||||
if (categoryFilter) categoryFilter.value = "";
|
||||
if (searchInput) searchInput.value = "";
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener("DOMContentLoaded", initToolsPage);
|
||||
184
website/src/scripts/search.ts
Normal file
184
website/src/scripts/search.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Fuzzy search implementation for the Awesome Copilot website
|
||||
* Simple substring matching on title and description with scoring
|
||||
*/
|
||||
|
||||
import { escapeHtml, fetchData } from "./utils";
|
||||
|
||||
export interface SearchItem {
|
||||
title: string;
|
||||
description?: string;
|
||||
searchText?: string;
|
||||
path: string;
|
||||
type: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface SearchableItem {
|
||||
title: string;
|
||||
description?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface SearchOptions {
|
||||
fields?: string[];
|
||||
limit?: number;
|
||||
minScore?: number;
|
||||
}
|
||||
|
||||
export class FuzzySearch<T extends SearchableItem = SearchItem> {
|
||||
private items: T[] = [];
|
||||
|
||||
constructor(items: T[] = []) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the items to search
|
||||
*/
|
||||
setItems(items: T[]): void {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search items with fuzzy matching
|
||||
*/
|
||||
search(query: string, options: SearchOptions = {}): T[] {
|
||||
const {
|
||||
fields = ["title", "description", "searchText"],
|
||||
limit = 50,
|
||||
minScore = 0,
|
||||
} = options;
|
||||
|
||||
if (!query || query.trim().length === 0) {
|
||||
return this.items.slice(0, limit);
|
||||
}
|
||||
|
||||
const normalizedQuery = query.toLowerCase().trim();
|
||||
const queryWords = normalizedQuery.split(/\s+/);
|
||||
const results: Array<{ item: T; score: number }> = [];
|
||||
|
||||
for (const item of this.items) {
|
||||
const score = this.calculateScore(item, queryWords, fields);
|
||||
if (score > minScore) {
|
||||
results.push({ item, score });
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by score descending
|
||||
results.sort((a, b) => b.score - a.score);
|
||||
|
||||
return results.slice(0, limit).map((r) => r.item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate match score for an item
|
||||
*/
|
||||
private calculateScore(
|
||||
item: T,
|
||||
queryWords: string[],
|
||||
fields: string[]
|
||||
): number {
|
||||
let totalScore = 0;
|
||||
|
||||
for (const word of queryWords) {
|
||||
let wordScore = 0;
|
||||
|
||||
for (const field of fields) {
|
||||
const value = (item as Record<string, unknown>)[field];
|
||||
if (!value) continue;
|
||||
|
||||
const normalizedValue = String(value).toLowerCase();
|
||||
|
||||
// Exact match in title gets highest score
|
||||
if (field === "title" && normalizedValue === word) {
|
||||
wordScore = Math.max(wordScore, 100);
|
||||
}
|
||||
// Title starts with word
|
||||
else if (field === "title" && normalizedValue.startsWith(word)) {
|
||||
wordScore = Math.max(wordScore, 80);
|
||||
}
|
||||
// Title contains word
|
||||
else if (field === "title" && normalizedValue.includes(word)) {
|
||||
wordScore = Math.max(wordScore, 60);
|
||||
}
|
||||
// Description contains word
|
||||
else if (field === "description" && normalizedValue.includes(word)) {
|
||||
wordScore = Math.max(wordScore, 30);
|
||||
}
|
||||
// searchText (includes tags, tools, etc) contains word
|
||||
else if (field === "searchText" && normalizedValue.includes(word)) {
|
||||
wordScore = Math.max(wordScore, 20);
|
||||
}
|
||||
}
|
||||
|
||||
totalScore += wordScore;
|
||||
}
|
||||
|
||||
// Bonus for matching all words
|
||||
const matchesAllWords = queryWords.every((word) =>
|
||||
fields.some((field) => {
|
||||
const value = (item as Record<string, unknown>)[field];
|
||||
return value && String(value).toLowerCase().includes(word);
|
||||
})
|
||||
);
|
||||
|
||||
if (matchesAllWords && queryWords.length > 1) {
|
||||
totalScore *= 1.5;
|
||||
}
|
||||
|
||||
return totalScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight matching text in a string
|
||||
*/
|
||||
highlight(text: string, query: string): string {
|
||||
if (!query || !text) return escapeHtml(text || "");
|
||||
|
||||
const normalizedQuery = query.toLowerCase().trim();
|
||||
const words = normalizedQuery.split(/\s+/);
|
||||
let result = escapeHtml(text);
|
||||
|
||||
for (const word of words) {
|
||||
if (word.length < 2) continue;
|
||||
const regex = new RegExp(
|
||||
`(${word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`,
|
||||
"gi"
|
||||
);
|
||||
const parts = result.split(/(<[^>]+>)/g);
|
||||
let inMark = false;
|
||||
result = parts
|
||||
.map((part) => {
|
||||
if (part.startsWith("<")) {
|
||||
if (part.toLowerCase() === "<mark>") inMark = true;
|
||||
if (part.toLowerCase() === "</mark>") inMark = false;
|
||||
return part;
|
||||
}
|
||||
|
||||
if (inMark) {
|
||||
return part;
|
||||
}
|
||||
|
||||
return part.replace(regex, "<mark>$1</mark>");
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Global search instance (uses SearchItem for the global search index)
|
||||
export const globalSearch = new FuzzySearch<SearchItem>();
|
||||
|
||||
/**
|
||||
* Initialize global search with search index
|
||||
*/
|
||||
export async function initGlobalSearch(): Promise<FuzzySearch<SearchItem>> {
|
||||
const searchIndex = await fetchData<SearchItem[]>("search-index.json");
|
||||
if (searchIndex) {
|
||||
globalSearch.setItems(searchIndex);
|
||||
}
|
||||
return globalSearch;
|
||||
}
|
||||
65
website/src/scripts/theme.ts
Normal file
65
website/src/scripts/theme.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Theme management for the Awesome Copilot website
|
||||
* Supports light/dark mode with user preference storage
|
||||
*/
|
||||
|
||||
const THEME_KEY = 'theme';
|
||||
|
||||
/**
|
||||
* Get the current theme preference
|
||||
*/
|
||||
function getThemePreference(): 'light' | 'dark' {
|
||||
const stored = localStorage.getItem(THEME_KEY);
|
||||
if (stored === 'light' || stored === 'dark') {
|
||||
return stored;
|
||||
}
|
||||
// Check system preference
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) {
|
||||
return 'light';
|
||||
}
|
||||
return 'dark';
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply theme to the document
|
||||
*/
|
||||
function applyTheme(theme: 'light' | 'dark'): void {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
|
||||
const initialTheme = getThemePreference();
|
||||
applyTheme(initialTheme);
|
||||
|
||||
/**
|
||||
* Toggle between light and dark theme
|
||||
*/
|
||||
export function toggleTheme(): void {
|
||||
const current = document.documentElement.getAttribute('data-theme') as 'light' | 'dark';
|
||||
const newTheme = current === 'light' ? 'dark' : 'light';
|
||||
applyTheme(newTheme);
|
||||
localStorage.setItem(THEME_KEY, newTheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize theme toggle button
|
||||
*/
|
||||
export function initThemeToggle(): void {
|
||||
const toggleBtn = document.getElementById('theme-toggle');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener('click', toggleTheme);
|
||||
}
|
||||
|
||||
// Listen for system theme changes
|
||||
if (window.matchMedia) {
|
||||
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', (e) => {
|
||||
// Only auto-switch if user hasn't set a preference
|
||||
const stored = localStorage.getItem(THEME_KEY);
|
||||
if (!stored) {
|
||||
applyTheme(e.matches ? 'light' : 'dark');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initThemeToggle);
|
||||
431
website/src/scripts/utils.ts
Normal file
431
website/src/scripts/utils.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
/**
|
||||
* Utility functions for the Awesome Copilot website
|
||||
*/
|
||||
|
||||
const REPO_BASE_URL =
|
||||
"https://raw.githubusercontent.com/github/awesome-copilot/main";
|
||||
const REPO_GITHUB_URL = "https://github.com/github/awesome-copilot/blob/main";
|
||||
|
||||
// VS Code install URL configurations
|
||||
const VSCODE_INSTALL_CONFIG: Record<
|
||||
string,
|
||||
{ baseUrl: string; scheme: string }
|
||||
> = {
|
||||
instructions: {
|
||||
baseUrl: "https://aka.ms/awesome-copilot/install/instructions",
|
||||
scheme: "chat-instructions",
|
||||
},
|
||||
prompt: {
|
||||
baseUrl: "https://aka.ms/awesome-copilot/install/prompt",
|
||||
scheme: "chat-prompt",
|
||||
},
|
||||
agent: {
|
||||
baseUrl: "https://aka.ms/awesome-copilot/install/agent",
|
||||
scheme: "chat-agent",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the base path for the site
|
||||
*/
|
||||
export function getBasePath(): string {
|
||||
// In Astro, import.meta.env.BASE_URL is available at build time
|
||||
// At runtime, we use a data attribute on the body
|
||||
if (typeof document !== "undefined") {
|
||||
return document.body.dataset.basePath || "/";
|
||||
}
|
||||
return "/";
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch JSON data from the data directory
|
||||
*/
|
||||
export async function fetchData<T = unknown>(
|
||||
filename: string
|
||||
): Promise<T | null> {
|
||||
try {
|
||||
const basePath = getBasePath();
|
||||
const response = await fetch(`${basePath}data/${filename}`);
|
||||
if (!response.ok) throw new Error(`Failed to fetch ${filename}`);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`Error fetching ${filename}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch raw file content from GitHub
|
||||
*/
|
||||
export async function fetchFileContent(
|
||||
filePath: string
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const response = await fetch(`${REPO_BASE_URL}/${filePath}`);
|
||||
if (!response.ok) throw new Error(`Failed to fetch ${filePath}`);
|
||||
return await response.text();
|
||||
} catch (error) {
|
||||
console.error(`Error fetching file content:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy text to clipboard
|
||||
*/
|
||||
export async function copyToClipboard(text: string): Promise<boolean> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
} catch {
|
||||
// Deprecated fallback for older browsers that lack the async clipboard API.
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.opacity = "0";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
const success = document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate VS Code install URL
|
||||
* @param type - Resource type (agent, prompt, instructions)
|
||||
* @param filePath - Path to the file
|
||||
* @param insiders - Whether to use VS Code Insiders
|
||||
*/
|
||||
export function getVSCodeInstallUrl(
|
||||
type: string,
|
||||
filePath: string,
|
||||
insiders = false
|
||||
): string | null {
|
||||
const config = VSCODE_INSTALL_CONFIG[type];
|
||||
if (!config) return null;
|
||||
|
||||
const rawUrl = `${REPO_BASE_URL}/${filePath}`;
|
||||
const vscodeScheme = insiders ? "vscode-insiders" : "vscode";
|
||||
const innerUrl = `${vscodeScheme}:${
|
||||
config.scheme
|
||||
}/install?url=${encodeURIComponent(rawUrl)}`;
|
||||
|
||||
return `${config.baseUrl}?url=${encodeURIComponent(innerUrl)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get GitHub URL for a file
|
||||
*/
|
||||
export function getGitHubUrl(filePath: string): string {
|
||||
return `${REPO_GITHUB_URL}/${filePath}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raw GitHub URL for a file (for fetching content)
|
||||
*/
|
||||
export function getRawGitHubUrl(filePath: string): string {
|
||||
return `${REPO_BASE_URL}/${filePath}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file from its path
|
||||
*/
|
||||
export async function downloadFile(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${REPO_BASE_URL}/${filePath}`);
|
||||
if (!response.ok) throw new Error("Failed to fetch file");
|
||||
|
||||
const content = await response.text();
|
||||
const filename = filePath.split("/").pop() || "file.md";
|
||||
|
||||
const blob = new Blob([content], { type: "text/markdown" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Download failed:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Share/copy link to clipboard (deep link to current page with file hash)
|
||||
*/
|
||||
export async function shareFile(filePath: string): Promise<boolean> {
|
||||
const deepLinkUrl = `${window.location.origin}${
|
||||
window.location.pathname
|
||||
}#file=${encodeURIComponent(filePath)}`;
|
||||
return copyToClipboard(deepLinkUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a toast notification
|
||||
*/
|
||||
export function showToast(
|
||||
message: string,
|
||||
type: "success" | "error" = "success"
|
||||
): void {
|
||||
const existing = document.querySelector(".toast");
|
||||
if (existing) existing.remove();
|
||||
|
||||
const toast = document.createElement("div");
|
||||
toast.className = `toast ${type}`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce function for search input
|
||||
*/
|
||||
export function debounce<T extends (...args: unknown[]) => void>(
|
||||
func: T,
|
||||
wait: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
return function executedFunction(...args: Parameters<T>) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
*/
|
||||
export function escapeHtml(text: string): string {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate text with ellipsis
|
||||
*/
|
||||
export function truncate(text: string | undefined, maxLength: number): string {
|
||||
if (!text || text.length <= maxLength) return text || "";
|
||||
return text.slice(0, maxLength).trim() + "...";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resource type from file path
|
||||
*/
|
||||
export function getResourceType(filePath: string): string {
|
||||
if (filePath.endsWith(".agent.md")) return "agent";
|
||||
if (filePath.endsWith(".prompt.md")) return "prompt";
|
||||
if (filePath.endsWith(".instructions.md")) return "instruction";
|
||||
if (filePath.includes("/skills/") && filePath.endsWith("SKILL.md"))
|
||||
return "skill";
|
||||
if (filePath.endsWith(".collection.yml")) return "collection";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a resource type for display
|
||||
*/
|
||||
export function formatResourceType(type: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
agent: "🤖 Agent",
|
||||
prompt: "🎯 Prompt",
|
||||
instruction: "📋 Instruction",
|
||||
skill: "⚡ Skill",
|
||||
collection: "📦 Collection",
|
||||
};
|
||||
return labels[type] || type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get icon for resource type
|
||||
*/
|
||||
export function getResourceIcon(type: string): string {
|
||||
const icons: Record<string, string> = {
|
||||
agent: "🤖",
|
||||
prompt: "🎯",
|
||||
instruction: "📋",
|
||||
skill: "⚡",
|
||||
collection: "📦",
|
||||
};
|
||||
return icons[type] || "📄";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HTML for install dropdown button
|
||||
*/
|
||||
export function getInstallDropdownHtml(
|
||||
type: string,
|
||||
filePath: string,
|
||||
small = false
|
||||
): string {
|
||||
const vscodeUrl = getVSCodeInstallUrl(type, filePath, false);
|
||||
const insidersUrl = getVSCodeInstallUrl(type, filePath, true);
|
||||
|
||||
if (!vscodeUrl) return "";
|
||||
|
||||
const sizeClass = small ? "install-dropdown-small" : "";
|
||||
const uniqueId = `install-${filePath.replace(/[^a-zA-Z0-9]/g, "-")}`;
|
||||
|
||||
return `
|
||||
<div class="install-dropdown ${sizeClass}" id="${uniqueId}" data-install-scope="list">
|
||||
<a href="${vscodeUrl}" class="btn btn-primary ${
|
||||
small ? "btn-small" : ""
|
||||
} install-btn-main" target="_blank" rel="noopener">
|
||||
Install
|
||||
</a>
|
||||
<button type="button" class="btn btn-primary ${
|
||||
small ? "btn-small" : ""
|
||||
} install-btn-toggle" aria-label="Install options" aria-expanded="false">
|
||||
<svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor">
|
||||
<path d="M4.427 7.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 7H4.604a.25.25 0 00-.177.427z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="install-dropdown-menu">
|
||||
<a href="${vscodeUrl}" target="_blank" rel="noopener">
|
||||
VS Code
|
||||
</a>
|
||||
<a href="${insidersUrl}" target="_blank" rel="noopener">
|
||||
VS Code Insiders
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup dropdown close handlers for dynamically created dropdowns
|
||||
*/
|
||||
export function setupDropdownCloseHandlers(): void {
|
||||
if (dropdownHandlersReady) return;
|
||||
dropdownHandlersReady = true;
|
||||
|
||||
document.addEventListener(
|
||||
"click",
|
||||
(e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const dropdown = target.closest(
|
||||
'.install-dropdown[data-install-scope="list"]'
|
||||
);
|
||||
const toggle = target.closest(
|
||||
".install-btn-toggle"
|
||||
) as HTMLButtonElement | null;
|
||||
const menuLink = target.closest(
|
||||
".install-dropdown-menu a"
|
||||
) as HTMLAnchorElement | null;
|
||||
|
||||
if (dropdown) {
|
||||
e.stopPropagation();
|
||||
|
||||
if (toggle) {
|
||||
e.preventDefault();
|
||||
const isOpen = dropdown.classList.toggle("open");
|
||||
toggle.setAttribute("aria-expanded", String(isOpen));
|
||||
return;
|
||||
}
|
||||
|
||||
if (menuLink) {
|
||||
dropdown.classList.remove("open");
|
||||
const toggleBtn = dropdown.querySelector<HTMLButtonElement>(
|
||||
".install-btn-toggle"
|
||||
);
|
||||
toggleBtn?.setAttribute("aria-expanded", "false");
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
document
|
||||
.querySelectorAll('.install-dropdown[data-install-scope="list"].open')
|
||||
.forEach((openDropdown) => {
|
||||
openDropdown.classList.remove("open");
|
||||
const toggleBtn = openDropdown.querySelector<HTMLButtonElement>(
|
||||
".install-btn-toggle"
|
||||
);
|
||||
toggleBtn?.setAttribute("aria-expanded", "false");
|
||||
});
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HTML for action buttons (download, share) in list view
|
||||
*/
|
||||
export function getActionButtonsHtml(filePath: string, small = false): string {
|
||||
const btnClass = small ? "btn-small" : "";
|
||||
const iconSize = small ? 14 : 16;
|
||||
|
||||
return `
|
||||
<button class="btn btn-secondary ${btnClass} action-download" data-path="${escapeHtml(
|
||||
filePath
|
||||
)}" title="Download file">
|
||||
<svg viewBox="0 0 16 16" width="${iconSize}" height="${iconSize}" fill="currentColor">
|
||||
<path d="M7.47 10.78a.75.75 0 0 0 1.06 0l3.75-3.75a.75.75 0 0 0-1.06-1.06L8.75 8.44V1.75a.75.75 0 0 0-1.5 0v6.69L4.78 5.97a.75.75 0 0 0-1.06 1.06l3.75 3.75ZM3.75 13a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-secondary ${btnClass} action-share" data-path="${escapeHtml(
|
||||
filePath
|
||||
)}" title="Copy link">
|
||||
<svg viewBox="0 0 16 16" width="${iconSize}" height="${iconSize}" fill="currentColor">
|
||||
<path d="M7.775 3.275a.75.75 0 0 0 1.06 1.06l1.25-1.25a2 2 0 1 1 2.83 2.83l-2.5 2.5a2 2 0 0 1-2.83 0 .75.75 0 0 0-1.06 1.06 3.5 3.5 0 0 0 4.95 0l2.5-2.5a3.5 3.5 0 0 0-4.95-4.95l-1.25 1.25zm-.025 5.45a.75.75 0 0 0-1.06-1.06l-1.25 1.25a2 2 0 1 1-2.83-2.83l2.5-2.5a2 2 0 0 1 2.83 0 .75.75 0 0 0 1.06-1.06 3.5 3.5 0 0 0-4.95 0l-2.5 2.5a3.5 3.5 0 0 0 4.95 4.95l1.25-1.25z"/>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup global action handlers for download and share buttons
|
||||
*/
|
||||
export function setupActionHandlers(): void {
|
||||
if (actionHandlersReady) return;
|
||||
actionHandlersReady = true;
|
||||
|
||||
document.addEventListener(
|
||||
"click",
|
||||
async (e) => {
|
||||
const target = (e.target as HTMLElement).closest(
|
||||
".action-download, .action-share"
|
||||
) as HTMLElement | null;
|
||||
if (!target) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const path = target.dataset.path;
|
||||
if (!path) return;
|
||||
|
||||
if (target.classList.contains("action-download")) {
|
||||
const success = await downloadFile(path);
|
||||
showToast(
|
||||
success ? "Download started!" : "Download failed",
|
||||
success ? "success" : "error"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const success = await shareFile(path);
|
||||
showToast(
|
||||
success ? "Link copied!" : "Failed to copy link",
|
||||
success ? "success" : "error"
|
||||
);
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
let dropdownHandlersReady = false;
|
||||
let actionHandlersReady = false;
|
||||
Reference in New Issue
Block a user