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

@@ -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()">