feat(website): add comprehensive accessibility improvements

Phase 1 - Screen Reader Critical:
- Add aria-label to main navigation
- Add accessible names to icon-only buttons (GitHub, theme toggle, close)
- Add aria-hidden to decorative SVGs and emoji icons
- Add role=dialog, aria-modal, aria-labelledby to modal
- Add skip link with visible focus state

Phase 2 - Keyboard Navigation:
- Implement focus trap in modal (Tab/Shift+Tab cycles)
- Return focus to trigger element on modal close
- Replace outline:none with visible focus rings
- Add keyboard navigation to install dropdown (arrows, escape)
- Add aria-expanded to dropdown toggles

Phase 3 - Dynamic Content:
- Add aria-live=polite to results counts and loading states
- Add role=listbox to search results
- Add role=list to resource lists
- Add role=menu/menuitem to dropdown menus

Phase 4 - Forms & Labels:
- Add .sr-only utility class for screen reader text
- Add visually hidden labels to all search inputs
- Add aria-label to filter dropdowns

Files modified:
- BaseLayout.astro, Modal.astro (ARIA attributes)
- modal.ts (focus trap, keyboard navigation)
- global.css (sr-only, skip-link, focus styles)
- All page files (labels, live regions, roles)
This commit is contained in:
Aaron Powell
2026-02-02 11:52:31 +11:00
parent f99c99a5d4
commit 94a395dbe0
13 changed files with 272 additions and 92 deletions

View File

