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

View File

@@ -80,17 +80,34 @@ function generateAgentsData() {
.readdirSync(AGENTS_DIR)
.filter((f) => f.endsWith(".agent.md"));
// Track all unique values for filters
const allModels = new Set();
const allTools = new Set();
for (const file of files) {
const filePath = path.join(AGENTS_DIR, file);
const frontmatter = parseFrontmatter(filePath);
const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/");
const model = frontmatter?.model || null;
const tools = frontmatter?.tools || [];
const handoffs = frontmatter?.handoffs || [];
// Track unique values
if (model) allModels.add(model);
tools.forEach((t) => allTools.add(t));
agents.push({
id: file.replace(".agent.md", ""),
title: extractTitle(filePath, frontmatter),
description: frontmatter?.description || "",
model: frontmatter?.model || null,
tools: frontmatter?.tools || [],
model: model,
tools: tools,
hasHandoffs: handoffs.length > 0,
handoffs: handoffs.map((h) => ({
label: h.label || "",
agent: h.agent || "",
})),
mcpServers: frontmatter?.["mcp-servers"]
? Object.keys(frontmatter["mcp-servers"])
: [],
@@ -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)
.filter((f) => f.endsWith(".prompt.md"));
// Track all unique tools for filters
const allTools = new Set();
for (const file of files) {
const filePath = path.join(PROMPTS_DIR, file);
const frontmatter = parseFrontmatter(filePath);
const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/");
const tools = frontmatter?.tools || [];
tools.forEach((t) => allTools.add(t));
prompts.push({
id: file.replace(".prompt.md", ""),
title: extractTitle(filePath, frontmatter),
description: frontmatter?.description || "",
agent: frontmatter?.agent || null,
model: frontmatter?.model || null,
tools: frontmatter?.tools || [],
tools: tools,
path: relativePath,
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)
.filter((f) => f.endsWith(".instructions.md"));
// Track all unique patterns and extensions for filters
const allPatterns = new Set();
const allExtensions = new Set();
for (const file of files) {
const filePath = path.join(INSTRUCTIONS_DIR, file);
const frontmatter = parseFrontmatter(filePath);
const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/");
const applyToRaw = frontmatter?.applyTo || null;
const applyToPatterns = parseApplyToPatterns(applyToRaw);
// Extract extensions from patterns
const extensions = [];
for (const pattern of applyToPatterns) {
allPatterns.add(pattern);
const ext = extractExtensionFromPattern(pattern);
if (ext) {
if (Array.isArray(ext)) {
ext.forEach(e => {
extensions.push(e);
allExtensions.add(e);
});
} else {
extensions.push(ext);
allExtensions.add(ext);
}
}
}
instructions.push({
id: file.replace(".instructions.md", ""),
title: extractTitle(filePath, frontmatter),
description: frontmatter?.description || "",
applyTo: frontmatter?.applyTo || null,
applyTo: applyToRaw,
applyToPatterns: applyToPatterns,
extensions: [...new Set(extensions)],
path: relativePath,
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 = [];
if (!fs.existsSync(SKILLS_DIR)) {
return skills;
return { items: [], filters: { categories: [], hasAssets: ['Yes', 'No'] } };
}
const folders = fs
.readdirSync(SKILLS_DIR)
.filter((f) => fs.statSync(path.join(SKILLS_DIR, f)).isDirectory());
const allCategories = new Set();
for (const folder of folders) {
const skillPath = path.join(SKILLS_DIR, folder);
const metadata = parseSkillMetadata(skillPath);
if (metadata) {
const relativePath = path.relative(ROOT_FOLDER, skillPath).replace(/\\/g, "/");
const category = categorizeSkill(metadata.name, metadata.description);
allCategories.add(category);
// Get all files in the skill folder recursively
const files = getSkillFiles(skillPath, relativePath);
skills.push({
id: folder,
@@ -188,13 +323,55 @@ function generateSkillsData() {
.join(" "),
description: metadata.description,
assets: metadata.assets,
hasAssets: metadata.assets.length > 0,
assetCount: metadata.assets.length,
category: category,
path: relativePath,
skillFile: `${relativePath}/SKILL.md`,
files: files,
});
}
}
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)
.filter((f) => f.endsWith(".collection.yml"));
// Track all unique tags
const allTags = new Set();
for (const file of files) {
const filePath = path.join(COLLECTIONS_DIR, file);
const data = parseCollectionYaml(filePath);
const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/");
if (data) {
const tags = data.tags || [];
tags.forEach((t) => allTags.add(t));
collections.push({
id: file.replace(".collection.yml", ""),
name: data.name || file.replace(".collection.yml", ""),
description: data.description || "",
tags: data.tags || [],
tags: tags,
featured: data.featured || false,
items: (data.items || []).map((item) => ({
path: item.path,
@@ -235,11 +418,18 @@ function generateCollectionsData() {
}
// 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;
return a.name.localeCompare(b.name);
});
return {
items: sortedCollections,
filters: {
tags: Array.from(allTags).sort(),
},
};
}
/**
@@ -316,20 +506,25 @@ async function main() {
ensureDataDir();
// Generate all data
const agents = generateAgentsData();
console.log(`✓ Generated ${agents.length} agents`);
const agentsData = generateAgentsData();
const agents = agentsData.items;
console.log(`✓ Generated ${agents.length} agents (${agentsData.filters.models.length} models, ${agentsData.filters.tools.length} tools)`);
const prompts = generatePromptsData();
console.log(`✓ Generated ${prompts.length} prompts`);
const promptsData = generatePromptsData();
const prompts = promptsData.items;
console.log(`✓ Generated ${prompts.length} prompts (${promptsData.filters.tools.length} tools)`);
const instructions = generateInstructionsData();
console.log(`✓ Generated ${instructions.length} instructions`);
const instructionsData = generateInstructionsData();
const instructions = instructionsData.items;
console.log(`✓ Generated ${instructions.length} instructions (${instructionsData.filters.extensions.length} extensions)`);
const skills = generateSkillsData();
console.log(`✓ Generated ${skills.length} skills`);
const skillsData = generateSkillsData();
const skills = skillsData.items;
console.log(`✓ Generated ${skills.length} skills (${skillsData.filters.categories.length} categories)`);
const collections = generateCollectionsData();
console.log(`✓ Generated ${collections.length} collections`);
const collectionsData = generateCollectionsData();
const collections = collectionsData.items;
console.log(`✓ Generated ${collections.length} collections (${collectionsData.filters.tags.length} tags)`);
const searchIndex = generateSearchIndex(agents, prompts, instructions, skills, collections);
console.log(`✓ Generated search index with ${searchIndex.length} items`);
@@ -337,27 +532,27 @@ async function main() {
// Write JSON files
fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "agents.json"),
JSON.stringify(agents, null, 2)
JSON.stringify(agentsData, null, 2)
);
fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "prompts.json"),
JSON.stringify(prompts, null, 2)
JSON.stringify(promptsData, null, 2)
);
fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "instructions.json"),
JSON.stringify(instructions, null, 2)
JSON.stringify(instructionsData, null, 2)
);
fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "skills.json"),
JSON.stringify(skills, null, 2)
JSON.stringify(skillsData, null, 2)
);
fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "collections.json"),
JSON.stringify(collections, null, 2)
JSON.stringify(collectionsData, null, 2)
);
fs.writeFileSync(

View File

@@ -1,5 +1,6 @@
/* CSS Variables and Base Styles */
:root {
/* Dark theme (default) */
--color-bg: #0d1117;
--color-bg-secondary: #161b22;
--color-bg-tertiary: #21262d;
@@ -25,9 +26,26 @@
--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) {
:root {
:root:not([data-theme="dark"]) {
--color-bg: #ffffff;
--color-bg-secondary: #f6f8fa;
--color-bg-tertiary: #f0f3f6;
@@ -39,6 +57,8 @@
--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);
}
}
@@ -140,6 +160,69 @@ a:hover {
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 {
background: linear-gradient(180deg, var(--color-bg-secondary) 0%, var(--color-bg) 100%);
@@ -452,6 +535,26 @@ a:hover {
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 {
position: fixed;
@@ -570,6 +673,308 @@ a:hover {
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 {
font-size: 14px;
color: var(--color-text-muted);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{
"generated": "2026-01-28T02:42:05.621Z",
"generated": "2026-01-28T03:53:29.513Z",
"counts": {
"agents": 140,
"prompts": 134,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@
<meta name="description" content="Community-contributed instructions, prompts, agents, and skills for GitHub Copilot">
<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>">
<script src="js/theme.js"></script>
</head>
<body>
<header class="site-header">
@@ -25,11 +26,21 @@
<a href="pages/tools.html">Tools</a>
<a href="pages/samples.html">Samples</a>
</nav>
<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 class="header-actions">
<button id="theme-toggle" class="theme-toggle" title="Toggle theme">
<svg class="icon-sun" viewBox="0 0 16 16" fill="currentColor">
<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"/>
</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>
</header>

13
website/js/jszip.min.js vendored Normal file

File diff suppressed because one or more lines are too long

209
website/js/multi-select.js Normal file
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
website/js/theme.js Normal file
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);
}
});

View File

@@ -78,6 +78,13 @@ function getGitHubUrl(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
*/

View File

@@ -7,6 +7,7 @@
<meta name="description" content="Custom agents for specialized GitHub Copilot experiences">
<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>">
<script src="../js/theme.js"></script>
</head>
<body>
<header class="site-header">
@@ -25,11 +26,21 @@
<a href="tools.html">Tools</a>
<a href="samples.html">Samples</a>
</nav>
<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 class="header-actions">
<button id="theme-toggle" class="theme-toggle" title="Toggle theme">
<svg class="icon-sun" viewBox="0 0 16 16" fill="currentColor">
<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"/>
</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>
</header>
@@ -47,6 +58,26 @@
<div class="search-bar">
<input type="text" id="search-input" placeholder="Search agents..." autocomplete="off">
</div>
<!-- Filters -->
<div class="filters-bar" id="filters-bar">
<div class="filter-group">
<label>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="resource-list" id="resource-list">
<div class="loading">Loading agents...</div>
@@ -100,45 +131,131 @@
<script src="../js/utils.js"></script>
<script src="../js/search.js"></script>
<script src="../js/multi-select.js"></script>
<script src="../js/app.js"></script>
<script>
// Page-specific initialization
const resourceType = 'agent';
const dataFile = 'agents.json';
let allItems = [];
let filters = { models: [], tools: [] };
let search = new FuzzySearch();
let modelSelect, toolSelect;
// Current filter state
let currentFilters = {
models: [],
tools: [],
hasHandoffs: false,
};
async function initPage() {
const list = document.getElementById('resource-list');
const countEl = document.getElementById('results-count');
const searchInput = document.getElementById('search-input');
const handoffsCheckbox = document.getElementById('filter-handoffs');
const clearFiltersBtn = document.getElementById('clear-filters');
// Load data
const data = await fetchData(dataFile);
if (!data) {
if (!data || !data.items) {
list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
return;
}
allItems = data;
allItems = data.items;
filters = data.filters;
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
renderItems(allItems);
countEl.textContent = `${allItems.length} agents`;
applyFiltersAndRender();
// Setup search
searchInput.addEventListener('input', debounce((e) => {
const query = e.target.value;
const results = query ? search.search(query) : allItems;
renderItems(results, query);
countEl.textContent = `${results.length} of ${allItems.length} agents`;
}, 200));
searchInput.addEventListener('input', debounce(() => applyFiltersAndRender(), 200));
// Setup filter listeners
handoffsCheckbox.addEventListener('change', () => {
currentFilters.hasHandoffs = handoffsCheckbox.checked;
applyFiltersAndRender();
});
clearFiltersBtn.addEventListener('click', () => {
currentFilters = { models: [], tools: [], hasHandoffs: false };
modelSelect.clearSelection();
toolSelect.clearSelection();
handoffsCheckbox.checked = false;
searchInput.value = '';
applyFiltersAndRender();
});
// Setup modal
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 = '') {
const list = document.getElementById('resource-list');
@@ -146,7 +263,7 @@
list.innerHTML = `
<div class="empty-state">
<h3>No agents found</h3>
<p>Try a different search term</p>
<p>Try a different search term or adjust filters</p>
</div>
`;
return;
@@ -157,12 +274,12 @@
<div class="resource-info">
<div class="resource-title">${query ? search.highlight(item.title, query) : escapeHtml(item.title)}</div>
<div class="resource-description">${escapeHtml(item.description || 'No description')}</div>
${item.tools?.length || item.mcpServers?.length ? `
<div class="resource-meta">
${item.model ? `<span class="resource-tag">Model: ${escapeHtml(item.model)}</span>` : ''}
${item.mcpServers?.slice(0, 3).map(s => `<span class="resource-tag">MCP: ${escapeHtml(s)}</span>`).join('') || ''}
</div>
` : ''}
<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.hasHandoffs ? '<span class="resource-tag tag-handoffs">Has Handoffs</span>' : ''}
${item.tools?.length ? `<span class="resource-tag">${item.tools.length} tools</span>` : ''}
${item.mcpServers?.length ? `<span class="resource-tag">MCP: ${item.mcpServers.length}</span>` : ''}
</div>
</div>
<div class="resource-actions">
<a href="${getVSCodeInstallUrl(resourceType, item.path)}" class="btn btn-primary" onclick="event.stopPropagation()">

View File

@@ -7,6 +7,7 @@
<meta name="description" content="Curated collections of prompts, instructions, and agents for specific workflows">
<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>">
<script src="../js/theme.js"></script>
</head>
<body>
<header class="site-header">
@@ -25,11 +26,21 @@
<a href="tools.html">Tools</a>
<a href="samples.html">Samples</a>
</nav>
<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 class="header-actions">
<button id="theme-toggle" class="theme-toggle" title="Toggle theme">
<svg class="icon-sun" viewBox="0 0 16 16" fill="currentColor">
<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"/>
</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>
</header>
@@ -47,6 +58,22 @@
<div class="search-bar">
<input type="text" id="search-input" placeholder="Search collections..." autocomplete="off">
</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="resource-list" id="resource-list">
<div class="loading">Loading collections...</div>
@@ -96,43 +123,109 @@
<script src="../js/utils.js"></script>
<script src="../js/search.js"></script>
<script src="../js/multi-select.js"></script>
<script src="../js/app.js"></script>
<script>
const resourceType = 'collection';
const dataFile = 'collections.json';
let allItems = [];
let filters = { tags: [] };
let search = new FuzzySearch();
let tagSelect;
// Current filter state
let currentFilters = {
tags: [],
featured: false,
};
async function initPage() {
const list = document.getElementById('resource-list');
const countEl = document.getElementById('results-count');
const searchInput = document.getElementById('search-input');
const featuredCheckbox = document.getElementById('filter-featured');
const clearFiltersBtn = document.getElementById('clear-filters');
const data = await fetchData(dataFile);
if (!data) {
if (!data || !data.items) {
list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
return;
}
allItems = data;
allItems = data.items;
filters = data.filters;
search.setItems(allItems.map(item => ({
...item,
title: item.name,
searchText: `${item.name} ${item.description} ${item.tags?.join(' ') || ''}`.toLowerCase()
})));
renderItems(allItems);
countEl.textContent = `${allItems.length} collections`;
searchInput.addEventListener('input', debounce((e) => {
const query = e.target.value;
const results = query ? search.search(query) : allItems;
renderItems(results, query);
countEl.textContent = `${results.length} of ${allItems.length} collections`;
}, 200));
// Initialize multi-select filter
tagSelect = new MultiSelect('#filter-tag', {
placeholder: 'All Tags',
onChange: (selected) => {
currentFilters.tags = selected;
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();
}
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 = '') {
const list = document.getElementById('resource-list');
@@ -140,7 +233,7 @@
list.innerHTML = `
<div class="empty-state">
<h3>No collections found</h3>
<p>Try a different search term</p>
<p>Try a different search term or adjust filters</p>
</div>
`;
return;
@@ -150,11 +243,11 @@
<div class="resource-item" onclick="openFileModal('${item.path}', '${resourceType}')">
<div class="resource-info">
<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 class="resource-description">${escapeHtml(item.description || 'No description')}</div>
<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>
</div>
${item.items?.length ? `

View File

@@ -7,6 +7,7 @@
<meta name="description" content="Coding standards and best practices for GitHub Copilot">
<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>">
<script src="../js/theme.js"></script>
</head>
<body>
<header class="site-header">
@@ -25,11 +26,21 @@
<a href="tools.html">Tools</a>
<a href="samples.html">Samples</a>
</nav>
<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 class="header-actions">
<button id="theme-toggle" class="theme-toggle" title="Toggle theme">
<svg class="icon-sun" viewBox="0 0 16 16" fill="currentColor">
<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"/>
</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>
</header>
@@ -47,6 +58,16 @@
<div class="search-bar">
<input type="text" id="search-input" placeholder="Search instructions..." autocomplete="off">
</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="resource-list" id="resource-list">
<div class="loading">Loading instructions...</div>
@@ -100,39 +121,91 @@
<script src="../js/utils.js"></script>
<script src="../js/search.js"></script>
<script src="../js/multi-select.js"></script>
<script src="../js/app.js"></script>
<script>
const resourceType = 'instruction';
const dataFile = 'instructions.json';
let allItems = [];
let filters = { extensions: [], patterns: [] };
let search = new FuzzySearch();
let extensionSelect;
// Current filter state
let currentFilters = {
extensions: [],
};
async function initPage() {
const list = document.getElementById('resource-list');
const countEl = document.getElementById('results-count');
const searchInput = document.getElementById('search-input');
const clearFiltersBtn = document.getElementById('clear-filters');
const data = await fetchData(dataFile);
if (!data) {
if (!data || !data.items) {
list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
return;
}
allItems = data;
allItems = data.items;
filters = data.filters;
search.setItems(allItems);
renderItems(allItems);
countEl.textContent = `${allItems.length} instructions`;
searchInput.addEventListener('input', debounce((e) => {
const query = e.target.value;
const results = query ? search.search(query) : allItems;
renderItems(results, query);
countEl.textContent = `${results.length} of ${allItems.length} instructions`;
}, 200));
// Initialize multi-select filter
extensionSelect = new MultiSelect('#filter-extension', {
placeholder: 'All Extensions',
onChange: (selected) => {
currentFilters.extensions = selected;
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();
}
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 = '') {
const list = document.getElementById('resource-list');
@@ -140,7 +213,7 @@
list.innerHTML = `
<div class="empty-state">
<h3>No instructions found</h3>
<p>Try a different search term</p>
<p>Try a different search term or adjust filters</p>
</div>
`;
return;
@@ -151,11 +224,9 @@
<div class="resource-info">
<div class="resource-title">${query ? search.highlight(item.title, query) : escapeHtml(item.title)}</div>
<div class="resource-description">${escapeHtml(item.description || 'No description')}</div>
${item.applyTo ? `
<div class="resource-meta">
<span class="resource-tag">Applies to: ${escapeHtml(item.applyTo)}</span>
</div>
` : ''}
<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>'}
</div>
</div>
<div class="resource-actions">
<a href="${getVSCodeInstallUrl('instructions', item.path)}" class="btn btn-primary" onclick="event.stopPropagation()">

View File

@@ -7,6 +7,7 @@
<meta name="description" content="Ready-to-use prompt templates for development tasks with GitHub Copilot">
<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>">
<script src="../js/theme.js"></script>
</head>
<body>
<header class="site-header">
@@ -25,11 +26,21 @@
<a href="tools.html">Tools</a>
<a href="samples.html">Samples</a>
</nav>
<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 class="header-actions">
<button id="theme-toggle" class="theme-toggle" title="Toggle theme">
<svg class="icon-sun" viewBox="0 0 16 16" fill="currentColor">
<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"/>
</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>
</header>
@@ -47,6 +58,16 @@
<div class="search-bar">
<input type="text" id="search-input" placeholder="Search prompts..." autocomplete="off">
</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="resource-list" id="resource-list">
<div class="loading">Loading prompts...</div>
@@ -100,39 +121,88 @@
<script src="../js/utils.js"></script>
<script src="../js/search.js"></script>
<script src="../js/multi-select.js"></script>
<script src="../js/app.js"></script>
<script>
const resourceType = 'prompt';
const dataFile = 'prompts.json';
let allItems = [];
let filters = { tools: [] };
let search = new FuzzySearch();
let toolSelect;
// Current filter state
let currentFilters = {
tools: [],
};
async function initPage() {
const list = document.getElementById('resource-list');
const countEl = document.getElementById('results-count');
const searchInput = document.getElementById('search-input');
const clearFiltersBtn = document.getElementById('clear-filters');
const data = await fetchData(dataFile);
if (!data) {
if (!data || !data.items) {
list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
return;
}
allItems = data;
allItems = data.items;
filters = data.filters;
search.setItems(allItems);
renderItems(allItems);
countEl.textContent = `${allItems.length} prompts`;
searchInput.addEventListener('input', debounce((e) => {
const query = e.target.value;
const results = query ? search.search(query) : allItems;
renderItems(results, query);
countEl.textContent = `${results.length} of ${allItems.length} prompts`;
}, 200));
// Initialize multi-select filter
toolSelect = new MultiSelect('#filter-tool', {
placeholder: 'All Tools',
onChange: (selected) => {
currentFilters.tools = selected;
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();
}
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 = '') {
const list = document.getElementById('resource-list');
@@ -140,7 +210,7 @@
list.innerHTML = `
<div class="empty-state">
<h3>No prompts found</h3>
<p>Try a different search term</p>
<p>Try a different search term or adjust filters</p>
</div>
`;
return;
@@ -152,8 +222,8 @@
<div class="resource-title">${query ? search.highlight(item.title, query) : escapeHtml(item.title)}</div>
<div class="resource-description">${escapeHtml(item.description || 'No description')}</div>
<div class="resource-meta">
${item.model ? `<span class="resource-tag">Model: ${escapeHtml(item.model)}</span>` : ''}
${item.agent ? `<span class="resource-tag">Agent: ${escapeHtml(item.agent)}</span>` : ''}
${item.model ? `<span class="resource-tag tag-model">${escapeHtml(item.model)}</span>` : ''}
${item.tools?.length ? `<span class="resource-tag">${item.tools.length} tools</span>` : ''}
</div>
</div>
<div class="resource-actions">

View File

@@ -7,6 +7,7 @@
<meta name="description" content="Code samples and examples for building with GitHub Copilot">
<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>">
<script src="../js/theme.js"></script>
</head>
<body>
<header class="site-header">
@@ -25,11 +26,21 @@
<a href="tools.html">Tools</a>
<a href="samples.html" class="active">Samples</a>
</nav>
<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 class="header-actions">
<button id="theme-toggle" class="theme-toggle" title="Toggle theme">
<svg class="icon-sun" viewBox="0 0 16 16" fill="currentColor">
<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"/>
</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>
</header>

View File

@@ -7,6 +7,7 @@
<meta name="description" content="Self-contained agent skills with instructions and bundled resources">
<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>">
<script src="../js/theme.js"></script>
</head>
<body>
<header class="site-header">
@@ -25,11 +26,21 @@
<a href="tools.html">Tools</a>
<a href="samples.html">Samples</a>
</nav>
<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 class="header-actions">
<button id="theme-toggle" class="theme-toggle" title="Toggle theme">
<svg class="icon-sun" viewBox="0 0 16 16" fill="currentColor">
<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"/>
</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>
</header>
@@ -47,6 +58,22 @@
<div class="search-bar">
<input type="text" id="search-input" placeholder="Search skills..." autocomplete="off">
</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="resource-list" id="resource-list">
<div class="loading">Loading skills...</div>
@@ -99,39 +126,104 @@
<script src="../js/utils.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>
const resourceType = 'skill';
const dataFile = 'skills.json';
let allItems = [];
let filters = { categories: [], hasAssets: [] };
let search = new FuzzySearch();
let categorySelect;
// Current filter state
let currentFilters = {
categories: [],
hasAssets: false,
};
async function initPage() {
const list = document.getElementById('resource-list');
const countEl = document.getElementById('results-count');
const searchInput = document.getElementById('search-input');
const hasAssetsCheckbox = document.getElementById('filter-has-assets');
const clearFiltersBtn = document.getElementById('clear-filters');
const data = await fetchData(dataFile);
if (!data) {
if (!data || !data.items) {
list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
return;
}
allItems = data;
allItems = data.items;
filters = data.filters;
search.setItems(allItems);
renderItems(allItems);
countEl.textContent = `${allItems.length} skills`;
searchInput.addEventListener('input', debounce((e) => {
const query = e.target.value;
const results = query ? search.search(query) : allItems;
renderItems(results, query);
countEl.textContent = `${results.length} of ${allItems.length} skills`;
}, 200));
// Initialize multi-select filter
categorySelect = new MultiSelect('#filter-category', {
placeholder: 'All Categories',
onChange: (selected) => {
currentFilters.categories = selected;
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();
}
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 = '') {
const list = document.getElementById('resource-list');
@@ -139,7 +231,7 @@
list.innerHTML = `
<div class="empty-state">
<h3>No skills found</h3>
<p>Try a different search term</p>
<p>Try a different search term or adjust filters</p>
</div>
`;
return;
@@ -150,13 +242,20 @@
<div class="resource-info">
<div class="resource-title">${query ? search.highlight(item.title, query) : escapeHtml(item.title)}</div>
<div class="resource-description">${escapeHtml(item.description || 'No description')}</div>
${item.assets?.length ? `
<div class="resource-meta">
<span class="resource-tag">${item.assets.length} bundled asset${item.assets.length === 1 ? '' : 's'}</span>
</div>
` : ''}
<div class="resource-meta">
<span class="resource-tag tag-category">${escapeHtml(item.category)}</span>
${item.hasAssets ? `<span class="resource-tag tag-assets">${item.assetCount} asset${item.assetCount === 1 ? '' : 's'}</span>` : ''}
<span class="resource-tag">${item.files.length} file${item.files.length === 1 ? '' : 's'}</span>
</div>
</div>
<div class="resource-actions">
<button class="btn btn-primary" 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()">
View Folder
</a>
@@ -165,6 +264,103 @@
`).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);
</script>
</body>

View File

@@ -7,6 +7,7 @@
<meta name="description" content="MCP servers and developer tools for GitHub Copilot">
<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>">
<script src="../js/theme.js"></script>
</head>
<body>
<header class="site-header">
@@ -25,11 +26,21 @@
<a href="tools.html" class="active">Tools</a>
<a href="samples.html">Samples</a>
</nav>
<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 class="header-actions">
<button id="theme-toggle" class="theme-toggle" title="Toggle theme">
<svg class="icon-sun" viewBox="0 0 16 16" fill="currentColor">
<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"/>
</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>
</header>