Add multi-select filters, light/dark theme, and skill ZIP downloads

- Add multi-select dropdown component for all filter fields
- Implement light/dark theme toggle with system preference detection
- Add client-side ZIP download for skills using JSZip
- Include file lists in skills metadata for download feature
- Add title tooltips to multi-select options for long values
- Update all pages with consistent theme toggle in header
This commit is contained in:
Aaron Powell
2026-01-28 14:59:19 +11:00
parent f8829be835
commit 875219812e
20 changed files with 12575 additions and 8382 deletions
+221 -26
View File
@@ -80,17 +80,34 @@ function generateAgentsData() {
.readdirSync(AGENTS_DIR) .readdirSync(AGENTS_DIR)
.filter((f) => f.endsWith(".agent.md")); .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) { for (const file of files) {
const filePath = path.join(AGENTS_DIR, file); const filePath = path.join(AGENTS_DIR, file);
const frontmatter = parseFrontmatter(filePath); const frontmatter = parseFrontmatter(filePath);
const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/"); 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({ agents.push({
id: file.replace(".agent.md", ""), id: file.replace(".agent.md", ""),
title: extractTitle(filePath, frontmatter), title: extractTitle(filePath, frontmatter),
description: frontmatter?.description || "", description: frontmatter?.description || "",
model: frontmatter?.model || null, model: model,
tools: frontmatter?.tools || [], tools: tools,
hasHandoffs: handoffs.length > 0,
handoffs: handoffs.map((h) => ({
label: h.label || "",
agent: h.agent || "",
})),
mcpServers: frontmatter?.["mcp-servers"] mcpServers: frontmatter?.["mcp-servers"]
? Object.keys(frontmatter["mcp-servers"]) ? Object.keys(frontmatter["mcp-servers"])
: [], : [],
@@ -99,7 +116,16 @@ function generateAgentsData() {
}); });
} }
return agents.sort((a, b) => a.title.localeCompare(b.title)); // 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(),
},
};
} }
/** /**
@@ -111,24 +137,73 @@ function generatePromptsData() {
.readdirSync(PROMPTS_DIR) .readdirSync(PROMPTS_DIR)
.filter((f) => f.endsWith(".prompt.md")); .filter((f) => f.endsWith(".prompt.md"));
// Track all unique tools for filters
const allTools = new Set();
for (const file of files) { for (const file of files) {
const filePath = path.join(PROMPTS_DIR, file); const filePath = path.join(PROMPTS_DIR, file);
const frontmatter = parseFrontmatter(filePath); const frontmatter = parseFrontmatter(filePath);
const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/"); const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/");
const tools = frontmatter?.tools || [];
tools.forEach((t) => allTools.add(t));
prompts.push({ prompts.push({
id: file.replace(".prompt.md", ""), id: file.replace(".prompt.md", ""),
title: extractTitle(filePath, frontmatter), title: extractTitle(filePath, frontmatter),
description: frontmatter?.description || "", description: frontmatter?.description || "",
agent: frontmatter?.agent || null, agent: frontmatter?.agent || null,
model: frontmatter?.model || null, model: frontmatter?.model || null,
tools: frontmatter?.tools || [], tools: tools,
path: relativePath, path: relativePath,
filename: file, filename: file,
}); });
} }
return prompts.sort((a, b) => a.title.localeCompare(b.title)); 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;
} }
/** /**
@@ -140,22 +215,75 @@ function generateInstructionsData() {
.readdirSync(INSTRUCTIONS_DIR) .readdirSync(INSTRUCTIONS_DIR)
.filter((f) => f.endsWith(".instructions.md")); .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) { for (const file of files) {
const filePath = path.join(INSTRUCTIONS_DIR, file); const filePath = path.join(INSTRUCTIONS_DIR, file);
const frontmatter = parseFrontmatter(filePath); const frontmatter = parseFrontmatter(filePath);
const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/"); 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({ instructions.push({
id: file.replace(".instructions.md", ""), id: file.replace(".instructions.md", ""),
title: extractTitle(filePath, frontmatter), title: extractTitle(filePath, frontmatter),
description: frontmatter?.description || "", description: frontmatter?.description || "",
applyTo: frontmatter?.applyTo || null, applyTo: applyToRaw,
applyToPatterns: applyToPatterns,
extensions: [...new Set(extensions)],
path: relativePath, path: relativePath,
filename: file, filename: file,
}); });
} }
return instructions.sort((a, b) => a.title.localeCompare(b.title)); 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';
} }
/** /**
@@ -165,19 +293,26 @@ function generateSkillsData() {
const skills = []; const skills = [];
if (!fs.existsSync(SKILLS_DIR)) { if (!fs.existsSync(SKILLS_DIR)) {
return skills; return { items: [], filters: { categories: [], hasAssets: ['Yes', 'No'] } };
} }
const folders = fs const folders = fs
.readdirSync(SKILLS_DIR) .readdirSync(SKILLS_DIR)
.filter((f) => fs.statSync(path.join(SKILLS_DIR, f)).isDirectory()); .filter((f) => fs.statSync(path.join(SKILLS_DIR, f)).isDirectory());
const allCategories = new Set();
for (const folder of folders) { for (const folder of folders) {
const skillPath = path.join(SKILLS_DIR, folder); const skillPath = path.join(SKILLS_DIR, folder);
const metadata = parseSkillMetadata(skillPath); const metadata = parseSkillMetadata(skillPath);
if (metadata) { if (metadata) {
const relativePath = path.relative(ROOT_FOLDER, skillPath).replace(/\\/g, "/"); 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({ skills.push({
id: folder, id: folder,
@@ -188,13 +323,55 @@ function generateSkillsData() {
.join(" "), .join(" "),
description: metadata.description, description: metadata.description,
assets: metadata.assets, assets: metadata.assets,
hasAssets: metadata.assets.length > 0,
assetCount: metadata.assets.length,
category: category,
path: relativePath, path: relativePath,
skillFile: `${relativePath}/SKILL.md`, skillFile: `${relativePath}/SKILL.md`,
files: files,
}); });
} }
} }
return skills.sort((a, b) => a.title.localeCompare(b.title)); 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;
} }
/** /**
@@ -211,17 +388,23 @@ function generateCollectionsData() {
.readdirSync(COLLECTIONS_DIR) .readdirSync(COLLECTIONS_DIR)
.filter((f) => f.endsWith(".collection.yml")); .filter((f) => f.endsWith(".collection.yml"));
// Track all unique tags
const allTags = new Set();
for (const file of files) { for (const file of files) {
const filePath = path.join(COLLECTIONS_DIR, file); const filePath = path.join(COLLECTIONS_DIR, file);
const data = parseCollectionYaml(filePath); const data = parseCollectionYaml(filePath);
const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/"); const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/");
if (data) { if (data) {
const tags = data.tags || [];
tags.forEach((t) => allTags.add(t));
collections.push({ collections.push({
id: file.replace(".collection.yml", ""), id: file.replace(".collection.yml", ""),
name: data.name || file.replace(".collection.yml", ""), name: data.name || file.replace(".collection.yml", ""),
description: data.description || "", description: data.description || "",
tags: data.tags || [], tags: tags,
featured: data.featured || false, featured: data.featured || false,
items: (data.items || []).map((item) => ({ items: (data.items || []).map((item) => ({
path: item.path, path: item.path,
@@ -235,11 +418,18 @@ function generateCollectionsData() {
} }
// Sort with featured first, then alphabetically // Sort with featured first, then alphabetically
return collections.sort((a, b) => { const sortedCollections = collections.sort((a, b) => {
if (a.featured && !b.featured) return -1; if (a.featured && !b.featured) return -1;
if (!a.featured && b.featured) return 1; if (!a.featured && b.featured) return 1;
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
}); });
return {
items: sortedCollections,
filters: {
tags: Array.from(allTags).sort(),
},
};
} }
/** /**
@@ -316,20 +506,25 @@ async function main() {
ensureDataDir(); ensureDataDir();
// Generate all data // Generate all data
const agents = generateAgentsData(); const agentsData = generateAgentsData();
console.log(`✓ Generated ${agents.length} agents`); const agents = agentsData.items;
console.log(`✓ Generated ${agents.length} agents (${agentsData.filters.models.length} models, ${agentsData.filters.tools.length} tools)`);
const prompts = generatePromptsData(); const promptsData = generatePromptsData();
console.log(`✓ Generated ${prompts.length} prompts`); const prompts = promptsData.items;
console.log(`✓ Generated ${prompts.length} prompts (${promptsData.filters.tools.length} tools)`);
const instructions = generateInstructionsData(); const instructionsData = generateInstructionsData();
console.log(`✓ Generated ${instructions.length} instructions`); const instructions = instructionsData.items;
console.log(`✓ Generated ${instructions.length} instructions (${instructionsData.filters.extensions.length} extensions)`);
const skills = generateSkillsData(); const skillsData = generateSkillsData();
console.log(`✓ Generated ${skills.length} skills`); const skills = skillsData.items;
console.log(`✓ Generated ${skills.length} skills (${skillsData.filters.categories.length} categories)`);
const collections = generateCollectionsData(); const collectionsData = generateCollectionsData();
console.log(`✓ Generated ${collections.length} collections`); const collections = collectionsData.items;
console.log(`✓ Generated ${collections.length} collections (${collectionsData.filters.tags.length} tags)`);
const searchIndex = generateSearchIndex(agents, prompts, instructions, skills, collections); const searchIndex = generateSearchIndex(agents, prompts, instructions, skills, collections);
console.log(`✓ Generated search index with ${searchIndex.length} items`); console.log(`✓ Generated search index with ${searchIndex.length} items`);
@@ -337,27 +532,27 @@ async function main() {
// Write JSON files // Write JSON files
fs.writeFileSync( fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "agents.json"), path.join(WEBSITE_DATA_DIR, "agents.json"),
JSON.stringify(agents, null, 2) JSON.stringify(agentsData, null, 2)
); );
fs.writeFileSync( fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "prompts.json"), path.join(WEBSITE_DATA_DIR, "prompts.json"),
JSON.stringify(prompts, null, 2) JSON.stringify(promptsData, null, 2)
); );
fs.writeFileSync( fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "instructions.json"), path.join(WEBSITE_DATA_DIR, "instructions.json"),
JSON.stringify(instructions, null, 2) JSON.stringify(instructionsData, null, 2)
); );
fs.writeFileSync( fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "skills.json"), path.join(WEBSITE_DATA_DIR, "skills.json"),
JSON.stringify(skills, null, 2) JSON.stringify(skillsData, null, 2)
); );
fs.writeFileSync( fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "collections.json"), path.join(WEBSITE_DATA_DIR, "collections.json"),
JSON.stringify(collections, null, 2) JSON.stringify(collectionsData, null, 2)
); );
fs.writeFileSync( fs.writeFileSync(
+407 -2
View File
@@ -1,5 +1,6 @@
/* CSS Variables and Base Styles */ /* CSS Variables and Base Styles */
:root { :root {
/* Dark theme (default) */
--color-bg: #0d1117; --color-bg: #0d1117;
--color-bg-secondary: #161b22; --color-bg-secondary: #161b22;
--color-bg-tertiary: #21262d; --color-bg-tertiary: #21262d;
@@ -25,9 +26,26 @@
--header-height: 64px; --header-height: 64px;
} }
/* Light mode support */ /* Light theme */
[data-theme="light"] {
--color-bg: #ffffff;
--color-bg-secondary: #f6f8fa;
--color-bg-tertiary: #f0f3f6;
--color-border: #d0d7de;
--color-text: #24292f;
--color-text-muted: #57606a;
--color-text-emphasis: #1f2328;
--color-link: #0969da;
--color-link-hover: #0550ae;
--color-card-bg: #ffffff;
--color-card-hover: #f6f8fa;
--shadow: 0 1px 3px rgba(0,0,0,0.08), 0 8px 24px rgba(0,0,0,0.08);
--shadow-lg: 0 8px 24px rgba(0,0,0,0.15);
}
/* Auto theme based on system preference */
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
:root { :root:not([data-theme="dark"]) {
--color-bg: #ffffff; --color-bg: #ffffff;
--color-bg-secondary: #f6f8fa; --color-bg-secondary: #f6f8fa;
--color-bg-tertiary: #f0f3f6; --color-bg-tertiary: #f0f3f6;
@@ -39,6 +57,8 @@
--color-link-hover: #0550ae; --color-link-hover: #0550ae;
--color-card-bg: #ffffff; --color-card-bg: #ffffff;
--color-card-hover: #f6f8fa; --color-card-hover: #f6f8fa;
--shadow: 0 1px 3px rgba(0,0,0,0.08), 0 8px 24px rgba(0,0,0,0.08);
--shadow-lg: 0 8px 24px rgba(0,0,0,0.15);
} }
} }
@@ -140,6 +160,69 @@ a:hover {
color: var(--color-text-emphasis); color: var(--color-text-emphasis);
} }
/* Theme Toggle */
.header-actions {
display: flex;
align-items: center;
gap: 16px;
}
.theme-toggle {
background: none;
border: none;
cursor: pointer;
padding: 8px;
border-radius: var(--border-radius);
color: var(--color-text);
display: flex;
align-items: center;
justify-content: center;
transition: all var(--transition);
}
.theme-toggle:hover {
background-color: var(--color-bg-tertiary);
color: var(--color-text-emphasis);
}
.theme-toggle svg {
width: 20px;
height: 20px;
}
.theme-toggle .icon-sun,
.theme-toggle .icon-moon {
display: none;
}
/* Show sun icon in dark mode (click to switch to light) */
:root:not([data-theme="light"]) .theme-toggle .icon-sun {
display: block;
}
:root:not([data-theme="light"]) .theme-toggle .icon-moon {
display: none;
}
/* Show moon icon in light mode (click to switch to dark) */
[data-theme="light"] .theme-toggle .icon-sun {
display: none;
}
[data-theme="light"] .theme-toggle .icon-moon {
display: block;
}
/* Handle auto mode with prefers-color-scheme */
@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]):not([data-theme="light"]) .theme-toggle .icon-sun {
display: none;
}
:root:not([data-theme="dark"]):not([data-theme="light"]) .theme-toggle .icon-moon {
display: block;
}
}
/* Hero Section */ /* Hero Section */
.hero { .hero {
background: linear-gradient(180deg, var(--color-bg-secondary) 0%, var(--color-bg) 100%); background: linear-gradient(180deg, var(--color-bg-secondary) 0%, var(--color-bg) 100%);
@@ -452,6 +535,26 @@ a:hover {
color: var(--color-text); color: var(--color-text);
} }
/* Spinner animation */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.spinner {
animation: spin 1s linear infinite;
}
/* Button states */
.btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
/* Modal */ /* Modal */
.modal { .modal {
position: fixed; position: fixed;
@@ -570,6 +673,308 @@ a:hover {
border-color: var(--color-link); border-color: var(--color-link);
} }
/* Filters Bar */
.filters-bar {
display: flex;
gap: 16px;
margin-bottom: 20px;
flex-wrap: wrap;
align-items: center;
padding: 16px;
background-color: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
}
.filter-group label {
font-size: 13px;
color: var(--color-text-muted);
white-space: nowrap;
}
.filter-group select {
padding: 6px 12px;
font-size: 13px;
background-color: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
color: var(--color-text);
min-width: 150px;
cursor: pointer;
}
.filter-group select:focus {
outline: none;
border-color: var(--color-link);
}
.checkbox-label {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
user-select: none;
}
.checkbox-label input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
}
/* Multi-Select Component */
.multi-select {
position: relative;
min-width: 180px;
}
.multi-select-trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
padding: 6px 12px;
font-size: 13px;
background-color: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
color: var(--color-text);
cursor: pointer;
text-align: left;
transition: all var(--transition);
}
.multi-select-trigger:hover {
border-color: var(--color-text-muted);
}
.multi-select.is-open .multi-select-trigger {
border-color: var(--color-link);
}
.multi-select-display {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--color-text-muted);
}
.multi-select-display.has-value {
color: var(--color-text);
}
.multi-select-arrow {
flex-shrink: 0;
transition: transform var(--transition);
color: var(--color-text-muted);
}
.multi-select.is-open .multi-select-arrow {
transform: rotate(180deg);
}
.multi-select-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 4px;
background-color: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
box-shadow: var(--shadow-lg);
z-index: 100;
display: none;
flex-direction: column;
max-height: 320px;
}
.multi-select.is-open .multi-select-dropdown {
display: flex;
}
.multi-select-search-wrapper {
padding: 8px;
border-bottom: 1px solid var(--color-border);
}
.multi-select-search {
width: 100%;
padding: 8px 10px;
font-size: 13px;
background-color: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
color: var(--color-text);
}
.multi-select-search:focus {
outline: none;
border-color: var(--color-link);
}
.multi-select-options {
flex: 1;
overflow-y: auto;
padding: 4px 0;
}
.multi-select-option {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: pointer;
transition: background-color var(--transition);
}
.multi-select-option:hover {
background-color: var(--color-bg-tertiary);
}
.multi-select-option input[type="checkbox"] {
display: none;
}
.multi-select-checkbox {
width: 16px;
height: 16px;
border: 1px solid var(--color-border);
border-radius: 3px;
background-color: var(--color-bg);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all var(--transition);
}
.multi-select-option input[type="checkbox"]:checked + .multi-select-checkbox {
background-color: var(--color-link);
border-color: var(--color-link);
}
.multi-select-option input[type="checkbox"]:checked + .multi-select-checkbox::after {
content: '';
width: 10px;
height: 10px;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z'/%3E%3C/svg%3E");
background-size: contain;
}
.multi-select-label {
flex: 1;
font-size: 13px;
color: var(--color-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.multi-select-empty {
padding: 16px;
text-align: center;
color: var(--color-text-muted);
font-size: 13px;
}
.multi-select-actions {
display: flex;
gap: 8px;
padding: 8px;
border-top: 1px solid var(--color-border);
}
.multi-select-actions button {
flex: 1;
padding: 6px 12px;
font-size: 12px;
border-radius: var(--border-radius);
cursor: pointer;
transition: all var(--transition);
}
.multi-select-clear {
background-color: transparent;
border: 1px solid var(--color-border);
color: var(--color-text-muted);
}
.multi-select-clear:hover {
background-color: var(--color-bg-tertiary);
color: var(--color-text);
}
.multi-select-done {
background-color: var(--color-link);
border: 1px solid var(--color-link);
color: white;
}
.multi-select-done:hover {
background-color: var(--color-link-hover);
border-color: var(--color-link-hover);
}
/* Tag variants */
.tag-model {
background-color: rgba(88, 166, 255, 0.15);
color: var(--color-link);
}
.tag-none {
background-color: var(--color-bg-tertiary);
color: var(--color-text-muted);
font-style: italic;
}
.tag-handoffs {
background-color: rgba(210, 153, 34, 0.15);
color: var(--color-warning);
}
.tag-extension {
background-color: rgba(35, 134, 54, 0.15);
color: var(--color-success);
}
.tag-category {
background-color: rgba(130, 80, 223, 0.15);
color: #a371f7;
}
.tag-assets {
background-color: rgba(88, 166, 255, 0.15);
color: var(--color-link);
}
.tag-collection {
background-color: rgba(210, 153, 34, 0.15);
color: var(--color-warning);
}
.tag-featured {
background-color: rgba(210, 153, 34, 0.2);
color: var(--color-warning);
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.results-count { .results-count {
font-size: 14px; font-size: 14px;
color: var(--color-text-muted); color: var(--color-text-muted);
+3218 -2750
View File
File diff suppressed because it is too large Load Diff
+2119 -1969
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"generated": "2026-01-28T02:42:05.621Z", "generated": "2026-01-28T03:53:29.513Z",
"counts": { "counts": {
"agents": 140, "agents": 140,
"prompts": 134, "prompts": 134,
+1991 -1910
View File
File diff suppressed because it is too large Load Diff
+780 -297
View File
File diff suppressed because it is too large Load Diff
+16 -5
View File
@@ -7,6 +7,7 @@
<meta name="description" content="Community-contributed instructions, prompts, agents, and skills for GitHub Copilot"> <meta name="description" content="Community-contributed instructions, prompts, agents, and skills for GitHub Copilot">
<link rel="stylesheet" href="css/styles.css"> <link rel="stylesheet" href="css/styles.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🤖</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🤖</text></svg>">
<script src="js/theme.js"></script>
</head> </head>
<body> <body>
<header class="site-header"> <header class="site-header">
@@ -25,11 +26,21 @@
<a href="pages/tools.html">Tools</a> <a href="pages/tools.html">Tools</a>
<a href="pages/samples.html">Samples</a> <a href="pages/samples.html">Samples</a>
</nav> </nav>
<a href="https://github.com/github/awesome-copilot" class="github-link" target="_blank" rel="noopener"> <div class="header-actions">
<svg viewBox="0 0 16 16" width="24" height="24" fill="currentColor"> <button id="theme-toggle" class="theme-toggle" title="Toggle theme">
<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 class="icon-sun" viewBox="0 0 16 16" fill="currentColor">
</svg> <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"/>
</a> </svg>
<svg class="icon-moon" viewBox="0 0 16 16" fill="currentColor">
<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"/>
</svg>
</button>
<a href="https://github.com/github/awesome-copilot" class="github-link" target="_blank" rel="noopener">
<svg viewBox="0 0 16 16" width="24" height="24" fill="currentColor">
<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>
</div> </div>
</header> </header>
+13
View File
File diff suppressed because one or more lines are too long
+209
View File
@@ -0,0 +1,209 @@
/**
* Multi-select dropdown component
* Creates a dropdown with checkboxes for multiple selections
*/
class MultiSelect {
constructor(container, options = {}) {
this.container = typeof container === 'string' ? document.querySelector(container) : container;
this.options = {
placeholder: options.placeholder || 'Select...',
searchable: options.searchable !== false,
onChange: options.onChange || (() => {}),
maxDisplay: options.maxDisplay || 2,
};
this.items = [];
this.selected = new Set();
this.isOpen = false;
this.searchQuery = '';
this.render();
this.setupEventListeners();
}
render() {
this.container.classList.add('multi-select');
this.container.innerHTML = `
<button type="button" class="multi-select-trigger" aria-haspopup="listbox" aria-expanded="false">
<span class="multi-select-display">${this.options.placeholder}</span>
<svg class="multi-select-arrow" viewBox="0 0 16 16" width="16" height="16" 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="multi-select-dropdown" role="listbox" aria-multiselectable="true">
${this.options.searchable ? `
<div class="multi-select-search-wrapper">
<input type="text" class="multi-select-search" placeholder="Search..." autocomplete="off">
</div>
` : ''}
<div class="multi-select-options"></div>
<div class="multi-select-actions">
<button type="button" class="multi-select-clear">Clear</button>
<button type="button" class="multi-select-done">Done</button>
</div>
</div>
`;
this.trigger = this.container.querySelector('.multi-select-trigger');
this.display = this.container.querySelector('.multi-select-display');
this.dropdown = this.container.querySelector('.multi-select-dropdown');
this.optionsContainer = this.container.querySelector('.multi-select-options');
this.searchInput = this.container.querySelector('.multi-select-search');
this.clearBtn = this.container.querySelector('.multi-select-clear');
this.doneBtn = this.container.querySelector('.multi-select-done');
}
setupEventListeners() {
// Toggle dropdown
this.trigger.addEventListener('click', (e) => {
e.stopPropagation();
this.toggle();
});
// Search
if (this.searchInput) {
this.searchInput.addEventListener('input', () => {
this.searchQuery = this.searchInput.value.toLowerCase();
this.renderOptions();
});
this.searchInput.addEventListener('click', (e) => e.stopPropagation());
}
// Clear selection
this.clearBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.clearSelection();
});
// Done button
this.doneBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.close();
});
// Close on outside click
document.addEventListener('click', (e) => {
if (!this.container.contains(e.target)) {
this.close();
}
});
// Keyboard navigation
this.container.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.close();
}
});
}
setItems(items) {
this.items = items.map(item => {
if (typeof item === 'string') {
return { value: item, label: item };
}
return item;
});
this.renderOptions();
}
renderOptions() {
const filteredItems = this.items.filter(item => {
if (!this.searchQuery) return true;
return item.label.toLowerCase().includes(this.searchQuery);
});
if (filteredItems.length === 0) {
this.optionsContainer.innerHTML = '<div class="multi-select-empty">No options found</div>';
return;
}
this.optionsContainer.innerHTML = filteredItems.map(item => `
<label class="multi-select-option" data-value="${this.escapeHtml(item.value)}" title="${this.escapeHtml(item.label)}">
<input type="checkbox" ${this.selected.has(item.value) ? 'checked' : ''}>
<span class="multi-select-checkbox"></span>
<span class="multi-select-label">${this.escapeHtml(item.label)}</span>
</label>
`).join('');
// Add change listeners to checkboxes
this.optionsContainer.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const value = e.target.closest('.multi-select-option').dataset.value;
if (e.target.checked) {
this.selected.add(value);
} else {
this.selected.delete(value);
}
this.updateDisplay();
this.options.onChange(this.getSelected());
});
});
}
updateDisplay() {
const selected = this.getSelected();
if (selected.length === 0) {
this.display.textContent = this.options.placeholder;
this.display.classList.remove('has-value');
} else if (selected.length <= this.options.maxDisplay) {
this.display.textContent = selected.join(', ');
this.display.classList.add('has-value');
} else {
this.display.textContent = `${selected.length} selected`;
this.display.classList.add('has-value');
}
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
open() {
this.isOpen = true;
this.container.classList.add('is-open');
this.trigger.setAttribute('aria-expanded', 'true');
if (this.searchInput) {
this.searchInput.value = '';
this.searchQuery = '';
this.renderOptions();
setTimeout(() => this.searchInput.focus(), 10);
}
}
close() {
this.isOpen = false;
this.container.classList.remove('is-open');
this.trigger.setAttribute('aria-expanded', 'false');
}
getSelected() {
return Array.from(this.selected);
}
setSelected(values) {
this.selected = new Set(values);
this.renderOptions();
this.updateDisplay();
}
clearSelection() {
this.selected.clear();
this.renderOptions();
this.updateDisplay();
this.options.onChange(this.getSelected());
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = MultiSelect;
}
+74
View File
@@ -0,0 +1,74 @@
/**
* Theme management for the Awesome Copilot website
* Supports light/dark mode with user preference storage
*/
const THEME_KEY = 'awesome-copilot-theme';
/**
* Get the current theme preference
* Priority: localStorage > system preference > dark (default)
*/
function getThemePreference() {
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) {
if (theme === 'light') {
document.documentElement.setAttribute('data-theme', 'light');
} else {
document.documentElement.setAttribute('data-theme', 'dark');
}
}
/**
* Toggle between light and dark theme
*/
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme');
const newTheme = current === 'light' ? 'dark' : 'light';
applyTheme(newTheme);
localStorage.setItem(THEME_KEY, newTheme);
}
/**
* Initialize theme on page load
*/
function initTheme() {
// Apply theme immediately to prevent flash
const theme = getThemePreference();
applyTheme(theme);
// 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');
}
});
}
}
// Initialize theme immediately (before DOM ready to prevent flash)
initTheme();
// Setup toggle button after DOM ready
document.addEventListener('DOMContentLoaded', () => {
const toggleBtn = document.getElementById('theme-toggle');
if (toggleBtn) {
toggleBtn.addEventListener('click', toggleTheme);
}
});
+7
View File
@@ -78,6 +78,13 @@ function getGitHubUrl(filePath) {
return `${REPO_GITHUB_URL}/${filePath}`; return `${REPO_GITHUB_URL}/${filePath}`;
} }
/**
* Get raw GitHub URL for a file (for fetching content)
*/
function getRawGitHubUrl(filePath) {
return `${REPO_BASE_URL}/${filePath}`;
}
/** /**
* Show a toast notification * Show a toast notification
*/ */
+139 -22
View File
@@ -7,6 +7,7 @@
<meta name="description" content="Custom agents for specialized GitHub Copilot experiences"> <meta name="description" content="Custom agents for specialized GitHub Copilot experiences">
<link rel="stylesheet" href="../css/styles.css"> <link rel="stylesheet" href="../css/styles.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🤖</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🤖</text></svg>">
<script src="../js/theme.js"></script>
</head> </head>
<body> <body>
<header class="site-header"> <header class="site-header">
@@ -25,11 +26,21 @@
<a href="tools.html">Tools</a> <a href="tools.html">Tools</a>
<a href="samples.html">Samples</a> <a href="samples.html">Samples</a>
</nav> </nav>
<a href="https://github.com/github/awesome-copilot" class="github-link" target="_blank" rel="noopener"> <div class="header-actions">
<svg viewBox="0 0 16 16" width="24" height="24" fill="currentColor"> <button id="theme-toggle" class="theme-toggle" title="Toggle theme">
<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 class="icon-sun" viewBox="0 0 16 16" fill="currentColor">
</svg> <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"/>
</a> </svg>
<svg class="icon-moon" viewBox="0 0 16 16" fill="currentColor">
<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"/>
</svg>
</button>
<a href="https://github.com/github/awesome-copilot" class="github-link" target="_blank" rel="noopener">
<svg viewBox="0 0 16 16" width="24" height="24" fill="currentColor">
<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>
</div> </div>
</header> </header>
@@ -47,6 +58,26 @@
<div class="search-bar"> <div class="search-bar">
<input type="text" id="search-input" placeholder="Search agents..." autocomplete="off"> <input type="text" id="search-input" placeholder="Search agents..." autocomplete="off">
</div> </div>
<!-- Filters -->
<div class="filters-bar" id="filters-bar">
<div class="filter-group">
<label>Model:</label>
<div id="filter-model" class="multi-select-container"></div>
</div>
<div class="filter-group">
<label>Tool:</label>
<div id="filter-tool" class="multi-select-container"></div>
</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"></div> <div class="results-count" id="results-count"></div>
<div class="resource-list" id="resource-list"> <div class="resource-list" id="resource-list">
<div class="loading">Loading agents...</div> <div class="loading">Loading agents...</div>
@@ -100,45 +131,131 @@
<script src="../js/utils.js"></script> <script src="../js/utils.js"></script>
<script src="../js/search.js"></script> <script src="../js/search.js"></script>
<script src="../js/multi-select.js"></script>
<script src="../js/app.js"></script> <script src="../js/app.js"></script>
<script> <script>
// Page-specific initialization // Page-specific initialization
const resourceType = 'agent'; const resourceType = 'agent';
const dataFile = 'agents.json'; const dataFile = 'agents.json';
let allItems = []; let allItems = [];
let filters = { models: [], tools: [] };
let search = new FuzzySearch(); let search = new FuzzySearch();
let modelSelect, toolSelect;
// Current filter state
let currentFilters = {
models: [],
tools: [],
hasHandoffs: false,
};
async function initPage() { async function initPage() {
const list = document.getElementById('resource-list'); const list = document.getElementById('resource-list');
const countEl = document.getElementById('results-count'); const countEl = document.getElementById('results-count');
const searchInput = document.getElementById('search-input'); const searchInput = document.getElementById('search-input');
const handoffsCheckbox = document.getElementById('filter-handoffs');
const clearFiltersBtn = document.getElementById('clear-filters');
// Load data // Load data
const data = await fetchData(dataFile); const data = await fetchData(dataFile);
if (!data) { if (!data || !data.items) {
list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>'; list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
return; return;
} }
allItems = data; allItems = data.items;
filters = data.filters;
search.setItems(allItems); search.setItems(allItems);
// Initialize multi-select filters
modelSelect = new MultiSelect('#filter-model', {
placeholder: 'All Models',
onChange: (selected) => {
currentFilters.models = selected;
applyFiltersAndRender();
}
});
modelSelect.setItems(filters.models);
toolSelect = new MultiSelect('#filter-tool', {
placeholder: 'All Tools',
onChange: (selected) => {
currentFilters.tools = selected;
applyFiltersAndRender();
}
});
toolSelect.setItems(filters.tools);
// Render all items // Render all items
renderItems(allItems); applyFiltersAndRender();
countEl.textContent = `${allItems.length} agents`;
// Setup search // Setup search
searchInput.addEventListener('input', debounce((e) => { searchInput.addEventListener('input', debounce(() => applyFiltersAndRender(), 200));
const query = e.target.value;
const results = query ? search.search(query) : allItems; // Setup filter listeners
renderItems(results, query); handoffsCheckbox.addEventListener('change', () => {
countEl.textContent = `${results.length} of ${allItems.length} agents`; currentFilters.hasHandoffs = handoffsCheckbox.checked;
}, 200)); applyFiltersAndRender();
});
clearFiltersBtn.addEventListener('click', () => {
currentFilters = { models: [], tools: [], hasHandoffs: false };
modelSelect.clearSelection();
toolSelect.clearSelection();
handoffsCheckbox.checked = false;
searchInput.value = '';
applyFiltersAndRender();
});
// Setup modal // Setup modal
setupModal(); setupModal();
} }
function applyFiltersAndRender() {
const searchInput = document.getElementById('search-input');
const countEl = document.getElementById('results-count');
const query = searchInput.value;
// Start with all items or search results
let results = query ? search.search(query) : [...allItems];
// Apply model filter (OR logic - match any selected model)
if (currentFilters.models.length > 0) {
results = results.filter(item => {
if (currentFilters.models.includes('(none)') && !item.model) {
return true;
}
return currentFilters.models.includes(item.model);
});
}
// Apply tool filter (OR logic - match any selected tool)
if (currentFilters.tools.length > 0) {
results = results.filter(item =>
item.tools?.some(tool => currentFilters.tools.includes(tool))
);
}
// Apply handoffs filter
if (currentFilters.hasHandoffs) {
results = results.filter(item => item.hasHandoffs);
}
renderItems(results, query);
// Update count with filter info
const activeFilters = [];
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(', ')})`;
}
countEl.textContent = countText;
}
function renderItems(items, query = '') { function renderItems(items, query = '') {
const list = document.getElementById('resource-list'); const list = document.getElementById('resource-list');
@@ -146,7 +263,7 @@
list.innerHTML = ` list.innerHTML = `
<div class="empty-state"> <div class="empty-state">
<h3>No agents found</h3> <h3>No agents found</h3>
<p>Try a different search term</p> <p>Try a different search term or adjust filters</p>
</div> </div>
`; `;
return; return;
@@ -157,12 +274,12 @@
<div class="resource-info"> <div class="resource-info">
<div class="resource-title">${query ? search.highlight(item.title, query) : escapeHtml(item.title)}</div> <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-description">${escapeHtml(item.description || 'No description')}</div>
${item.tools?.length || item.mcpServers?.length ? ` <div class="resource-meta">
<div class="resource-meta"> ${item.model ? `<span class="resource-tag tag-model">${escapeHtml(item.model)}</span>` : '<span class="resource-tag tag-none">No model</span>'}
${item.model ? `<span class="resource-tag">Model: ${escapeHtml(item.model)}</span>` : ''} ${item.hasHandoffs ? '<span class="resource-tag tag-handoffs">Has Handoffs</span>' : ''}
${item.mcpServers?.slice(0, 3).map(s => `<span class="resource-tag">MCP: ${escapeHtml(s)}</span>`).join('') || ''} ${item.tools?.length ? `<span class="resource-tag">${item.tools.length} tools</span>` : ''}
</div> ${item.mcpServers?.length ? `<span class="resource-tag">MCP: ${item.mcpServers.length}</span>` : ''}
` : ''} </div>
</div> </div>
<div class="resource-actions"> <div class="resource-actions">
<a href="${getVSCodeInstallUrl(resourceType, item.path)}" class="btn btn-primary" onclick="event.stopPropagation()"> <a href="${getVSCodeInstallUrl(resourceType, item.path)}" class="btn btn-primary" onclick="event.stopPropagation()">
+111 -18
View File
@@ -7,6 +7,7 @@
<meta name="description" content="Curated collections of prompts, instructions, and agents for specific workflows"> <meta name="description" content="Curated collections of prompts, instructions, and agents for specific workflows">
<link rel="stylesheet" href="../css/styles.css"> <link rel="stylesheet" href="../css/styles.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📦</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📦</text></svg>">
<script src="../js/theme.js"></script>
</head> </head>
<body> <body>
<header class="site-header"> <header class="site-header">
@@ -25,11 +26,21 @@
<a href="tools.html">Tools</a> <a href="tools.html">Tools</a>
<a href="samples.html">Samples</a> <a href="samples.html">Samples</a>
</nav> </nav>
<a href="https://github.com/github/awesome-copilot" class="github-link" target="_blank" rel="noopener"> <div class="header-actions">
<svg viewBox="0 0 16 16" width="24" height="24" fill="currentColor"> <button id="theme-toggle" class="theme-toggle" title="Toggle theme">
<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 class="icon-sun" viewBox="0 0 16 16" fill="currentColor">
</svg> <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"/>
</a> </svg>
<svg class="icon-moon" viewBox="0 0 16 16" fill="currentColor">
<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"/>
</svg>
</button>
<a href="https://github.com/github/awesome-copilot" class="github-link" target="_blank" rel="noopener">
<svg viewBox="0 0 16 16" width="24" height="24" fill="currentColor">
<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>
</div> </div>
</header> </header>
@@ -47,6 +58,22 @@
<div class="search-bar"> <div class="search-bar">
<input type="text" id="search-input" placeholder="Search collections..." autocomplete="off"> <input type="text" id="search-input" placeholder="Search collections..." autocomplete="off">
</div> </div>
<!-- Filters -->
<div class="filters-bar" id="filters-bar">
<div class="filter-group">
<label>Tag:</label>
<div id="filter-tag" class="multi-select-container"></div>
</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"></div> <div class="results-count" id="results-count"></div>
<div class="resource-list" id="resource-list"> <div class="resource-list" id="resource-list">
<div class="loading">Loading collections...</div> <div class="loading">Loading collections...</div>
@@ -96,43 +123,109 @@
<script src="../js/utils.js"></script> <script src="../js/utils.js"></script>
<script src="../js/search.js"></script> <script src="../js/search.js"></script>
<script src="../js/multi-select.js"></script>
<script src="../js/app.js"></script> <script src="../js/app.js"></script>
<script> <script>
const resourceType = 'collection'; const resourceType = 'collection';
const dataFile = 'collections.json'; const dataFile = 'collections.json';
let allItems = []; let allItems = [];
let filters = { tags: [] };
let search = new FuzzySearch(); let search = new FuzzySearch();
let tagSelect;
// Current filter state
let currentFilters = {
tags: [],
featured: false,
};
async function initPage() { async function initPage() {
const list = document.getElementById('resource-list'); const list = document.getElementById('resource-list');
const countEl = document.getElementById('results-count'); const countEl = document.getElementById('results-count');
const searchInput = document.getElementById('search-input'); const searchInput = document.getElementById('search-input');
const featuredCheckbox = document.getElementById('filter-featured');
const clearFiltersBtn = document.getElementById('clear-filters');
const data = await fetchData(dataFile); const data = await fetchData(dataFile);
if (!data) { if (!data || !data.items) {
list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>'; list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
return; return;
} }
allItems = data; allItems = data.items;
filters = data.filters;
search.setItems(allItems.map(item => ({ search.setItems(allItems.map(item => ({
...item, ...item,
title: item.name, title: item.name,
searchText: `${item.name} ${item.description} ${item.tags?.join(' ') || ''}`.toLowerCase() searchText: `${item.name} ${item.description} ${item.tags?.join(' ') || ''}`.toLowerCase()
}))); })));
renderItems(allItems);
countEl.textContent = `${allItems.length} collections`;
searchInput.addEventListener('input', debounce((e) => { // Initialize multi-select filter
const query = e.target.value; tagSelect = new MultiSelect('#filter-tag', {
const results = query ? search.search(query) : allItems; placeholder: 'All Tags',
renderItems(results, query); onChange: (selected) => {
countEl.textContent = `${results.length} of ${allItems.length} collections`; currentFilters.tags = selected;
}, 200)); applyFiltersAndRender();
}
});
tagSelect.setItems(filters.tags);
// Render all items
applyFiltersAndRender();
// Setup search
searchInput.addEventListener('input', debounce(() => applyFiltersAndRender(), 200));
featuredCheckbox.addEventListener('change', () => {
currentFilters.featured = featuredCheckbox.checked;
applyFiltersAndRender();
});
clearFiltersBtn.addEventListener('click', () => {
currentFilters = { tags: [], featured: false };
tagSelect.clearSelection();
featuredCheckbox.checked = false;
searchInput.value = '';
applyFiltersAndRender();
});
setupModal(); setupModal();
} }
function applyFiltersAndRender() {
const searchInput = document.getElementById('search-input');
const countEl = document.getElementById('results-count');
const query = searchInput.value;
// Start with all items or search results
let results = query ? search.search(query) : [...allItems];
// Apply tag filter (OR logic)
if (currentFilters.tags.length > 0) {
results = results.filter(item =>
item.tags?.some(tag => currentFilters.tags.includes(tag))
);
}
// Apply featured filter
if (currentFilters.featured) {
results = results.filter(item => item.featured);
}
renderItems(results, query);
// Update count with filter info
const activeFilters = [];
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(', ')})`;
}
countEl.textContent = countText;
}
function renderItems(items, query = '') { function renderItems(items, query = '') {
const list = document.getElementById('resource-list'); const list = document.getElementById('resource-list');
@@ -140,7 +233,7 @@
list.innerHTML = ` list.innerHTML = `
<div class="empty-state"> <div class="empty-state">
<h3>No collections found</h3> <h3>No collections found</h3>
<p>Try a different search term</p> <p>Try a different search term or adjust filters</p>
</div> </div>
`; `;
return; return;
@@ -150,11 +243,11 @@
<div class="resource-item" onclick="openFileModal('${item.path}', '${resourceType}')"> <div class="resource-item" onclick="openFileModal('${item.path}', '${resourceType}')">
<div class="resource-info"> <div class="resource-info">
<div class="resource-title"> <div class="resource-title">
${item.featured ? ' ' : ''}${query ? search.highlight(item.name, query) : escapeHtml(item.name)} ${item.featured ? '<span class="tag-featured">⭐ Featured</span> ' : ''}${query ? search.highlight(item.name, query) : escapeHtml(item.name)}
</div> </div>
<div class="resource-description">${escapeHtml(item.description || 'No description')}</div> <div class="resource-description">${escapeHtml(item.description || 'No description')}</div>
<div class="resource-meta"> <div class="resource-meta">
${item.tags?.map(tag => `<span class="resource-tag">${escapeHtml(tag)}</span>`).join('') || ''} ${item.tags?.map(tag => `<span class="resource-tag tag-collection">${escapeHtml(tag)}</span>`).join('') || ''}
<span class="resource-tag">${item.items?.length || 0} items</span> <span class="resource-tag">${item.items?.length || 0} items</span>
</div> </div>
${item.items?.length ? ` ${item.items?.length ? `
+92 -21
View File
@@ -7,6 +7,7 @@
<meta name="description" content="Coding standards and best practices for GitHub Copilot"> <meta name="description" content="Coding standards and best practices for GitHub Copilot">
<link rel="stylesheet" href="../css/styles.css"> <link rel="stylesheet" href="../css/styles.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📋</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📋</text></svg>">
<script src="../js/theme.js"></script>
</head> </head>
<body> <body>
<header class="site-header"> <header class="site-header">
@@ -25,11 +26,21 @@
<a href="tools.html">Tools</a> <a href="tools.html">Tools</a>
<a href="samples.html">Samples</a> <a href="samples.html">Samples</a>
</nav> </nav>
<a href="https://github.com/github/awesome-copilot" class="github-link" target="_blank" rel="noopener"> <div class="header-actions">
<svg viewBox="0 0 16 16" width="24" height="24" fill="currentColor"> <button id="theme-toggle" class="theme-toggle" title="Toggle theme">
<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 class="icon-sun" viewBox="0 0 16 16" fill="currentColor">
</svg> <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"/>
</a> </svg>
<svg class="icon-moon" viewBox="0 0 16 16" fill="currentColor">
<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"/>
</svg>
</button>
<a href="https://github.com/github/awesome-copilot" class="github-link" target="_blank" rel="noopener">
<svg viewBox="0 0 16 16" width="24" height="24" fill="currentColor">
<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>
</div> </div>
</header> </header>
@@ -47,6 +58,16 @@
<div class="search-bar"> <div class="search-bar">
<input type="text" id="search-input" placeholder="Search instructions..." autocomplete="off"> <input type="text" id="search-input" placeholder="Search instructions..." autocomplete="off">
</div> </div>
<!-- Filters -->
<div class="filters-bar" id="filters-bar">
<div class="filter-group">
<label>File Extension:</label>
<div id="filter-extension" class="multi-select-container"></div>
</div>
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
</div>
<div class="results-count" id="results-count"></div> <div class="results-count" id="results-count"></div>
<div class="resource-list" id="resource-list"> <div class="resource-list" id="resource-list">
<div class="loading">Loading instructions...</div> <div class="loading">Loading instructions...</div>
@@ -100,39 +121,91 @@
<script src="../js/utils.js"></script> <script src="../js/utils.js"></script>
<script src="../js/search.js"></script> <script src="../js/search.js"></script>
<script src="../js/multi-select.js"></script>
<script src="../js/app.js"></script> <script src="../js/app.js"></script>
<script> <script>
const resourceType = 'instruction'; const resourceType = 'instruction';
const dataFile = 'instructions.json'; const dataFile = 'instructions.json';
let allItems = []; let allItems = [];
let filters = { extensions: [], patterns: [] };
let search = new FuzzySearch(); let search = new FuzzySearch();
let extensionSelect;
// Current filter state
let currentFilters = {
extensions: [],
};
async function initPage() { async function initPage() {
const list = document.getElementById('resource-list'); const list = document.getElementById('resource-list');
const countEl = document.getElementById('results-count'); const countEl = document.getElementById('results-count');
const searchInput = document.getElementById('search-input'); const searchInput = document.getElementById('search-input');
const clearFiltersBtn = document.getElementById('clear-filters');
const data = await fetchData(dataFile); const data = await fetchData(dataFile);
if (!data) { if (!data || !data.items) {
list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>'; list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
return; return;
} }
allItems = data; allItems = data.items;
filters = data.filters;
search.setItems(allItems); search.setItems(allItems);
renderItems(allItems);
countEl.textContent = `${allItems.length} instructions`;
searchInput.addEventListener('input', debounce((e) => { // Initialize multi-select filter
const query = e.target.value; extensionSelect = new MultiSelect('#filter-extension', {
const results = query ? search.search(query) : allItems; placeholder: 'All Extensions',
renderItems(results, query); onChange: (selected) => {
countEl.textContent = `${results.length} of ${allItems.length} instructions`; currentFilters.extensions = selected;
}, 200)); applyFiltersAndRender();
}
});
extensionSelect.setItems(filters.extensions);
// Render all items
applyFiltersAndRender();
// Setup search
searchInput.addEventListener('input', debounce(() => applyFiltersAndRender(), 200));
clearFiltersBtn.addEventListener('click', () => {
currentFilters = { extensions: [] };
extensionSelect.clearSelection();
searchInput.value = '';
applyFiltersAndRender();
});
setupModal(); setupModal();
} }
function applyFiltersAndRender() {
const searchInput = document.getElementById('search-input');
const countEl = document.getElementById('results-count');
const query = searchInput.value;
// Start with all items or search results
let results = query ? search.search(query) : [...allItems];
// Apply extension filter (OR logic)
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);
// Update count with filter info
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' : ''})`;
}
countEl.textContent = countText;
}
function renderItems(items, query = '') { function renderItems(items, query = '') {
const list = document.getElementById('resource-list'); const list = document.getElementById('resource-list');
@@ -140,7 +213,7 @@
list.innerHTML = ` list.innerHTML = `
<div class="empty-state"> <div class="empty-state">
<h3>No instructions found</h3> <h3>No instructions found</h3>
<p>Try a different search term</p> <p>Try a different search term or adjust filters</p>
</div> </div>
`; `;
return; return;
@@ -151,11 +224,9 @@
<div class="resource-info"> <div class="resource-info">
<div class="resource-title">${query ? search.highlight(item.title, query) : escapeHtml(item.title)}</div> <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-description">${escapeHtml(item.description || 'No description')}</div>
${item.applyTo ? ` <div class="resource-meta">
<div class="resource-meta"> ${item.extensions?.length ? item.extensions.map(ext => `<span class="resource-tag tag-extension">${escapeHtml(ext)}</span>`).join('') : '<span class="resource-tag tag-none">All files</span>'}
<span class="resource-tag">Applies to: ${escapeHtml(item.applyTo)}</span> </div>
</div>
` : ''}
</div> </div>
<div class="resource-actions"> <div class="resource-actions">
<a href="${getVSCodeInstallUrl('instructions', item.path)}" class="btn btn-primary" onclick="event.stopPropagation()"> <a href="${getVSCodeInstallUrl('instructions', item.path)}" class="btn btn-primary" onclick="event.stopPropagation()">
+88 -18
View File
@@ -7,6 +7,7 @@
<meta name="description" content="Ready-to-use prompt templates for development tasks with GitHub Copilot"> <meta name="description" content="Ready-to-use prompt templates for development tasks with GitHub Copilot">
<link rel="stylesheet" href="../css/styles.css"> <link rel="stylesheet" href="../css/styles.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎯</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎯</text></svg>">
<script src="../js/theme.js"></script>
</head> </head>
<body> <body>
<header class="site-header"> <header class="site-header">
@@ -25,11 +26,21 @@
<a href="tools.html">Tools</a> <a href="tools.html">Tools</a>
<a href="samples.html">Samples</a> <a href="samples.html">Samples</a>
</nav> </nav>
<a href="https://github.com/github/awesome-copilot" class="github-link" target="_blank" rel="noopener"> <div class="header-actions">
<svg viewBox="0 0 16 16" width="24" height="24" fill="currentColor"> <button id="theme-toggle" class="theme-toggle" title="Toggle theme">
<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 class="icon-sun" viewBox="0 0 16 16" fill="currentColor">
</svg> <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"/>
</a> </svg>
<svg class="icon-moon" viewBox="0 0 16 16" fill="currentColor">
<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"/>
</svg>
</button>
<a href="https://github.com/github/awesome-copilot" class="github-link" target="_blank" rel="noopener">
<svg viewBox="0 0 16 16" width="24" height="24" fill="currentColor">
<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>
</div> </div>
</header> </header>
@@ -47,6 +58,16 @@
<div class="search-bar"> <div class="search-bar">
<input type="text" id="search-input" placeholder="Search prompts..." autocomplete="off"> <input type="text" id="search-input" placeholder="Search prompts..." autocomplete="off">
</div> </div>
<!-- Filters -->
<div class="filters-bar" id="filters-bar">
<div class="filter-group">
<label>Tool:</label>
<div id="filter-tool" class="multi-select-container"></div>
</div>
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
</div>
<div class="results-count" id="results-count"></div> <div class="results-count" id="results-count"></div>
<div class="resource-list" id="resource-list"> <div class="resource-list" id="resource-list">
<div class="loading">Loading prompts...</div> <div class="loading">Loading prompts...</div>
@@ -100,39 +121,88 @@
<script src="../js/utils.js"></script> <script src="../js/utils.js"></script>
<script src="../js/search.js"></script> <script src="../js/search.js"></script>
<script src="../js/multi-select.js"></script>
<script src="../js/app.js"></script> <script src="../js/app.js"></script>
<script> <script>
const resourceType = 'prompt'; const resourceType = 'prompt';
const dataFile = 'prompts.json'; const dataFile = 'prompts.json';
let allItems = []; let allItems = [];
let filters = { tools: [] };
let search = new FuzzySearch(); let search = new FuzzySearch();
let toolSelect;
// Current filter state
let currentFilters = {
tools: [],
};
async function initPage() { async function initPage() {
const list = document.getElementById('resource-list'); const list = document.getElementById('resource-list');
const countEl = document.getElementById('results-count'); const countEl = document.getElementById('results-count');
const searchInput = document.getElementById('search-input'); const searchInput = document.getElementById('search-input');
const clearFiltersBtn = document.getElementById('clear-filters');
const data = await fetchData(dataFile); const data = await fetchData(dataFile);
if (!data) { if (!data || !data.items) {
list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>'; list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
return; return;
} }
allItems = data; allItems = data.items;
filters = data.filters;
search.setItems(allItems); search.setItems(allItems);
renderItems(allItems);
countEl.textContent = `${allItems.length} prompts`;
searchInput.addEventListener('input', debounce((e) => { // Initialize multi-select filter
const query = e.target.value; toolSelect = new MultiSelect('#filter-tool', {
const results = query ? search.search(query) : allItems; placeholder: 'All Tools',
renderItems(results, query); onChange: (selected) => {
countEl.textContent = `${results.length} of ${allItems.length} prompts`; currentFilters.tools = selected;
}, 200)); applyFiltersAndRender();
}
});
toolSelect.setItems(filters.tools);
// Render all items
applyFiltersAndRender();
// Setup search
searchInput.addEventListener('input', debounce(() => applyFiltersAndRender(), 200));
clearFiltersBtn.addEventListener('click', () => {
currentFilters = { tools: [] };
toolSelect.clearSelection();
searchInput.value = '';
applyFiltersAndRender();
});
setupModal(); setupModal();
} }
function applyFiltersAndRender() {
const searchInput = document.getElementById('search-input');
const countEl = document.getElementById('results-count');
const query = searchInput.value;
// Start with all items or search results
let results = query ? search.search(query) : [...allItems];
// Apply tool filter (OR logic)
if (currentFilters.tools.length > 0) {
results = results.filter(item =>
item.tools?.some(tool => currentFilters.tools.includes(tool))
);
}
renderItems(results, query);
// Update count with filter info
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' : ''})`;
}
countEl.textContent = countText;
}
function renderItems(items, query = '') { function renderItems(items, query = '') {
const list = document.getElementById('resource-list'); const list = document.getElementById('resource-list');
@@ -140,7 +210,7 @@
list.innerHTML = ` list.innerHTML = `
<div class="empty-state"> <div class="empty-state">
<h3>No prompts found</h3> <h3>No prompts found</h3>
<p>Try a different search term</p> <p>Try a different search term or adjust filters</p>
</div> </div>
`; `;
return; return;
@@ -152,8 +222,8 @@
<div class="resource-title">${query ? search.highlight(item.title, query) : escapeHtml(item.title)}</div> <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-description">${escapeHtml(item.description || 'No description')}</div>
<div class="resource-meta"> <div class="resource-meta">
${item.model ? `<span class="resource-tag">Model: ${escapeHtml(item.model)}</span>` : ''} ${item.model ? `<span class="resource-tag tag-model">${escapeHtml(item.model)}</span>` : ''}
${item.agent ? `<span class="resource-tag">Agent: ${escapeHtml(item.agent)}</span>` : ''} ${item.tools?.length ? `<span class="resource-tag">${item.tools.length} tools</span>` : ''}
</div> </div>
</div> </div>
<div class="resource-actions"> <div class="resource-actions">
+16 -5
View File
@@ -7,6 +7,7 @@
<meta name="description" content="Code samples and examples for building with GitHub Copilot"> <meta name="description" content="Code samples and examples for building with GitHub Copilot">
<link rel="stylesheet" href="../css/styles.css"> <link rel="stylesheet" href="../css/styles.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📚</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📚</text></svg>">
<script src="../js/theme.js"></script>
</head> </head>
<body> <body>
<header class="site-header"> <header class="site-header">
@@ -25,11 +26,21 @@
<a href="tools.html">Tools</a> <a href="tools.html">Tools</a>
<a href="samples.html" class="active">Samples</a> <a href="samples.html" class="active">Samples</a>
</nav> </nav>
<a href="https://github.com/github/awesome-copilot" class="github-link" target="_blank" rel="noopener"> <div class="header-actions">
<svg viewBox="0 0 16 16" width="24" height="24" fill="currentColor"> <button id="theme-toggle" class="theme-toggle" title="Toggle theme">
<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 class="icon-sun" viewBox="0 0 16 16" fill="currentColor">
</svg> <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"/>
</a> </svg>
<svg class="icon-moon" viewBox="0 0 16 16" fill="currentColor">
<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"/>
</svg>
</button>
<a href="https://github.com/github/awesome-copilot" class="github-link" target="_blank" rel="noopener">
<svg viewBox="0 0 16 16" width="24" height="24" fill="currentColor">
<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>
</div> </div>
</header> </header>
+217 -21
View File
@@ -7,6 +7,7 @@
<meta name="description" content="Self-contained agent skills with instructions and bundled resources"> <meta name="description" content="Self-contained agent skills with instructions and bundled resources">
<link rel="stylesheet" href="../css/styles.css"> <link rel="stylesheet" href="../css/styles.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>">
<script src="../js/theme.js"></script>
</head> </head>
<body> <body>
<header class="site-header"> <header class="site-header">
@@ -25,11 +26,21 @@
<a href="tools.html">Tools</a> <a href="tools.html">Tools</a>
<a href="samples.html">Samples</a> <a href="samples.html">Samples</a>
</nav> </nav>
<a href="https://github.com/github/awesome-copilot" class="github-link" target="_blank" rel="noopener"> <div class="header-actions">
<svg viewBox="0 0 16 16" width="24" height="24" fill="currentColor"> <button id="theme-toggle" class="theme-toggle" title="Toggle theme">
<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 class="icon-sun" viewBox="0 0 16 16" fill="currentColor">
</svg> <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"/>
</a> </svg>
<svg class="icon-moon" viewBox="0 0 16 16" fill="currentColor">
<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"/>
</svg>
</button>
<a href="https://github.com/github/awesome-copilot" class="github-link" target="_blank" rel="noopener">
<svg viewBox="0 0 16 16" width="24" height="24" fill="currentColor">
<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>
</div> </div>
</header> </header>
@@ -47,6 +58,22 @@
<div class="search-bar"> <div class="search-bar">
<input type="text" id="search-input" placeholder="Search skills..." autocomplete="off"> <input type="text" id="search-input" placeholder="Search skills..." autocomplete="off">
</div> </div>
<!-- Filters -->
<div class="filters-bar" id="filters-bar">
<div class="filter-group">
<label>Category:</label>
<div id="filter-category" class="multi-select-container"></div>
</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"></div> <div class="results-count" id="results-count"></div>
<div class="resource-list" id="resource-list"> <div class="resource-list" id="resource-list">
<div class="loading">Loading skills...</div> <div class="loading">Loading skills...</div>
@@ -99,39 +126,104 @@
<script src="../js/utils.js"></script> <script src="../js/utils.js"></script>
<script src="../js/search.js"></script> <script src="../js/search.js"></script>
<script src="../js/multi-select.js"></script>
<script src="../js/jszip.min.js"></script>
<script src="../js/app.js"></script> <script src="../js/app.js"></script>
<script> <script>
const resourceType = 'skill'; const resourceType = 'skill';
const dataFile = 'skills.json'; const dataFile = 'skills.json';
let allItems = []; let allItems = [];
let filters = { categories: [], hasAssets: [] };
let search = new FuzzySearch(); let search = new FuzzySearch();
let categorySelect;
// Current filter state
let currentFilters = {
categories: [],
hasAssets: false,
};
async function initPage() { async function initPage() {
const list = document.getElementById('resource-list'); const list = document.getElementById('resource-list');
const countEl = document.getElementById('results-count'); const countEl = document.getElementById('results-count');
const searchInput = document.getElementById('search-input'); const searchInput = document.getElementById('search-input');
const hasAssetsCheckbox = document.getElementById('filter-has-assets');
const clearFiltersBtn = document.getElementById('clear-filters');
const data = await fetchData(dataFile); const data = await fetchData(dataFile);
if (!data) { if (!data || !data.items) {
list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>'; list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
return; return;
} }
allItems = data; allItems = data.items;
filters = data.filters;
search.setItems(allItems); search.setItems(allItems);
renderItems(allItems);
countEl.textContent = `${allItems.length} skills`;
searchInput.addEventListener('input', debounce((e) => { // Initialize multi-select filter
const query = e.target.value; categorySelect = new MultiSelect('#filter-category', {
const results = query ? search.search(query) : allItems; placeholder: 'All Categories',
renderItems(results, query); onChange: (selected) => {
countEl.textContent = `${results.length} of ${allItems.length} skills`; currentFilters.categories = selected;
}, 200)); applyFiltersAndRender();
}
});
categorySelect.setItems(filters.categories);
// Render all items
applyFiltersAndRender();
// Setup search
searchInput.addEventListener('input', debounce(() => applyFiltersAndRender(), 200));
hasAssetsCheckbox.addEventListener('change', () => {
currentFilters.hasAssets = hasAssetsCheckbox.checked;
applyFiltersAndRender();
});
clearFiltersBtn.addEventListener('click', () => {
currentFilters = { categories: [], hasAssets: false };
categorySelect.clearSelection();
hasAssetsCheckbox.checked = false;
searchInput.value = '';
applyFiltersAndRender();
});
setupModal(); setupModal();
} }
function applyFiltersAndRender() {
const searchInput = document.getElementById('search-input');
const countEl = document.getElementById('results-count');
const query = searchInput.value;
// Start with all items or search results
let results = query ? search.search(query) : [...allItems];
// Apply category filter (OR logic)
if (currentFilters.categories.length > 0) {
results = results.filter(item => currentFilters.categories.includes(item.category));
}
// Apply has assets filter
if (currentFilters.hasAssets) {
results = results.filter(item => item.hasAssets);
}
renderItems(results, query);
// Update count with filter info
const activeFilters = [];
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(', ')})`;
}
countEl.textContent = countText;
}
function renderItems(items, query = '') { function renderItems(items, query = '') {
const list = document.getElementById('resource-list'); const list = document.getElementById('resource-list');
@@ -139,7 +231,7 @@
list.innerHTML = ` list.innerHTML = `
<div class="empty-state"> <div class="empty-state">
<h3>No skills found</h3> <h3>No skills found</h3>
<p>Try a different search term</p> <p>Try a different search term or adjust filters</p>
</div> </div>
`; `;
return; return;
@@ -150,13 +242,20 @@
<div class="resource-info"> <div class="resource-info">
<div class="resource-title">${query ? search.highlight(item.title, query) : escapeHtml(item.title)}</div> <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-description">${escapeHtml(item.description || 'No description')}</div>
${item.assets?.length ? ` <div class="resource-meta">
<div class="resource-meta"> <span class="resource-tag tag-category">${escapeHtml(item.category)}</span>
<span class="resource-tag">${item.assets.length} bundled asset${item.assets.length === 1 ? '' : 's'}</span> ${item.hasAssets ? `<span class="resource-tag tag-assets">${item.assetCount} asset${item.assetCount === 1 ? '' : 's'}</span>` : ''}
</div> <span class="resource-tag">${item.files.length} file${item.files.length === 1 ? '' : 's'}</span>
` : ''} </div>
</div> </div>
<div class="resource-actions"> <div class="resource-actions">
<button class="btn btn-primary" onclick="event.stopPropagation(); downloadSkill('${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()"> <a href="${getGitHubUrl(item.path)}" class="btn btn-secondary" target="_blank" onclick="event.stopPropagation()">
View Folder View Folder
</a> </a>
@@ -165,6 +264,103 @@
`).join(''); `).join('');
} }
// Download skill as ZIP file
async function downloadSkill(skillId) {
const skill = allItems.find(item => item.id === skillId);
if (!skill || !skill.files || skill.files.length === 0) {
alert('No files found for this skill');
return;
}
// Show loading state
const btn = event.target.closest('button');
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);
// Fetch all files in parallel
const fetchPromises = skill.files.map(async (file) => {
const url = getRawGitHubUrl(file.path);
try {
const response = await fetch(url);
if (!response.ok) {
console.warn(`Failed to fetch ${file.path}: ${response.status}`);
return null;
}
const content = await response.text();
return { name: file.name, content };
} catch (err) {
console.warn(`Error fetching ${file.path}:`, err);
return null;
}
});
const results = await Promise.all(fetchPromises);
// Add successfully fetched files to zip
let addedFiles = 0;
for (const result of results) {
if (result) {
folder.file(result.name, result.content);
addedFiles++;
}
}
if (addedFiles === 0) {
throw new Error('Failed to fetch any files');
}
// Generate and download zip
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);
// Show success
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 (err) {
console.error('Download failed:', err);
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);
}
}
document.addEventListener('DOMContentLoaded', initPage); document.addEventListener('DOMContentLoaded', initPage);
</script> </script>
</body> </body>
+16 -5
View File
@@ -7,6 +7,7 @@
<meta name="description" content="MCP servers and developer tools for GitHub Copilot"> <meta name="description" content="MCP servers and developer tools for GitHub Copilot">
<link rel="stylesheet" href="../css/styles.css"> <link rel="stylesheet" href="../css/styles.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔧</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔧</text></svg>">
<script src="../js/theme.js"></script>
</head> </head>
<body> <body>
<header class="site-header"> <header class="site-header">
@@ -25,11 +26,21 @@
<a href="tools.html" class="active">Tools</a> <a href="tools.html" class="active">Tools</a>
<a href="samples.html">Samples</a> <a href="samples.html">Samples</a>
</nav> </nav>
<a href="https://github.com/github/awesome-copilot" class="github-link" target="_blank" rel="noopener"> <div class="header-actions">
<svg viewBox="0 0 16 16" width="24" height="24" fill="currentColor"> <button id="theme-toggle" class="theme-toggle" title="Toggle theme">
<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 class="icon-sun" viewBox="0 0 16 16" fill="currentColor">
</svg> <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"/>
</a> </svg>
<svg class="icon-moon" viewBox="0 0 16 16" fill="currentColor">
<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"/>
</svg>
</button>
<a href="https://github.com/github/awesome-copilot" class="github-link" target="_blank" rel="noopener">
<svg viewBox="0 0 16 16" width="24" height="24" fill="currentColor">
<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>
</div> </div>
</header> </header>