@@ -1,5 +1,5 @@
{
"generated": "2026-02-01T23:32:16.397Z",
"generated": "2026-02-01T23:35:23.497Z",
"counts": {
"agents": 140,
"prompts": 134,

View File

@@ -110,6 +110,39 @@ body {
min-height: 100vh;
}
/* Accessibility Utilities */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.skip-link {
position: absolute;
top: -100%;
left: 50%;
transform: translateX(-50%);
background: var(--color-accent);
color: var(--color-text-emphasis);
padding: 12px 24px;
border-radius: var(--border-radius);
font-weight: 600;
z-index: 10000;
transition: top var(--transition);
}
.skip-link:focus {
top: 12px;
outline: 2px solid var(--color-text-emphasis);
outline-offset: 2px;
}
.container {
max-width: var(--container-width);
margin: 0 auto;
@@ -348,7 +381,8 @@ a:hover {
}
.hero-search input:focus {
outline: none;
outline: 2px solid var(--color-accent);
outline-offset: 2px;
border-color: var(--color-accent);
box-shadow: var(--shadow-glow);
}
@@ -651,6 +685,20 @@ a:hover {
text-decoration: none;
}
.btn:focus {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
.btn:focus:not(:focus-visible) {
outline: none;
}
.btn:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
.btn-primary {
background: var(--gradient-primary);
color: white;
@@ -1024,7 +1072,8 @@ a:hover {
}
.search-bar input:focus {
outline: none;
outline: 2px solid var(--color-accent);
outline-offset: 2px;
border-color: var(--color-accent);
box-shadow: var(--shadow-glow);
}
@@ -1068,7 +1117,8 @@ a:hover {
}
.filter-group select:focus {
outline: none;
outline: 2px solid var(--color-accent);
outline-offset: 2px;
border-color: var(--color-accent);
}

View File

@@ -2,50 +2,50 @@
// Modal component for viewing file contents
---
<div id="file-modal" class="modal hidden">
<div id="file-modal" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div class="modal-content">
<div class="modal-header">
<h3 id="modal-title">File</h3>
<div class="modal-actions">
<button id="copy-btn" class="btn btn-secondary" title="Copy to clipboard">
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
<button id="copy-btn" class="btn btn-secondary" aria-label="Copy to clipboard">
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true">
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/>
<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/>
</svg>
Copy
<span aria-hidden="true">Copy</span>
</button>
<button id="download-btn" class="btn btn-secondary" title="Download file">
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
<button id="download-btn" class="btn btn-secondary" aria-label="Download file">
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true">
<path d="M7.47 10.78a.75.75 0 0 0 1.06 0l3.75-3.75a.75.75 0 0 0-1.06-1.06L8.75 8.44V1.75a.75.75 0 0 0-1.5 0v6.69L4.78 5.97a.75.75 0 0 0-1.06 1.06l3.75 3.75ZM3.75 13a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z"/>
</svg>
Download
<span aria-hidden="true">Download</span>
</button>
<button id="share-btn" class="btn btn-secondary" title="Copy link to clipboard">
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
<button id="share-btn" class="btn btn-secondary" aria-label="Copy link to clipboard">
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true">
<path d="M7.775 3.275a.75.75 0 0 0 1.06 1.06l1.25-1.25a2 2 0 1 1 2.83 2.83l-2.5 2.5a2 2 0 0 1-2.83 0 .75.75 0 0 0-1.06 1.06 3.5 3.5 0 0 0 4.95 0l2.5-2.5a3.5 3.5 0 0 0-4.95-4.95l-1.25 1.25zm-.025 5.45a.75.75 0 0 0-1.06-1.06l-1.25 1.25a2 2 0 1 1-2.83-2.83l2.5-2.5a2 2 0 0 1 2.83 0 .75.75 0 0 0 1.06-1.06 3.5 3.5 0 0 0-4.95 0l-2.5 2.5a3.5 3.5 0 0 0 4.95 4.95l1.25-1.25z"/>
</svg>
Share
<span aria-hidden="true">Share</span>
</button>
<div id="install-dropdown" class="install-dropdown" style="display: none;">
<a id="install-btn-main" class="btn btn-primary install-btn-main" target="_blank" rel="noopener">
Install
</a>
<button type="button" class="btn btn-primary install-btn-toggle" aria-label="Install options">
<svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor">
<button type="button" class="btn btn-primary install-btn-toggle" aria-label="Install options" aria-expanded="false" aria-haspopup="true">
<svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor" aria-hidden="true">
<path d="M4.427 7.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 7H4.604a.25.25 0 00-.177.427z"/>
</svg>
</button>
<div class="install-dropdown-menu">
<a id="install-vscode" target="_blank" rel="noopener">
<div class="install-dropdown-menu" role="menu">
<a id="install-vscode" target="_blank" rel="noopener" role="menuitem">
VS Code
</a>
<a id="install-insiders" target="_blank" rel="noopener">
<a id="install-insiders" target="_blank" rel="noopener" role="menuitem">
VS Code Insiders
</a>
</div>
</div>
<button id="close-modal" class="btn btn-icon" title="Close">
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
<button id="close-modal" class="btn btn-icon" aria-label="Close dialog">
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true">
<path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"></path>
</svg>
</button>

View File

@@ -28,15 +28,16 @@ const base = import.meta.env.BASE_URL;
</script>
</head>
<body data-base-path={base}>
<a href="#main-content" class="skip-link">Skip to main content</a>
<header class="site-header">
<div class="container">
<div class="header-content">
<a href={base} class="logo">
<img src={`${base}images/Copilot_Icon_White.svg`} alt="GitHub Copilot" class="logo-icon logo-icon-dark" width="32" height="32" />
<img src={`${base}images/Copilot_Icon_Black.svg`} alt="GitHub Copilot" class="logo-icon logo-icon-light" width="32" height="32" />
<img src={`${base}images/Copilot_Icon_White.svg`} alt="" class="logo-icon logo-icon-dark" width="32" height="32" aria-hidden="true" />
<img src={`${base}images/Copilot_Icon_Black.svg`} alt="" class="logo-icon logo-icon-light" width="32" height="32" aria-hidden="true" />
<span class="logo-text">Awesome Copilot</span>
</a>
<nav class="main-nav">
<nav class="main-nav" aria-label="Main navigation">
<a href={`${base}agents/`} class:list={[{ active: activeNav === 'agents' }]}>Agents</a>
<a href={`${base}prompts/`} class:list={[{ active: activeNav === 'prompts' }]}>Prompts</a>
<a href={`${base}instructions/`} class:list={[{ active: activeNav === 'instructions' }]}>Instructions</a>
@@ -46,16 +47,16 @@ const base = import.meta.env.BASE_URL;
<a href={`${base}samples/`} class:list={[{ active: activeNav === 'samples' }]}>Samples</a>
</nav>
<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">
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
<svg class="icon-sun" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0V.75A.75.75 0 0 1 8 0zm0 13a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 8 13zM2.343 2.343a.75.75 0 0 1 1.061 0l1.06 1.061a.75.75 0 0 1-1.06 1.06l-1.06-1.06a.75.75 0 0 1 0-1.06zm9.193 9.193a.75.75 0 0 1 1.06 0l1.061 1.06a.75.75 0 0 1-1.06 1.061l-1.061-1.06a.75.75 0 0 1 0-1.061zM0 8a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 8zm13 0a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 0 1.5h-1.5A.75.75 0 0 1 13 8zM2.343 13.657a.75.75 0 0 1 0-1.061l1.06-1.06a.75.75 0 0 1 1.061 1.06l-1.06 1.06a.75.75 0 0 1-1.061 0zm9.193-9.193a.75.75 0 0 1 0-1.06l1.061-1.061a.75.75 0 0 1 1.06 1.06l-1.06 1.061a.75.75 0 0 1-1.061 0z"/>
</svg>
<svg class="icon-moon" viewBox="0 0 16 16" fill="currentColor">
<svg class="icon-moon" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M9.598 1.591a.75.75 0 0 1 .785-.175 7 7 0 1 1-8.967 8.967.75.75 0 0 1 .961-.96 5.5 5.5 0 0 0 7.046-7.046.75.75 0 0 1 .175-.786zm1.616 1.945a7 7 0 0 1-7.678 7.678 5.5 5.5 0 1 0 7.678-7.678z"/>
</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">
<a href="https://github.com/github/awesome-copilot" class="github-link" target="_blank" rel="noopener" aria-label="View on GitHub">
<svg viewBox="0 0 16 16" width="24" height="24" fill="currentColor" aria-hidden="true">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path>
</svg>
</a>

View File

@@ -4,7 +4,7 @@ import Modal from '../components/Modal.astro';
---
<BaseLayout title="Agents" description="Specialized agents that enhance GitHub Copilot for specific technologies, workflows, and domains" activeNav="agents">
<main>
<main id="main-content">
<div class="page-header">
<div class="container">
<h1>🤖 Custom Agents</h1>
@@ -15,6 +15,7 @@ import Modal from '../components/Modal.astro';
<div class="page-content">
<div class="container">
<div class="search-bar">
<label for="search-input" class="sr-only">Search agents</label>
<input type="text" id="search-input" placeholder="Search agents..." autocomplete="off">
</div>
@@ -22,11 +23,11 @@ import Modal from '../components/Modal.astro';
<div class="filters-bar" id="filters-bar">
<div class="filter-group">
<label for="filter-model">Model:</label>
<select id="filter-model" multiple></select>
<select id="filter-model" multiple aria-label="Filter by model"></select>
</div>
<div class="filter-group">
<label for="filter-tool">Tool:</label>
<select id="filter-tool" multiple></select>
<select id="filter-tool" multiple aria-label="Filter by tool"></select>
</div>
<div class="filter-group">
<label class="checkbox-label">
@@ -37,9 +38,9 @@ import Modal from '../components/Modal.astro';
<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>
<div class="results-count" id="results-count" aria-live="polite"></div>
<div class="resource-list" id="resource-list" role="list">
<div class="loading" aria-live="polite">Loading agents...</div>
</div>
</div>
</div>

View File

@@ -4,7 +4,7 @@ import Modal from '../components/Modal.astro';
---
<BaseLayout title="Collections" description="Curated collections of prompts, instructions, and agents for specific workflows" activeNav="collections">
<main>
<main id="main-content">
<div class="page-header">
<div class="container">
<h1>📦 Collections</h1>
@@ -15,13 +15,14 @@ import Modal from '../components/Modal.astro';
<div class="page-content">
<div class="container">
<div class="search-bar">
<label for="search-input" class="sr-only">Search collections</label>
<input type="text" id="search-input" placeholder="Search collections..." autocomplete="off">
</div>
<div class="filters-bar" id="filters-bar">
<div class="filter-group">
<label for="filter-tag">Tag:</label>
<select id="filter-tag" multiple></select>
<select id="filter-tag" multiple aria-label="Filter by tag"></select>
</div>
<div class="filter-group">
<label class="checkbox-label">
@@ -32,9 +33,9 @@ import Modal from '../components/Modal.astro';
<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>
<div class="results-count" id="results-count" aria-live="polite"></div>
<div class="resource-list" id="resource-list" role="list">
<div class="loading" aria-live="polite">Loading collections...</div>
</div>
</div>
</div>

View File

@@ -6,102 +6,104 @@ const base = import.meta.env.BASE_URL;
---
<BaseLayout title="Home" activeNav="">
<main>
<main id="main-content">
<!-- Hero Section -->
<section class="hero">
<section class="hero" aria-labelledby="hero-heading">
<div class="container">
<h1>Awesome GitHub Copilot</h1>
<h1 id="hero-heading">Awesome GitHub Copilot</h1>
<p class="hero-subtitle">Community-contributed instructions, prompts, agents, and skills to enhance your GitHub Copilot experience</p>
<div class="hero-search">
<input type="text" id="global-search" placeholder="Search all resources..." autocomplete="off">
<div id="search-results" class="search-results hidden"></div>
<label for="global-search" class="sr-only">Search all resources</label>
<input type="text" id="global-search" placeholder="Search all resources..." autocomplete="off" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="search-results">
<div id="search-results" class="search-results hidden" role="listbox" aria-label="Search results"></div>
</div>
</div>
</section>
<!-- Quick Links -->
<section class="quick-links">
<section class="quick-links" aria-labelledby="quick-links-heading">
<h2 id="quick-links-heading" class="sr-only">Browse Resources</h2>
<div class="container">
<div class="cards-grid">
<a href={`${base}agents/`} class="card card-with-count" id="card-agents">
<div class="card-icon">🤖</div>
<div class="card-icon" aria-hidden="true">🤖</div>
<div class="card-content">
<h3>Agents</h3>
<p>Custom agents for specialized Copilot experiences</p>
</div>
<div class="card-count" data-count="agents">-</div>
<div class="card-count" data-count="agents" aria-label="Agent count">-</div>
</a>
<a href={`${base}prompts/`} class="card card-with-count" id="card-prompts">
<div class="card-icon">🎯</div>
<div class="card-icon" aria-hidden="true">🎯</div>
<div class="card-content">
<h3>Prompts</h3>
<p>Ready-to-use prompt templates for development tasks</p>
</div>
<div class="card-count" data-count="prompts">-</div>
<div class="card-count" data-count="prompts" aria-label="Prompt count">-</div>
</a>
<a href={`${base}instructions/`} class="card card-with-count" id="card-instructions">
<div class="card-icon">📋</div>
<div class="card-icon" aria-hidden="true">📋</div>
<div class="card-content">
<h3>Instructions</h3>
<p>Coding standards and best practices for Copilot</p>
</div>
<div class="card-count" data-count="instructions">-</div>
<div class="card-count" data-count="instructions" aria-label="Instruction count">-</div>
</a>
<a href={`${base}skills/`} class="card card-with-count" id="card-skills">
<div class="card-icon">⚡</div>
<div class="card-icon" aria-hidden="true">⚡</div>
<div class="card-content">
<h3>Skills</h3>
<p>Self-contained folders with instructions and resources</p>
</div>
<div class="card-count" data-count="skills">-</div>
<div class="card-count" data-count="skills" aria-label="Skill count">-</div>
</a>
<a href={`${base}collections/`} class="card card-with-count" id="card-collections">
<div class="card-icon">📦</div>
<div class="card-icon" aria-hidden="true">📦</div>
<div class="card-content">
<h3>Collections</h3>
<p>Curated collections organized by themes</p>
</div>
<div class="card-count" data-count="collections">-</div>
<div class="card-count" data-count="collections" aria-label="Collection count">-</div>
</a>
<a href={`${base}tools/`} class="card card-with-count" id="card-tools">
<div class="card-icon">🔧</div>
<div class="card-icon" aria-hidden="true">🔧</div>
<div class="card-content">
<h3>Tools</h3>
<p>MCP servers and developer tools</p>
</div>
<div class="card-count" data-count="tools">-</div>
<div class="card-count" data-count="tools" aria-label="Tool count">-</div>
</a>
</div>
</div>
</section>
<!-- Featured Collections -->
<section class="featured">
<section class="featured" aria-labelledby="featured-heading">
<div class="container">
<h2>Featured Collections</h2>
<div id="featured-collections" class="cards-grid">
<h2 id="featured-heading">Featured Collections</h2>
<div id="featured-collections" class="cards-grid" aria-live="polite">
<!-- Populated by JS -->
</div>
</div>
</section>
<!-- Getting Started -->
<section class="getting-started">
<section class="getting-started" aria-labelledby="getting-started-heading">
<div class="container">
<h2>Getting Started</h2>
<h2 id="getting-started-heading">Getting Started</h2>
<div class="steps">
<div class="step">
<div class="step-number">1</div>
<div class="step-number" aria-hidden="true">1</div>
<h3>Browse</h3>
<p>Explore agents, prompts, instructions, and skills</p>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-number" aria-hidden="true">2</div>
<h3>Preview</h3>
<p>Click any item to view its full content</p>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-number" aria-hidden="true">3</div>
<h3>Install</h3>
<p>One-click install to VS Code or copy to clipboard</p>
</div>

View File

@@ -4,7 +4,7 @@ import Modal from '../components/Modal.astro';
---
<BaseLayout title="Instructions" description="Coding standards and best practices for GitHub Copilot" activeNav="instructions">
<main>
<main id="main-content">
<div class="page-header">
<div class="container">
<h1>📋 Instructions</h1>
@@ -15,20 +15,21 @@ import Modal from '../components/Modal.astro';
<div class="page-content">
<div class="container">
<div class="search-bar">
<label for="search-input" class="sr-only">Search instructions</label>
<input type="text" id="search-input" placeholder="Search instructions..." autocomplete="off">
</div>
<div class="filters-bar" id="filters-bar">
<div class="filter-group">
<label for="filter-extension">File Extension:</label>
<select id="filter-extension" multiple></select>
<select id="filter-extension" multiple aria-label="Filter by file extension"></select>
</div>
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
</div>
<div class="results-count" id="results-count"></div>
<div class="resource-list" id="resource-list">
<div class="loading">Loading instructions...</div>
<div class="results-count" id="results-count" aria-live="polite"></div>
<div class="resource-list" id="resource-list" role="list">
<div class="loading" aria-live="polite">Loading instructions...</div>
</div>
</div>
</div>

View File

@@ -4,7 +4,7 @@ import Modal from '../components/Modal.astro';
---
<BaseLayout title="Prompts" description="Ready-to-use prompt templates for development tasks with GitHub Copilot" activeNav="prompts">
<main>
<main id="main-content">
<div class="page-header">
<div class="container">
<h1>🎯 Prompts</h1>
@@ -15,20 +15,21 @@ import Modal from '../components/Modal.astro';
<div class="page-content">
<div class="container">
<div class="search-bar">
<label for="search-input" class="sr-only">Search prompts</label>
<input type="text" id="search-input" placeholder="Search prompts..." autocomplete="off">
</div>
<div class="filters-bar" id="filters-bar">
<div class="filter-group">
<label for="filter-tool">Tool:</label>
<select id="filter-tool" multiple></select>
<select id="filter-tool" multiple aria-label="Filter by tool"></select>
</div>
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
</div>
<div class="results-count" id="results-count"></div>
<div class="resource-list" id="resource-list">
<div class="loading">Loading prompts...</div>
<div class="results-count" id="results-count" aria-live="polite"></div>
<div class="resource-list" id="resource-list" role="list">
<div class="loading" aria-live="polite">Loading prompts...</div>
</div>
</div>
</div>

View File

@@ -5,7 +5,7 @@ const base = import.meta.env.BASE_URL;
---
<BaseLayout title="Samples" description="Code samples and examples for building with GitHub Copilot" activeNav="samples">
<main>
<main id="main-content">
<div class="page-header">
<div class="container">
<h1>📚 Samples</h1>
@@ -16,7 +16,7 @@ const base = import.meta.env.BASE_URL;
<div class="page-content">
<div class="container">
<div class="coming-soon">
<div class="coming-soon-icon">🚧</div>
<div class="coming-soon-icon" aria-hidden="true">🚧</div>
<h2>Coming Soon</h2>
<p>We're migrating code samples from the <a href="https://github.com/github/copilot-sdk/tree/main/cookbook" target="_blank" rel="noopener">Copilot SDK Cookbook</a> to this repository.</p>
<p>Check back soon for examples including:</p>

View File

@@ -4,7 +4,7 @@ import Modal from '../components/Modal.astro';
---
<BaseLayout title="Skills" description="Self-contained agent skills with instructions and bundled resources" activeNav="skills">
<main>
<main id="main-content">
<div class="page-header">
<div class="container">
<h1>⚡ Skills</h1>
@@ -15,13 +15,14 @@ import Modal from '../components/Modal.astro';
<div class="page-content">
<div class="container">
<div class="search-bar">
<label for="search-input" class="sr-only">Search skills</label>
<input type="text" id="search-input" placeholder="Search skills..." autocomplete="off">
</div>
<div class="filters-bar" id="filters-bar">
<div class="filter-group">
<label for="filter-category">Category:</label>
<select id="filter-category" multiple></select>
<select id="filter-category" multiple aria-label="Filter by category"></select>
</div>
<div class="filter-group">
<label class="checkbox-label">
@@ -32,9 +33,9 @@ import Modal from '../components/Modal.astro';
<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>
<div class="results-count" id="results-count" aria-live="polite"></div>
<div class="resource-list" id="resource-list" role="list">
<div class="loading" aria-live="polite">Loading skills...</div>
</div>
</div>
</div>

View File

@@ -7,7 +7,7 @@ import BaseLayout from "../layouts/BaseLayout.astro";
description="MCP servers and developer tools for GitHub Copilot"
activeNav="tools"
>
<main>
<main id="main-content">
<div class="page-header">
<div class="container">
<h1>🔧 Tools</h1>
@@ -19,6 +19,7 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<div class="container">
<div class="search-section">
<div class="search-bar">
<label for="search-input" class="sr-only">Search tools</label>
<input
type="text"
id="search-input"
@@ -27,17 +28,18 @@ import BaseLayout from "../layouts/BaseLayout.astro";
/>
</div>
<div class="filters">
<select id="filter-category" class="filter-select">
<label for="filter-category" class="sr-only">Filter by category</label>
<select id="filter-category" class="filter-select" aria-label="Filter by category">
<option value="">All Categories</option>
</select>
<button id="clear-filters" class="btn btn-secondary btn-small"
>Clear</button
>
</div>
<div id="results-count" class="results-count"></div>
<div id="results-count" class="results-count" aria-live="polite"></div>
</div>
<div id="tools-list"></div>
<div id="tools-list" role="list"></div>
<div class="coming-soon">
<h2>More Tools Coming Soon</h2>

View File

@@ -8,6 +8,7 @@ import { fetchFileContent, fetchData, getVSCodeInstallUrl, copyToClipboard, show
let currentFilePath: string | null = null;
let currentFileContent: string | null = null;
let currentFileType: string | null = null;
let triggerElement: HTMLElement | null = null;
// Collection data cache
interface CollectionItem {
@@ -31,6 +32,50 @@ interface CollectionsData {
let collectionsCache: CollectionsData | null = null;
/**
* Get all focusable elements within a container
*/
function getFocusableElements(container: HTMLElement): HTMLElement[] {
const focusableSelectors = [
'button:not([disabled])',
'a[href]',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])'
].join(', ');
return Array.from(container.querySelectorAll<HTMLElement>(focusableSelectors))
.filter(el => el.offsetParent !== null); // Filter out hidden elements
}
/**
* Handle keyboard navigation within modal (focus trap)
*/
function handleModalKeydown(e: KeyboardEvent, modal: HTMLElement): void {
if (e.key === 'Tab') {
const focusableElements = getFocusableElements(modal);
if (focusableElements.length === 0) return;
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (e.shiftKey) {
// Shift+Tab: if on first element, wrap to last
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
// Tab: if on last element, wrap to first
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
}
}
/**
* Setup modal functionality
*/
@@ -50,8 +95,12 @@ export function setupModal(): void {
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !modal.classList.contains('hidden')) {
closeModal();
if (!modal.classList.contains('hidden')) {
if (e.key === 'Escape') {
closeModal();
} else {
handleModalKeydown(e, modal);
}
}
});
@@ -129,18 +178,72 @@ export function setupInstallDropdown(containerId: string): void {
const container = document.getElementById(containerId);
if (!container) return;
const toggle = container.querySelector('.install-btn-toggle');
const toggle = container.querySelector<HTMLButtonElement>('.install-btn-toggle');
const menu = container.querySelector('.install-dropdown-menu');
const menuItems = container.querySelectorAll<HTMLAnchorElement>('.install-dropdown-menu a');
toggle?.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
container.classList.toggle('open');
const isOpen = container.classList.toggle('open');
toggle.setAttribute('aria-expanded', String(isOpen));
// Focus first menu item when opening
if (isOpen && menuItems.length > 0) {
menuItems[0].focus();
}
});
// Keyboard navigation for dropdown
toggle?.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
container.classList.add('open');
toggle.setAttribute('aria-expanded', 'true');
if (menuItems.length > 0) {
menuItems[0].focus();
}
}
});
// Keyboard navigation within menu
menuItems.forEach((item, index) => {
item.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if (index < menuItems.length - 1) {
menuItems[index + 1].focus();
}
break;
case 'ArrowUp':
e.preventDefault();
if (index > 0) {
menuItems[index - 1].focus();
} else {
toggle?.focus();
}
break;
case 'Escape':
e.preventDefault();
container.classList.remove('open');
toggle?.setAttribute('aria-expanded', 'false');
toggle?.focus();
break;
case 'Tab':
// Close menu on tab out
container.classList.remove('open');
toggle?.setAttribute('aria-expanded', 'false');
break;
}
});
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!container.contains(e.target as Node)) {
container.classList.remove('open');
toggle?.setAttribute('aria-expanded', 'false');
}
});
@@ -148,6 +251,7 @@ export function setupInstallDropdown(containerId: string): void {
container.querySelectorAll('.install-dropdown-menu a').forEach(link => {
link.addEventListener('click', () => {
container.classList.remove('open');
toggle?.setAttribute('aria-expanded', 'false');
});
});
}
@@ -157,8 +261,9 @@ export function setupInstallDropdown(containerId: string): void {
* @param filePath - Path to the file
* @param type - Resource type (agent, prompt, instruction, etc.)
* @param updateUrl - Whether to update the URL hash (default: true)
* @param trigger - The element that triggered the modal (for focus return)
*/
export async function openFileModal(filePath: string, type: string, updateUrl = true): Promise<void> {
export async function openFileModal(filePath: string, type: string, updateUrl = true, trigger?: HTMLElement): Promise<void> {
const modal = document.getElementById('file-modal');
const title = document.getElementById('modal-title');
const modalContent = document.getElementById('modal-content');
@@ -169,12 +274,16 @@ export async function openFileModal(filePath: string, type: string, updateUrl =
const installInsiders = document.getElementById('install-insiders') as HTMLAnchorElement | null;
const copyBtn = document.getElementById('copy-btn');
const downloadBtn = document.getElementById('download-btn');
const closeBtn = document.getElementById('close-modal');
if (!modal || !title || !modalContent) return;
currentFilePath = filePath;
currentFileType = type;
// Track trigger element for focus return
triggerElement = trigger || document.activeElement as HTMLElement;
// Update URL for deep linking
if (updateUrl) {
updateHash(filePath);
@@ -183,6 +292,11 @@ export async function openFileModal(filePath: string, type: string, updateUrl =
// Show modal with loading state
title.textContent = filePath.split('/').pop() || filePath;
modal.classList.remove('hidden');
// Set focus to close button for accessibility
setTimeout(() => {
closeBtn?.focus();
}, 0);
// Handle collections differently - show as item list
if (type === 'collection') {
@@ -328,9 +442,15 @@ export function closeModal(updateUrl = true): void {
updateHash(null);
}
// Return focus to trigger element
if (triggerElement && typeof triggerElement.focus === 'function') {
triggerElement.focus();
}
currentFilePath = null;
currentFileContent = null;
currentFileType = null;
triggerElement = null;
}
/**