feat(ui): comprehensive UI/UX improvements (#1069)

* feat(ui): replace emoji icons with SVG icon system

Replace all emoji icons with a consistent SVG icon system to improve:
- Visual consistency across platforms
- Design token control and theming
- Professional appearance

Changes:
- Add new Icon.astro component with 16 custom SVG icons
- Update index.astro to use SVG icons in resource cards
- Update index.ts to render SVG icons in search results
- Update utils.ts to return icon names instead of emojis
- Update global.css with proper SVG icon styling
- Remove emoji from Footer component

Icons added: robot, document, lightning, hook, workflow, plug, wrench, book,
plus action icons: close, copy, download, share, external, plus, search, chevron-down

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(ui): enhance hero section, add animations and mobile responsiveness

Phase 2 & 3 UI/UX improvements:

Hero Section Enhancements:
- Add gradient text effect for title (purple to orange gradient)
- Add animated floating particles in hero background
- Increase hero padding for better visual impact

Card Category Colors:
- Add category-specific accent colors (AI, docs, power, automation, etc.)
- Each category has unique glow effect on hover
- Category colors: purple (AI), orange (docs), red (power), etc.

Entrance Animations:
- Add staggered fade-in animation for cards (50ms delay each)
- Cards animate in with translateY + opacity
- Add slide-down animation for search results

Mobile Responsiveness:
- Responsive grid: 4 cols → 2 cols → 1 col
- Adjust font sizes for mobile screens
- Add safe-area-inset support for notched devices
- Ensure touch targets ≥44px

Accessibility:
- Add prefers-reduced-motion support
- Disable animations for users who prefer reduced motion
- Smooth scroll with fallback

Additional Improvements:
- Add arrow indicator on card hover
- Add loading animation for count numbers
- Enhanced scrollbar styling
- Print styles for better printing

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(ui): add theme toggle, enhanced search, and back to top button

Theme Toggle:
- Create ThemeToggle.astro component with sun/moon icons
- Add theme initialization in Head.astro to prevent flash
- Store theme preference in localStorage
- Keyboard shortcut: Cmd/Ctrl + Shift + L
- Smooth icon transition animation

Back to Top Button:
- Create BackToTop.astro component
- Appears after scrolling 400px
- Smooth scroll to top on click
- Fixed position bottom-right
- Respects reduced motion preference

Enhanced Search:
- Recent searches functionality with localStorage
- Show recent searches on focus when empty
- Remove individual items or clear all
- Enhanced empty state with icon and hint
- Cmd/Ctrl + K keyboard shortcut to focus search
- Add search to recent when getting results

CSS Enhancements:
- Theme toggle container styles
- Recent searches section styling
- Search empty state with icon
- Search loading spinner
- Keyboard shortcut hint styles
- Print styles for new components

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(ui): resolve header and theme toggle issues

- Add Copilot logo to header via Starlight config with automatic theme switching
- Fix theme toggle slider direction (was reversed)
- Fix theme toggle active icon highlighting (was backwards)
- Change theme toggle from purple circle slider to bold text indicator
- Fix theme toggle slider overflow by adding overflow: hidden
- Remove duplicate banner image from home page
- Clean up conflicting logo CSS rules to prevent duplication

The header now displays: [ Copilot Icon ] Awesome GitHub Copilot [ Search ]
Theme toggle indicators are now visually clear with bold text for selected theme.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fix(ui): address feedback on UI/UX improvements

- Remove logo from header per brand guidance (logo config and CSS)
- Fix back-to-top button visibility by moving to body level and using global styles
- Fix modal visibility by adding 'visible' class for CSS animations
- Fix theme toggle applying site-wide by using global styles and proper theme initialization
- Update icons to use GitHub Primer SVG icons with proper fill-based styling
- Fix plugin modal to render SVG icons instead of icon names
- Add theme initialization script to prevent flash of unstyled content

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: move modal to body level to fix z-index stacking context issue

The modal was nested inside .main-pane which has isolation: isolate,
creating a new stacking context. This caused the modal's z-index
to be evaluated within that context, unable to stack above the header.

This fix moves the modal to be a direct child of body on page load,
allowing it to properly cover the entire viewport including navbar.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
JoeVenner
2026-03-23 00:13:29 +01:00
committed by GitHub
parent c50b3563f8
commit 10e717202f
22 changed files with 1815 additions and 171 deletions

View File

@@ -17,13 +17,7 @@ export default defineConfig({
starlight({ starlight({
title: "Awesome GitHub Copilot", title: "Awesome GitHub Copilot",
description: siteDescription, description: siteDescription,
social: [ social: [],
{
icon: "github",
label: "GitHub",
href: "https://github.com/github/awesome-copilot",
},
],
head: [ head: [
{ {
tag: "meta", tag: "meta",

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 KiB

View File

@@ -0,0 +1,129 @@
---
// Back to Top Button Component
---
<button
id="back-to-top"
class="back-to-top"
aria-label="Back to top"
title="Back to top"
>
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 15l-6-6-6 6"/>
</svg>
</button>
<script>
(function() {
const button = document.getElementById('back-to-top');
if (!button) return;
// Move button to body level to escape stacking contexts
if (button.parentElement !== document.body) {
document.body.appendChild(button);
}
// Show/hide button based on scroll position
function toggleVisibility() {
const scrollY = window.scrollY || document.documentElement.scrollTop;
if (scrollY > 400) {
button.classList.add('visible');
} else {
button.classList.remove('visible');
}
}
// Scroll to top with smooth behavior
function scrollToTop() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
}
// Throttled scroll handler
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
window.requestAnimationFrame(() => {
toggleVisibility();
ticking = false;
});
ticking = true;
}
}, { passive: true });
// Click handler
button.addEventListener('click', scrollToTop);
// Initial check
toggleVisibility();
})();
</script>
<style is:global>
.back-to-top {
position: fixed;
bottom: 24px;
right: 24px;
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--color-accent);
color: white;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transform: translateY(20px) scale(0.8);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 12px rgba(133, 52, 243, 0.4);
z-index: 9999;
}
.back-to-top.visible {
opacity: 1;
visibility: visible;
transform: translateY(0) scale(1);
}
.back-to-top:hover {
background: var(--color-accent-hover);
transform: translateY(-2px) scale(1.05);
box-shadow: 0 6px 20px rgba(133, 52, 243, 0.5);
}
.back-to-top:active {
transform: translateY(0) scale(0.95);
}
.back-to-top:focus-visible {
outline: 2px solid var(--color-text-emphasis);
outline-offset: 2px;
}
/* Mobile adjustments */
@media (max-width: 768px) {
.back-to-top {
bottom: 16px;
right: 16px;
width: 44px;
height: 44px;
}
}
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
.back-to-top {
transition: opacity 0.2s ease;
transform: none;
}
.back-to-top.visible {
transform: none;
}
}
</style>

View File

@@ -4,6 +4,7 @@ import LastUpdated from "@astrojs/starlight/components/LastUpdated.astro";
import Pagination from "@astrojs/starlight/components/Pagination.astro"; import Pagination from "@astrojs/starlight/components/Pagination.astro";
import config from "virtual:starlight/user-config"; import config from "virtual:starlight/user-config";
import { Icon } from "@astrojs/starlight/components"; import { Icon } from "@astrojs/starlight/components";
import ThemeToggle from "./ThemeToggle.astro";
--- ---
<footer class="sl-flex"> <footer class="sl-flex">
@@ -21,9 +22,11 @@ import { Icon } from "@astrojs/starlight/components";
) )
} }
<p class="made-by">Made with ❤️ by our amazing <a href="/contributors/">contributors</a></p> <p class="made-by">Made with love by our amazing <a href="/contributors/">contributors</a></p>
</footer> </footer>
<ThemeToggle />
<style> <style>
footer { footer {
flex-direction: column; flex-direction: column;
@@ -81,4 +84,4 @@ import { Icon } from "@astrojs/starlight/components";
.made-by a:hover { .made-by a:hover {
color: var(--sl-color-accent); color: var(--sl-color-accent);
} }
</style> </style>

View File

@@ -44,7 +44,29 @@ const twitterDomain =
{socialImageUrl && <meta property="og:image:secure_url" content={socialImageUrl} />} {socialImageUrl && <meta property="og:image:secure_url" content={socialImageUrl} />}
{socialImageType && <meta property="og:image:type" content={socialImageType} />} {socialImageType && <meta property="og:image:type" content={socialImageType} />}
{socialImageAlt && <meta name="twitter:image:alt" content={socialImageAlt} />} {socialImageAlt && <meta name="twitter:image:alt" content={socialImageAlt} />}
<!-- Theme initialization script (runs early to prevent flash) -->
<script is:inline>
(function() {
const STORAGE_KEY = 'awesome-copilot-theme';
const stored = localStorage.getItem(STORAGE_KEY);
// Theme handling:
// - 'dark' or 'light' → set data-theme attribute to that value
// - 'auto' or unset → don't set data-theme (CSS media query handles system preference)
if (stored === 'dark' || stored === 'light') {
document.documentElement.setAttribute('data-theme', stored);
}
// For 'auto' or unset, no attribute means CSS media query controls theme
})();
</script>
<script is:inline define:vars={{ basePath }}> <script is:inline define:vars={{ basePath }}>
// basePath setup for runtime use
document.addEventListener('DOMContentLoaded', () => {
document.body.dataset.basePath = basePath;
});
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
document.body.dataset.basePath = basePath; document.body.dataset.basePath = basePath;
}); });

View File

@@ -0,0 +1,120 @@
---
// Icon component with SVG icons
// Icons are either fill-based (from GitHub Primer) or stroke-based (custom)
// GitHub Primer icons are sourced from https://primer.style/foundations/icons/
export interface Props {
name: 'agents' | 'instructions' | 'skills' | 'hooks' | 'workflows' | 'plugins' | 'tools' | 'learning' | 'close' | 'copy' | 'download' | 'share' | 'external' | 'plus' | 'search' | 'chevron-down' | 'document' | 'lightning' | 'hook' | 'workflow' | 'plug' | 'wrench' | 'book' | 'robot' | 'sync';
size?: number;
class?: string;
}
const { name, size = 24, class: className = '' } = Astro.props;
// Icon definitions: { path: SVG path(s), fill: true for fill-based icons }
const icons: Record<string, { path: string; fill?: boolean }> = {
// Resource type icons - using GitHub Primer icons where available
// Agent icon - using GitHub Primer's agent-24 (sparkle over workflow)
// Source: https://primer.style/foundations/icons/agent-24
'robot': {
fill: true,
path: `<path d="M22.5 13.919v-.278a5.097 5.097 0 0 0-4.961-5.086.858.858 0 0 1-.754-.497l-.149-.327A6.414 6.414 0 0 0 10.81 4a6.133 6.133 0 0 0-6.13 6.32l.019.628a.863.863 0 0 1-.67.869A3.263 3.263 0 0 0 1.5 14.996v.108A3.397 3.397 0 0 0 4.896 18.5h1.577a.75.75 0 0 1 0 1.5H4.896A4.896 4.896 0 0 1 0 15.104v-.108a4.761 4.761 0 0 1 3.185-4.493l-.004-.137A7.633 7.633 0 0 1 10.81 2.5a7.911 7.911 0 0 1 7.176 4.58C21.36 7.377 24 10.207 24 13.641v.278a.75.75 0 0 1-1.5 0Z"/><path d="m12.306 11.77 3.374 3.375a.749.749 0 0 1 0 1.061l-3.375 3.375-.057.051a.751.751 0 0 1-1.004-.051.751.751 0 0 1-.051-1.004l.051-.057 2.845-2.845-2.844-2.844a.75.75 0 1 1 1.061-1.061ZM22.5 19.8H18a.75.75 0 0 1 0-1.5h4.5a.75.75 0 0 1 0 1.5Z"/>`
},
// Document icon - custom stroke-based
'document': {
path: `<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`
},
// Lightning icon - custom stroke-based (for skills)
'lightning': {
path: `<path d="M13 2 4.09 12.11a1.23 1.23 0 0 0 .13 1.72l.16.14a1.23 1.23 0 0 0 1.52 0L13 9.5V22l8.91-10.11a1.23 1.23 0 0 0-.13-1.72l-.16-.14a1.23 1.23 0 0 0-1.52 0L13 14.5V2Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`
},
// Hook icon - using GitHub Primer's sync-24 (represents hooks/iterations)
// Source: https://primer.style/foundations/icons/sync-24
'hook': {
fill: true,
path: `<path d="M3.38 8A9.502 9.502 0 0 1 12 2.5a9.502 9.502 0 0 1 9.215 7.182.75.75 0 1 0 1.456-.364C21.473 4.539 17.15 1 12 1a10.995 10.995 0 0 0-9.5 5.452V4.75a.75.75 0 0 0-1.5 0V8.5a1 1 0 0 0 1 1h3.75a.75.75 0 0 0 0-1.5H3.38Zm-.595 6.318a.75.75 0 0 0-1.455.364C2.527 19.461 6.85 23 12 23c4.052 0 7.592-2.191 9.5-5.451v1.701a.75.75 0 0 0 1.5 0V15.5a1 1 0 0 0-1-1h-3.75a.75.75 0 0 0 0 1.5h2.37A9.502 9.502 0 0 1 12 21.5c-4.446 0-8.181-3.055-9.215-7.182Z"/>`
},
// Workflow icon - using GitHub Primer's workflow-24
// Source: https://primer.style/foundations/icons/workflow-24
// Also used by https://github.github.com/gh-aw/
'workflow': {
fill: true,
path: `<path d="M1 3a2 2 0 0 1 2-2h6.5a2 2 0 0 1 2 2v6.5a2 2 0 0 1-2 2H7v4.063C7 16.355 7.644 17 8.438 17H12.5v-2.5a2 2 0 0 1 2-2H21a2 2 0 0 1 2 2V21a2 2 0 0 1-2 2h-6.5a2 2 0 0 1-2-2v-2.5H8.437A2.939 2.939 0 0 1 5.5 15.562V11.5H3a2 2 0 0 1-2-2Zm2-.5a.5.5 0 0 0-.5.5v6.5a.5.5 0 0 0 .5.5h6.5a.5.5 0 0 0 .5-.5V3a.5.5 0 0 0-.5-.5ZM14.5 14a.5.5 0 0 0-.5.5V21a.5.5 0 0 0 .5.5H21a.5.5 0 0 0 .5-.5v-6.5a.5.5 0 0 0-.5-.5Z"/>`
},
// Plug icon - using GitHub Primer's plug-24
// Source: https://primer.style/foundations/icons/plug-24
'plug': {
fill: true,
path: `<path d="M7 11.5H2.938c-.794 0-1.438.644-1.438 1.437v8.313a.75.75 0 0 1-1.5 0v-8.312A2.939 2.939 0 0 1 2.937 10H7V6.151c0-.897.678-1.648 1.57-1.74l6.055-.626 1.006-1.174A1.752 1.752 0 0 1 16.96 2h1.29c.966 0 1.75.784 1.75 1.75V6h3.25a.75.75 0 0 1 0 1.5H20V14h3.25a.75.75 0 0 1 0 1.5H20v2.25a1.75 1.75 0 0 1-1.75 1.75h-1.29a1.75 1.75 0 0 1-1.329-.611l-1.006-1.174-6.055-.627A1.749 1.749 0 0 1 7 15.348Zm9.77-7.913v.001l-1.201 1.4a.75.75 0 0 1-.492.258l-6.353.657a.25.25 0 0 0-.224.249v9.196a.25.25 0 0 0 .224.249l6.353.657c.191.02.368.112.493.258l1.2 1.401a.252.252 0 0 0 .19.087h1.29a.25.25 0 0 0 .25-.25v-14a.25.25 0 0 0-.25-.25h-1.29a.252.252 0 0 0-.19.087Z"/>`
},
// Wrench icon - custom stroke-based (for tools)
'wrench': {
path: `<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`
},
// Book icon - custom stroke-based (for learning)
'book': {
path: `<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`
},
// Action icons - all custom stroke-based
'close': {
path: `<path d="M18 6 6 18M6 6l12 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`
},
'copy': {
path: `<path d="M8 4h8a2 2 0 0 1 2 2v8M8 4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 4v10a2 2 0 0 0 2 2h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`
},
'download': {
path: `<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`
},
'share': {
path: `<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8M16 6l-4-4-4 4M12 2v13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`
},
'external': {
path: `<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14 21 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`
},
'plus': {
path: `<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`
},
'search': {
path: `<circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="m21 21-4.35-4.35" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`
},
'chevron-down': {
path: `<path d="m6 9 6 6 6-6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`
},
// Alias for hook - same as 'hook'
'sync': {
fill: true,
path: `<path d="M3.38 8A9.502 9.502 0 0 1 12 2.5a9.502 9.502 0 0 1 9.215 7.182.75.75 0 1 0 1.456-.364C21.473 4.539 17.15 1 12 1a10.995 10.995 0 0 0-9.5 5.452V4.75a.75.75 0 0 0-1.5 0V8.5a1 1 0 0 0 1 1h3.75a.75.75 0 0 0 0-1.5H3.38Zm-.595 6.318a.75.75 0 0 0-1.455.364C2.527 19.461 6.85 23 12 23c4.052 0 7.592-2.191 9.5-5.451v1.701a.75.75 0 0 0 1.5 0V15.5a1 1 0 0 0-1-1h-3.75a.75.75 0 0 0 0 1.5h2.37A9.502 9.502 0 0 1 12 21.5c-4.446 0-8.181-3.055-9.215-7.182Z"/>`
},
};
const iconData = icons[name] || { path: '' };
const isFill = iconData.fill ?? false;
const iconPath = iconData.path;
---
<svg
viewBox="0 0 24 24"
width={size}
height={size}
fill={isFill ? 'currentColor' : 'none'}
class={className}
aria-hidden="true"
set:html={iconPath}
/>

View File

@@ -1,10 +1,13 @@
--- ---
import Icon from './Icon.astro';
interface Props { interface Props {
title: string; title: string;
description: string; description: string;
icon?: 'robot' | 'document' | 'lightning' | 'hook' | 'workflow' | 'plug' | 'wrench' | 'book';
} }
const { title, description } = Astro.props; const { title, description, icon } = Astro.props;
const contributingUrl = 'https://github.com/github/awesome-copilot/blob/main/CONTRIBUTING.md'; const contributingUrl = 'https://github.com/github/awesome-copilot/blob/main/CONTRIBUTING.md';
--- ---
@@ -12,7 +15,10 @@ const contributingUrl = 'https://github.com/github/awesome-copilot/blob/main/CON
<div class="container"> <div class="container">
<div class="page-header-row"> <div class="page-header-row">
<div> <div>
<h1><Fragment set:html={title} /></h1> <h1>
{icon && <Icon name={icon} size={28} />}
<Fragment set:html={title} />
</h1>
<p><slot><Fragment set:html={description} /></slot></p> <p><slot><Fragment set:html={description} /></slot></p>
</div> </div>
<a href={contributingUrl} class="contribute-link" target="_blank" rel="noopener"> <a href={contributingUrl} class="contribute-link" target="_blank" rel="noopener">
@@ -22,3 +28,11 @@ const contributingUrl = 'https://github.com/github/awesome-copilot/blob/main/CON
</div> </div>
</div> </div>
</div> </div>
<style>
.page-header h1 {
display: flex;
align-items: center;
gap: 0.5rem;
}
</style>

View File

@@ -0,0 +1,170 @@
---
// Theme Toggle Component - 3 state slider: Auto | Dark | Light
---
<div class="theme-toggle-container">
<button
id="theme-toggle"
class="theme-toggle"
aria-label="Toggle theme"
title="Change theme"
>
<span class="theme-icon moon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
</span>
<span class="theme-icon auto" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M12 2v10l4.5 4.5"/>
</svg>
</span>
<span class="theme-icon sun" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="5"/>
<line x1="12" y1="1" x2="12" y2="3"/>
<line x1="12" y1="21" x2="12" y2="23"/>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
<line x1="1" y1="12" x2="3" y2="12"/>
<line x1="21" y1="12" x2="23" y2="12"/>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
</span>
<span class="theme-slider"></span>
</button>
</div>
<script>
(function() {
// Move theme toggle to body level to escape any stacking contexts
const container = document.querySelector('.theme-toggle-container');
if (container && container.parentElement !== document.body) {
document.body.appendChild(container);
}
const STORAGE_KEY = 'awesome-copilot-theme';
const toggle = document.getElementById('theme-toggle');
const html = document.documentElement;
const themes = ['dark', 'auto', 'light'];
const icons = ['moon', 'auto', 'sun'];
function getThemeIndex() {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored && themes.includes(stored)) {
return themes.indexOf(stored);
}
// Default to light theme
return 2;
}
function applyTheme(index: number) {
const theme = themes[index];
if (theme === 'auto') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
html.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
} else {
html.setAttribute('data-theme', theme);
}
// Move slider
const slider = toggle?.querySelector('.theme-slider') as HTMLElement;
if (slider) {
slider.style.transform = `translateX(${index * 100}%)`;
}
// Highlight active icon
const themeToggle = document.querySelector('.theme-toggle');
themeToggle?.setAttribute('data-active', String(index));
}
function cycleTheme() {
const currentIndex = getThemeIndex();
const nextIndex = (currentIndex + 1) % themes.length;
localStorage.setItem(STORAGE_KEY, themes[nextIndex]);
applyTheme(nextIndex);
}
// Initialize
applyTheme(getThemeIndex());
// Click handler
toggle?.addEventListener('click', cycleTheme);
// Keyboard shortcut
document.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'L') {
e.preventDefault();
cycleTheme();
}
});
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (localStorage.getItem(STORAGE_KEY) === 'auto') {
applyTheme(1); // auto
}
});
})();
</script>
<style is:global>
.theme-toggle-container {
display: flex;
align-items: center;
}
.theme-toggle {
display: flex;
position: relative;
width: 108px;
height: 36px;
padding: 3px;
border: 1px solid var(--color-border);
border-radius: 20px;
background: var(--color-bg-secondary);
cursor: pointer;
transition: all 0.2s ease;
overflow: hidden;
}
.theme-toggle:hover {
border-color: var(--color-accent);
}
.theme-toggle:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
.theme-icon {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 30px;
border-radius: 16px;
color: var(--color-text-muted);
transition: all 0.2s ease;
z-index: 1;
font-weight: 400;
}
.theme-icon.moon { color: #9898a6; }
.theme-icon.auto { color: #9898a6; }
.theme-icon.sun { color: #9898a6; }
.theme-toggle[data-active="0"] .moon,
.theme-toggle[data-active="1"] .auto,
.theme-toggle[data-active="2"] .sun {
color: var(--color-text);
font-weight: 700;
}
.theme-slider {
display: none;
}
</style>

View File

@@ -5,6 +5,7 @@ import Modal from '../components/Modal.astro';
import ContributeCTA from '../components/ContributeCTA.astro'; import ContributeCTA from '../components/ContributeCTA.astro';
import EmbeddedPageData from '../components/EmbeddedPageData.astro'; import EmbeddedPageData from '../components/EmbeddedPageData.astro';
import PageHeader from '../components/PageHeader.astro'; import PageHeader from '../components/PageHeader.astro';
import BackToTop from '../components/BackToTop.astro';
import { renderAgentsHtml, sortAgents } from '../scripts/pages/agents-render'; import { renderAgentsHtml, sortAgents } from '../scripts/pages/agents-render';
const initialItems = sortAgents(agentsData.items, 'title'); const initialItems = sortAgents(agentsData.items, 'title');
@@ -12,7 +13,7 @@ const initialItems = sortAgents(agentsData.items, 'title');
<StarlightPage frontmatter={{ title: 'Custom Agents', description: 'Specialized agents that enhance GitHub Copilot for specific technologies, workflows, and domains', template: 'splash', prev: false, next: false, editUrl: false }}> <StarlightPage frontmatter={{ title: 'Custom Agents', description: 'Specialized agents that enhance GitHub Copilot for specific technologies, workflows, and domains', template: 'splash', prev: false, next: false, editUrl: false }}>
<div id="main-content"> <div id="main-content">
<PageHeader title="🤖 Custom Agents" description="Specialized agents that enhance GitHub Copilot for specific technologies, workflows, and domains" /> <PageHeader title="Custom Agents" description="Specialized agents that enhance GitHub Copilot for specific technologies, workflows, and domains" icon="robot" />
<div class="page-content"> <div class="page-content">
<div class="container"> <div class="container">
@@ -57,6 +58,7 @@ const initialItems = sortAgents(agentsData.items, 'title');
</div> </div>
<Modal /> <Modal />
<BackToTop />
<EmbeddedPageData filename="agents.json" data={agentsData} /> <EmbeddedPageData filename="agents.json" data={agentsData} />
<script> <script>

View File

@@ -5,7 +5,7 @@ import PageHeader from '../components/PageHeader.astro';
<StarlightPage frontmatter={{ title: 'Contributors', description: 'The wonderful people who have contributed to Awesome GitHub Copilot', template: 'splash', prev: false, next: false, editUrl: false }}> <StarlightPage frontmatter={{ title: 'Contributors', description: 'The wonderful people who have contributed to Awesome GitHub Copilot', template: 'splash', prev: false, next: false, editUrl: false }}>
<div id="main-content"> <div id="main-content">
<PageHeader title="🌟 Contributors" description="The wonderful people who have contributed to Awesome GitHub Copilot" /> <PageHeader title="Contributors" description="The wonderful people who have contributed to Awesome GitHub Copilot" icon="book" />
<div class="page-content"> <div class="page-content">
<div class="container"> <div class="container">

View File

@@ -4,6 +4,7 @@ import Modal from '../components/Modal.astro';
import ContributeCTA from '../components/ContributeCTA.astro'; import ContributeCTA from '../components/ContributeCTA.astro';
import PageHeader from '../components/PageHeader.astro'; import PageHeader from '../components/PageHeader.astro';
import EmbeddedPageData from '../components/EmbeddedPageData.astro'; import EmbeddedPageData from '../components/EmbeddedPageData.astro';
import BackToTop from '../components/BackToTop.astro';
import hooksData from '../../public/data/hooks.json'; import hooksData from '../../public/data/hooks.json';
import { renderHooksHtml, sortHooks } from '../scripts/pages/hooks-render'; import { renderHooksHtml, sortHooks } from '../scripts/pages/hooks-render';
@@ -12,7 +13,7 @@ const initialItems = sortHooks(hooksData.items, 'title');
<StarlightPage frontmatter={{ title: 'Hooks', description: 'Automated workflows triggered by Copilot coding agent events', template: 'splash', prev: false, next: false, editUrl: false }}> <StarlightPage frontmatter={{ title: 'Hooks', description: 'Automated workflows triggered by Copilot coding agent events', template: 'splash', prev: false, next: false, editUrl: false }}>
<div id="main-content"> <div id="main-content">
<PageHeader title="🪝 Hooks" description="Automated workflows triggered by Copilot coding agent events" /> <PageHeader title="Hooks" description="Automated workflows triggered by Copilot coding agent events" icon="hook" />
<div class="page-content"> <div class="page-content">
<div class="container"> <div class="container">
@@ -21,7 +22,7 @@ const initialItems = sortHooks(hooksData.items, 'title');
<label for="search-input" class="sr-only">Search hooks</label> <label for="search-input" class="sr-only">Search hooks</label>
<input type="text" id="search-input" placeholder="Search hooks..." autocomplete="off"> <input type="text" id="search-input" placeholder="Search hooks..." autocomplete="off">
</div> </div>
<div class="filters-bar" id="filters-bar"> <div class="filters-bar" id="filters-bar">
<div class="filter-group"> <div class="filter-group">
<label for="filter-hook">Hook Event:</label> <label for="filter-hook">Hook Event:</label>
@@ -41,7 +42,7 @@ const initialItems = sortHooks(hooksData.items, 'title');
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button> <button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
</div> </div>
</div> </div>
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} of {initialItems.length} hooks</div> <div class="results-count" id="results-count" aria-live="polite">{initialItems.length} of {initialItems.length} hooks</div>
<div class="resource-list" id="resource-list" role="list" set:html={renderHooksHtml(initialItems)}></div> <div class="resource-list" id="resource-list" role="list" set:html={renderHooksHtml(initialItems)}></div>
<ContributeCTA resourceType="hooks" /> <ContributeCTA resourceType="hooks" />
@@ -50,6 +51,7 @@ const initialItems = sortHooks(hooksData.items, 'title');
</div> </div>
<Modal /> <Modal />
<BackToTop />
<EmbeddedPageData filename="hooks.json" data={hooksData} /> <EmbeddedPageData filename="hooks.json" data={hooksData} />
<script> <script>

View File

@@ -1,6 +1,8 @@
--- ---
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
import Modal from '../components/Modal.astro'; import Modal from '../components/Modal.astro';
import Icon from '../components/Icon.astro';
import BackToTop from '../components/BackToTop.astro';
const base = import.meta.env.BASE_URL; const base = import.meta.env.BASE_URL;
--- ---
@@ -31,7 +33,10 @@ const base = import.meta.env.BASE_URL;
<!-- Hero Section --> <!-- Hero Section -->
<section class="hero" aria-labelledby="hero-heading"> <section class="hero" aria-labelledby="hero-heading">
<div class="container"> <div class="container">
<h1 id="hero-heading">Awesome GitHub Copilot</h1> <h1 id="hero-heading">
<span class="gradient-text">Awesome</span>
<span class="gradient-text-alt">GitHub Copilot</span>
</h1>
<p class="hero-subtitle">Community-contributed agents, instructions, and skills to enhance your GitHub Copilot experience</p> <p class="hero-subtitle">Community-contributed agents, instructions, and skills to enhance your GitHub Copilot experience</p>
<div class="hero-search"> <div class="hero-search">
<label for="global-search" class="sr-only">Search all resources</label> <label for="global-search" class="sr-only">Search all resources</label>
@@ -39,13 +44,21 @@ const base = import.meta.env.BASE_URL;
Type at least two characters to show matching resources, then press the Down Arrow key to move into the results. Type at least two characters to show matching resources, then press the Down Arrow key to move into the results.
</p> </p>
<p id="global-search-status" class="sr-only" aria-live="polite"></p> <p id="global-search-status" class="sr-only" aria-live="polite"></p>
<input <div class="search-row">
type="text" <input
id="global-search" type="text"
placeholder="Search all resources..." id="global-search"
autocomplete="off" placeholder="Search all resources..."
aria-describedby="global-search-help global-search-status" autocomplete="off"
> aria-describedby="global-search-help global-search-status"
>
<a href="https://github.com/github/awesome-copilot" target="_blank" rel="noopener" class="github-btn" aria-label="View on GitHub">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
<span>GitHub</span>
</a>
</div>
<div id="search-results" class="search-results hidden" aria-label="Search results"></div> <div id="search-results" class="search-results hidden" aria-label="Search results"></div>
</div> </div>
</div> </div>
@@ -56,64 +69,72 @@ const base = import.meta.env.BASE_URL;
<h2 id="quick-links-heading" class="sr-only">Browse Resources</h2> <h2 id="quick-links-heading" class="sr-only">Browse Resources</h2>
<div class="container"> <div class="container">
<div class="cards-grid"> <div class="cards-grid">
<a href={`${base}agents/`} class="card card-with-count" id="card-agents"> <a href={`${base}agents/`} class="card card-with-count card-category-ai" id="card-agents" style="--animation-delay: 0ms;">
<div class="card-icon" aria-hidden="true">🤖</div> <div class="card-icon" aria-hidden="true"><Icon name="robot" size={40} /></div>
<div class="card-content"> <div class="card-content">
<h3>Agents</h3> <h3>Agents</h3>
<p>Custom agents for specialized Copilot experiences</p> <p>Custom agents for specialized Copilot experiences</p>
</div> </div>
<div class="card-count" data-count="agents" aria-label="Agent count">-</div> <div class="card-count" data-count="agents" aria-label="Agent count">-</div>
</a> </a>
<a href={`${base}instructions/`} class="card card-with-count" id="card-instructions"> <a href={`${base}instructions/`} class="card card-with-count card-category-docs" id="card-instructions" style="--animation-delay: 50ms;">
<div class="card-icon" aria-hidden="true">📋</div> <div class="card-icon" aria-hidden="true"><Icon name="document" size={40} /></div>
<div class="card-content"> <div class="card-content">
<h3>Instructions</h3> <h3>Instructions</h3>
<p>Coding standards and best practices for Copilot</p> <p>Coding standards and best practices for Copilot</p>
</div> </div>
<div class="card-count" data-count="instructions" aria-label="Instruction count">-</div> <div class="card-count" data-count="instructions" aria-label="Instruction count">-</div>
</a> </a>
<a href={`${base}skills/`} class="card card-with-count" id="card-skills"> <a href={`${base}skills/`} class="card card-with-count card-category-power" id="card-skills" style="--animation-delay: 100ms;">
<div class="card-icon" aria-hidden="true"></div> <div class="card-icon" aria-hidden="true"><Icon name="lightning" size={40} /></div>
<div class="card-content"> <div class="card-content">
<h3>Skills</h3> <h3>Skills</h3>
<p>Self-contained folders with instructions and resources</p> <p>Self-contained folders with instructions and resources</p>
</div> </div>
<div class="card-count" data-count="skills" aria-label="Skill count">-</div> <div class="card-count" data-count="skills" aria-label="Skill count">-</div>
</a> </a>
<a href={`${base}hooks/`} class="card card-with-count" id="card-hooks"> <a href={`${base}hooks/`} class="card card-with-count card-category-automation" id="card-hooks" style="--animation-delay: 150ms;">
<div class="card-icon" aria-hidden="true">🪝</div> <div class="card-icon" aria-hidden="true"><Icon name="hook" size={40} /></div>
<div class="card-content"> <div class="card-content">
<h3>Hooks</h3> <h3>Hooks</h3>
<p>Automated workflows triggered by agent events</p> <p>Automated workflows triggered by agent events</p>
</div> </div>
<div class="card-count" data-count="hooks" aria-label="Hook count">-</div> <div class="card-count" data-count="hooks" aria-label="Hook count">-</div>
</a> </a>
<a href={`${base}workflows/`} class="card card-with-count" id="card-workflows"> <a href={`${base}workflows/`} class="card card-with-count card-category-automation" id="card-workflows" style="--animation-delay: 200ms;">
<div class="card-icon" aria-hidden="true">⚡</div> <div class="card-icon" aria-hidden="true">
<Icon name="workflow" size={40} />
</div>
<div class="card-content"> <div class="card-content">
<h3>Workflows</h3> <h3>Workflows</h3>
<p>AI-powered automations for GitHub Actions</p> <p>AI-powered automations for GitHub Actions</p>
</div> </div>
<div class="card-count" data-count="workflows" aria-label="Workflow count">-</div> <div class="card-count" data-count="workflows" aria-label="Workflow count">-</div>
</a> </a>
<a href={`${base}plugins/`} class="card card-with-count" id="card-plugins"> <a href={`${base}plugins/`} class="card card-with-count card-category-extension" id="card-plugins" style="--animation-delay: 250ms;">
<div class="card-icon" aria-hidden="true">🔌</div> <div class="card-icon" aria-hidden="true">
<Icon name="plug" size={40} />
</div>
<div class="card-content"> <div class="card-content">
<h3>Plugins</h3> <h3>Plugins</h3>
<p>Curated plugins organized by themes</p> <p>Curated plugins organized by themes</p>
</div> </div>
<div class="card-count" data-count="plugins" aria-label="Plugin count">-</div> <div class="card-count" data-count="plugins" aria-label="Plugin count">-</div>
</a> </a>
<a href={`${base}tools/`} class="card card-with-count" id="card-tools"> <a href={`${base}tools/`} class="card card-with-count card-category-dev" id="card-tools" style="--animation-delay: 300ms;">
<div class="card-icon" aria-hidden="true">🔧</div> <div class="card-icon" aria-hidden="true">
<Icon name="wrench" size={40} />
</div>
<div class="card-content"> <div class="card-content">
<h3>Tools</h3> <h3>Tools</h3>
<p>MCP servers and developer tools</p> <p>MCP servers and developer tools</p>
</div> </div>
<div class="card-count" data-count="tools" aria-label="Tool count">-</div> <div class="card-count" data-count="tools" aria-label="Tool count">-</div>
</a> </a>
<a href={`${base}learning-hub/`} class="card card-with-count" id="card-learning-hub"> <a href={`${base}learning-hub/`} class="card card-with-count card-category-learn" id="card-learning-hub" style="--animation-delay: 350ms;">
<div class="card-icon" aria-hidden="true">📚</div> <div class="card-icon" aria-hidden="true">
<Icon name="book" size={40} />
</div>
<div class="card-content"> <div class="card-content">
<h3>Learning Hub</h3> <h3>Learning Hub</h3>
<p>Articles and guides to master GitHub Copilot</p> <p>Articles and guides to master GitHub Copilot</p>
@@ -127,6 +148,9 @@ const base = import.meta.env.BASE_URL;
<Modal /> <Modal />
<!-- Back to Top Button -->
<BackToTop />
<script> <script>
import '../scripts/pages/index'; import '../scripts/pages/index';
</script> </script>

View File

@@ -5,6 +5,7 @@ import Modal from '../components/Modal.astro';
import ContributeCTA from '../components/ContributeCTA.astro'; import ContributeCTA from '../components/ContributeCTA.astro';
import EmbeddedPageData from '../components/EmbeddedPageData.astro'; import EmbeddedPageData from '../components/EmbeddedPageData.astro';
import PageHeader from '../components/PageHeader.astro'; import PageHeader from '../components/PageHeader.astro';
import BackToTop from '../components/BackToTop.astro';
import { renderInstructionsHtml, sortInstructions } from '../scripts/pages/instructions-render'; import { renderInstructionsHtml, sortInstructions } from '../scripts/pages/instructions-render';
const initialItems = sortInstructions(instructionsData.items, 'title'); const initialItems = sortInstructions(instructionsData.items, 'title');
@@ -12,7 +13,7 @@ const initialItems = sortInstructions(instructionsData.items, 'title');
<StarlightPage frontmatter={{ title: 'Instructions', description: 'Coding standards and best practices for GitHub Copilot', template: 'splash', prev: false, next: false, editUrl: false }}> <StarlightPage frontmatter={{ title: 'Instructions', description: 'Coding standards and best practices for GitHub Copilot', template: 'splash', prev: false, next: false, editUrl: false }}>
<div id="main-content"> <div id="main-content">
<PageHeader title="📋 Instructions" description="Coding standards and best practices for GitHub Copilot" /> <PageHeader title="Instructions" description="Coding standards and best practices for GitHub Copilot" icon="document" />
<div class="page-content"> <div class="page-content">
<div class="container"> <div class="container">
@@ -21,7 +22,7 @@ const initialItems = sortInstructions(instructionsData.items, 'title');
<label for="search-input" class="sr-only">Search instructions</label> <label for="search-input" class="sr-only">Search instructions</label>
<input type="text" id="search-input" placeholder="Search instructions..." autocomplete="off"> <input type="text" id="search-input" placeholder="Search instructions..." autocomplete="off">
</div> </div>
<div class="filters-bar" id="filters-bar"> <div class="filters-bar" id="filters-bar">
<div class="filter-group"> <div class="filter-group">
<label for="filter-extension">File Extension:</label> <label for="filter-extension">File Extension:</label>
@@ -37,7 +38,7 @@ const initialItems = sortInstructions(instructionsData.items, 'title');
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button> <button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
</div> </div>
</div> </div>
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} of {initialItems.length} instructions</div> <div class="results-count" id="results-count" aria-live="polite">{initialItems.length} of {initialItems.length} instructions</div>
<div class="resource-list" id="resource-list" role="list" set:html={renderInstructionsHtml(initialItems)}></div> <div class="resource-list" id="resource-list" role="list" set:html={renderInstructionsHtml(initialItems)}></div>
<ContributeCTA resourceType="instructions" /> <ContributeCTA resourceType="instructions" />
@@ -46,6 +47,7 @@ const initialItems = sortInstructions(instructionsData.items, 'title');
</div> </div>
<Modal /> <Modal />
<BackToTop />
<EmbeddedPageData filename="instructions.json" data={instructionsData} /> <EmbeddedPageData filename="instructions.json" data={instructionsData} />
<script> <script>

View File

@@ -5,6 +5,7 @@ import Modal from '../components/Modal.astro';
import ContributeCTA from '../components/ContributeCTA.astro'; import ContributeCTA from '../components/ContributeCTA.astro';
import EmbeddedPageData from '../components/EmbeddedPageData.astro'; import EmbeddedPageData from '../components/EmbeddedPageData.astro';
import PageHeader from '../components/PageHeader.astro'; import PageHeader from '../components/PageHeader.astro';
import BackToTop from '../components/BackToTop.astro';
import { renderPluginsHtml } from '../scripts/pages/plugins-render'; import { renderPluginsHtml } from '../scripts/pages/plugins-render';
const initialItems = pluginsData.items; const initialItems = pluginsData.items;
@@ -12,7 +13,7 @@ const initialItems = pluginsData.items;
<StarlightPage frontmatter={{ title: 'Plugins', description: 'Curated plugins of agents, hooks, and skills for specific workflows', template: 'splash', prev: false, next: false, editUrl: false }}> <StarlightPage frontmatter={{ title: 'Plugins', description: 'Curated plugins of agents, hooks, and skills for specific workflows', template: 'splash', prev: false, next: false, editUrl: false }}>
<div id="main-content"> <div id="main-content">
<PageHeader title="🔌 Plugins" description="Curated plugins of agents, hooks, and skills for specific workflows" /> <PageHeader title="Plugins" description="Curated plugins of agents, hooks, and skills for specific workflows" icon="plug" />
<div class="page-content"> <div class="page-content">
<div class="container"> <div class="container">
@@ -48,6 +49,7 @@ const initialItems = pluginsData.items;
</div> </div>
<Modal /> <Modal />
<BackToTop />
<EmbeddedPageData filename="plugins.json" data={pluginsData} /> <EmbeddedPageData filename="plugins.json" data={pluginsData} />
<script> <script>

View File

@@ -4,6 +4,7 @@ import Modal from '../components/Modal.astro';
import ContributeCTA from '../components/ContributeCTA.astro'; import ContributeCTA from '../components/ContributeCTA.astro';
import PageHeader from '../components/PageHeader.astro'; import PageHeader from '../components/PageHeader.astro';
import EmbeddedPageData from '../components/EmbeddedPageData.astro'; import EmbeddedPageData from '../components/EmbeddedPageData.astro';
import BackToTop from '../components/BackToTop.astro';
import skillsData from '../../public/data/skills.json'; import skillsData from '../../public/data/skills.json';
import { renderSkillsHtml, sortSkills } from '../scripts/pages/skills-render'; import { renderSkillsHtml, sortSkills } from '../scripts/pages/skills-render';
@@ -12,7 +13,7 @@ const initialItems = sortSkills(skillsData.items, 'title');
<StarlightPage frontmatter={{ title: 'Skills', description: 'Self-contained agent skills with instructions and bundled resources', template: 'splash', prev: false, next: false, editUrl: false }}> <StarlightPage frontmatter={{ title: 'Skills', description: 'Self-contained agent skills with instructions and bundled resources', template: 'splash', prev: false, next: false, editUrl: false }}>
<div id="main-content"> <div id="main-content">
<PageHeader title="Skills" description="Self-contained agent skills with instructions and bundled resources" /> <PageHeader title="Skills" description="Self-contained agent skills with instructions and bundled resources" icon="lightning" />
<div class="page-content"> <div class="page-content">
<div class="container"> <div class="container">
@@ -21,7 +22,7 @@ const initialItems = sortSkills(skillsData.items, 'title');
<label for="search-input" class="sr-only">Search skills</label> <label for="search-input" class="sr-only">Search skills</label>
<input type="text" id="search-input" placeholder="Search skills..." autocomplete="off"> <input type="text" id="search-input" placeholder="Search skills..." autocomplete="off">
</div> </div>
<div class="filters-bar" id="filters-bar"> <div class="filters-bar" id="filters-bar">
<div class="filter-group"> <div class="filter-group">
<label for="filter-category">Category:</label> <label for="filter-category">Category:</label>
@@ -43,7 +44,7 @@ const initialItems = sortSkills(skillsData.items, 'title');
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button> <button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
</div> </div>
</div> </div>
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} of {initialItems.length} skills</div> <div class="results-count" id="results-count" aria-live="polite">{initialItems.length} of {initialItems.length} skills</div>
<div class="resource-list" id="resource-list" role="list" set:html={renderSkillsHtml(initialItems)}></div> <div class="resource-list" id="resource-list" role="list" set:html={renderSkillsHtml(initialItems)}></div>
<ContributeCTA resourceType="skills" /> <ContributeCTA resourceType="skills" />
@@ -52,6 +53,7 @@ const initialItems = sortSkills(skillsData.items, 'title');
</div> </div>
<Modal /> <Modal />
<BackToTop />
<EmbeddedPageData filename="skills.json" data={skillsData} /> <EmbeddedPageData filename="skills.json" data={skillsData} />
<script> <script>

View File

@@ -1,9 +1,11 @@
--- ---
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
import toolsData from '../../public/data/tools.json'; import toolsData from '../../public/data/tools.json';
import Modal from '../components/Modal.astro';
import ContributeCTA from "../components/ContributeCTA.astro"; import ContributeCTA from "../components/ContributeCTA.astro";
import EmbeddedPageData from "../components/EmbeddedPageData.astro"; import EmbeddedPageData from "../components/EmbeddedPageData.astro";
import PageHeader from "../components/PageHeader.astro"; import PageHeader from "../components/PageHeader.astro";
import BackToTop from '../components/BackToTop.astro';
import { renderToolsHtml } from "../scripts/pages/tools-render"; import { renderToolsHtml } from "../scripts/pages/tools-render";
const initialItems = toolsData.items.map((item) => ({ const initialItems = toolsData.items.map((item) => ({
@@ -14,7 +16,7 @@ const initialItems = toolsData.items.map((item) => ({
<StarlightPage frontmatter={{ title: 'Tools', description: 'MCP servers and developer tools for GitHub Copilot', template: 'splash', prev: false, next: false, editUrl: false }}> <StarlightPage frontmatter={{ title: 'Tools', description: 'MCP servers and developer tools for GitHub Copilot', template: 'splash', prev: false, next: false, editUrl: false }}>
<div id="main-content"> <div id="main-content">
<PageHeader title="🔧 Tools" description="MCP servers and developer tools for GitHub Copilot" /> <PageHeader title="Tools" description="MCP servers and developer tools for GitHub Copilot" icon="wrench" />
<div class="page-content"> <div class="page-content">
<div class="container"> <div class="container">
@@ -57,6 +59,8 @@ const initialItems = toolsData.items.map((item) => ({
</div> </div>
</div> </div>
<Modal />
<BackToTop />
<EmbeddedPageData filename="tools.json" data={toolsData} /> <EmbeddedPageData filename="tools.json" data={toolsData} />
<style is:global> <style is:global>

View File

@@ -5,6 +5,7 @@ import Modal from '../components/Modal.astro';
import ContributeCTA from '../components/ContributeCTA.astro'; import ContributeCTA from '../components/ContributeCTA.astro';
import EmbeddedPageData from '../components/EmbeddedPageData.astro'; import EmbeddedPageData from '../components/EmbeddedPageData.astro';
import PageHeader from '../components/PageHeader.astro'; import PageHeader from '../components/PageHeader.astro';
import BackToTop from '../components/BackToTop.astro';
import { renderWorkflowsHtml, sortWorkflows } from '../scripts/pages/workflows-render'; import { renderWorkflowsHtml, sortWorkflows } from '../scripts/pages/workflows-render';
const initialItems = sortWorkflows(workflowsData.items, 'title'); const initialItems = sortWorkflows(workflowsData.items, 'title');
@@ -12,9 +13,7 @@ const initialItems = sortWorkflows(workflowsData.items, 'title');
<StarlightPage frontmatter={{ title: 'Agentic Workflows', description: 'AI-powered repository automations that run coding agents in GitHub Actions', template: 'splash', prev: false, next: false, editUrl: false }}> <StarlightPage frontmatter={{ title: 'Agentic Workflows', description: 'AI-powered repository automations that run coding agents in GitHub Actions', template: 'splash', prev: false, next: false, editUrl: false }}>
<div id="main-content"> <div id="main-content">
<PageHeader title="Agentic Workflows" description=""> <PageHeader title="Agentic Workflows" description="AI-powered repository automations that run coding agents in GitHub Actions" icon="workflow" />
AI-powered repository automations that run coding agents in <a href="https://gh.io/gh-aw" target="_blank" rel="noopener">GitHub Actions</a>
</PageHeader>
<div class="page-content"> <div class="page-content">
<div class="container"> <div class="container">
@@ -23,7 +22,7 @@ const initialItems = sortWorkflows(workflowsData.items, 'title');
<label for="search-input" class="sr-only">Search workflows</label> <label for="search-input" class="sr-only">Search workflows</label>
<input type="text" id="search-input" placeholder="Search workflows..." autocomplete="off"> <input type="text" id="search-input" placeholder="Search workflows..." autocomplete="off">
</div> </div>
<div class="filters-bar" id="filters-bar"> <div class="filters-bar" id="filters-bar">
<div class="filter-group"> <div class="filter-group">
<label for="filter-trigger">Trigger:</label> <label for="filter-trigger">Trigger:</label>
@@ -39,7 +38,7 @@ const initialItems = sortWorkflows(workflowsData.items, 'title');
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button> <button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
</div> </div>
</div> </div>
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} of {initialItems.length} workflows</div> <div class="results-count" id="results-count" aria-live="polite">{initialItems.length} of {initialItems.length} workflows</div>
<div class="resource-list" id="resource-list" role="list" set:html={renderWorkflowsHtml(initialItems)}></div> <div class="resource-list" id="resource-list" role="list" set:html={renderWorkflowsHtml(initialItems)}></div>
<ContributeCTA resourceType="workflows" /> <ContributeCTA resourceType="workflows" />
@@ -48,6 +47,7 @@ const initialItems = sortWorkflows(workflowsData.items, 'title');
</div> </div>
<Modal /> <Modal />
<BackToTop />
<EmbeddedPageData filename="workflows.json" data={workflowsData} /> <EmbeddedPageData filename="workflows.json" data={workflowsData} />
<script> <script>

View File

@@ -14,7 +14,7 @@ import {
shareFile, shareFile,
getResourceType, getResourceType,
escapeHtml, escapeHtml,
getResourceIcon, getResourceIconSvg,
sanitizeUrl, sanitizeUrl,
} from "./utils"; } from "./utils";
import fm from "front-matter"; import fm from "front-matter";
@@ -489,6 +489,13 @@ function handleModalKeydown(e: KeyboardEvent, modal: HTMLElement): void {
*/ */
export function setupModal(): void { export function setupModal(): void {
const modal = document.getElementById("file-modal"); const modal = document.getElementById("file-modal");
// Move modal to body level to escape ancestor stacking contexts
// This fixes the issue where modal appears below header/theme-toggle
if (modal && modal.parentElement !== document.body) {
document.body.appendChild(modal);
}
const closeBtn = document.getElementById("close-modal"); const closeBtn = document.getElementById("close-modal");
const copyBtn = document.getElementById("copy-btn"); const copyBtn = document.getElementById("copy-btn");
const downloadBtn = document.getElementById("download-btn"); const downloadBtn = document.getElementById("download-btn");
@@ -867,6 +874,7 @@ export async function openFileModal(
const fallbackName = getFileName(filePath); const fallbackName = getFileName(filePath);
updateModalTitle(fallbackName, filePath); updateModalTitle(fallbackName, filePath);
modal.classList.remove("hidden"); modal.classList.remove("hidden");
modal.classList.add("visible");
// Set focus to close button for accessibility // Set focus to close button for accessibility
setTimeout(() => { setTimeout(() => {
@@ -1145,7 +1153,7 @@ function renderLocalPluginModal(
<div class="collection-item" data-path="${escapeHtml( <div class="collection-item" data-path="${escapeHtml(
item.path item.path
)}" data-type="${escapeHtml(item.kind)}"> )}" data-type="${escapeHtml(item.kind)}">
<span class="collection-item-icon">${getResourceIcon( <span class="collection-item-icon">${getResourceIconSvg(
item.kind item.kind
)}</span> )}</span>
<div class="collection-item-info"> <div class="collection-item-info">
@@ -1191,6 +1199,7 @@ export function closeModal(updateUrl = true): void {
if (modal) { if (modal) {
modal.classList.add("hidden"); modal.classList.add("hidden");
modal.classList.remove("visible");
} }
if (installDropdown) { if (installDropdown) {
installDropdown.classList.remove("open"); installDropdown.classList.remove("open");

View File

@@ -5,6 +5,46 @@ import { FuzzySearch, type SearchItem } from '../search';
import { fetchData, debounce, escapeHtml, truncate, getResourceIcon } from '../utils'; import { fetchData, debounce, escapeHtml, truncate, getResourceIcon } from '../utils';
import { setupModal, openFileModal } from '../modal'; import { setupModal, openFileModal } from '../modal';
// SVG icon definitions for search results
// Icons with `fill: true` use fill="currentColor", others use stroke
const iconDefs: Record<string, { path: string; fill?: boolean }> = {
// Agent icon - GitHub Primer's agent-24
robot: {
fill: true,
path: '<path d="M22.5 13.919v-.278a5.097 5.097 0 0 0-4.961-5.086.858.858 0 0 1-.754-.497l-.149-.327A6.414 6.414 0 0 0 10.81 4a6.133 6.133 0 0 0-6.13 6.32l.019.628a.863.863 0 0 1-.67.869A3.263 3.263 0 0 0 1.5 14.996v.108A3.397 3.397 0 0 0 4.896 18.5h1.577a.75.75 0 0 1 0 1.5H4.896A4.896 4.896 0 0 1 0 15.104v-.108a4.761 4.761 0 0 1 3.185-4.493l-.004-.137A7.633 7.633 0 0 1 10.81 2.5a7.911 7.911 0 0 1 7.176 4.58C21.36 7.377 24 10.207 24 13.641v.278a.75.75 0 0 1-1.5 0Z"/><path d="m12.306 11.77 3.374 3.375a.749.749 0 0 1 0 1.061l-3.375 3.375-.057.051a.751.751 0 0 1-1.004-.051.751.751 0 0 1-.051-1.004l.051-.057 2.845-2.845-2.844-2.844a.75.75 0 1 1 1.061-1.061ZM22.5 19.8H18a.75.75 0 0 1 0-1.5h4.5a.75.75 0 0 1 0 1.5Z"/>'
},
document: {
path: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>'
},
lightning: {
path: '<path d="M13 2 4.09 12.11a1.23 1.23 0 0 0 .13 1.72l.16.14a1.23 1.23 0 0 0 1.52 0L13 9.5V22l8.91-10.11a1.23 1.23 0 0 0-.13-1.72l-.16-.14a1.23 1.23 0 0 0-1.52 0L13 14.5V2Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>'
},
// Hook icon - GitHub Primer's sync-24
hook: {
fill: true,
path: '<path d="M3.38 8A9.502 9.502 0 0 1 12 2.5a9.502 9.502 0 0 1 9.215 7.182.75.75 0 1 0 1.456-.364C21.473 4.539 17.15 1 12 1a10.995 10.995 0 0 0-9.5 5.452V4.75a.75.75 0 0 0-1.5 0V8.5a1 1 0 0 0 1 1h3.75a.75.75 0 0 0 0-1.5H3.38Zm-.595 6.318a.75.75 0 0 0-1.455.364C2.527 19.461 6.85 23 12 23c4.052 0 7.592-2.191 9.5-5.451v1.701a.75.75 0 0 0 1.5 0V15.5a1 1 0 0 0-1-1h-3.75a.75.75 0 0 0 0 1.5h2.37A9.502 9.502 0 0 1 12 21.5c-4.446 0-8.181-3.055-9.215-7.182Z"/>'
},
// Workflow icon - GitHub Primer's workflow-24
workflow: {
fill: true,
path: '<path d="M1 3a2 2 0 0 1 2-2h6.5a2 2 0 0 1 2 2v6.5a2 2 0 0 1-2 2H7v4.063C7 16.355 7.644 17 8.438 17H12.5v-2.5a2 2 0 0 1 2-2H21a2 2 0 0 1 2 2V21a2 2 0 0 1-2 2h-6.5a2 2 0 0 1-2-2v-2.5H8.437A2.939 2.939 0 0 1 5.5 15.562V11.5H3a2 2 0 0 1-2-2Zm2-.5a.5.5 0 0 0-.5.5v6.5a.5.5 0 0 0 .5.5h6.5a.5.5 0 0 0 .5-.5V3a.5.5 0 0 0-.5-.5ZM14.5 14a.5.5 0 0 0-.5.5V21a.5.5 0 0 0 .5.5H21a.5.5 0 0 0 .5-.5v-6.5a.5.5 0 0 0-.5-.5Z"/>'
},
// Plug icon - GitHub Primer's plug-24
plug: {
fill: true,
path: '<path d="M7 11.5H2.938c-.794 0-1.438.644-1.438 1.437v8.313a.75.75 0 0 1-1.5 0v-8.312A2.939 2.939 0 0 1 2.937 10H7V6.151c0-.897.678-1.648 1.57-1.74l6.055-.626 1.006-1.174A1.752 1.752 0 0 1 16.96 2h1.29c.966 0 1.75.784 1.75 1.75V6h3.25a.75.75 0 0 1 0 1.5H20V14h3.25a.75.75 0 0 1 0 1.5H20v2.25a1.75 1.75 0 0 1-1.75 1.75h-1.29a1.75 1.75 0 0 1-1.329-.611l-1.006-1.174-6.055-.627A1.749 1.749 0 0 1 7 15.348Zm9.77-7.913v.001l-1.201 1.4a.75.75 0 0 1-.492.258l-6.353.657a.25.25 0 0 0-.224.249v9.196a.25.25 0 0 0 .224.249l6.353.657c.191.02.368.112.493.258l1.2 1.401a.252.252 0 0 0 .19.087h1.29a.25.25 0 0 0 .25-.25v-14a.25.25 0 0 0-.25-.25h-1.29a.252.252 0 0 0-.19.087Z"/>'
},
wrench: {
path: '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>'
}
};
function getIconSvg(iconName: string): string {
const icon = iconDefs[iconName] || iconDefs.document;
const fill = icon.fill ? 'fill="currentColor"' : 'fill="none"';
return `<svg viewBox="0 0 24 24" ${fill} aria-hidden="true">${icon.path}</svg>`;
}
interface Manifest { interface Manifest {
counts: { counts: {
agents: number; agents: number;
@@ -31,6 +71,38 @@ interface PluginsData {
items: Plugin[]; items: Plugin[];
} }
// Recent searches storage
const RECENT_SEARCHES_KEY = 'awesome-copilot-recent-searches';
const MAX_RECENT_SEARCHES = 5;
function getRecentSearches(): string[] {
try {
const stored = localStorage.getItem(RECENT_SEARCHES_KEY);
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
}
function addRecentSearch(query: string): void {
if (!query.trim()) return;
const searches = getRecentSearches();
const filtered = searches.filter(s => s.toLowerCase() !== query.toLowerCase());
filtered.unshift(query);
const limited = filtered.slice(0, MAX_RECENT_SEARCHES);
localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(limited));
}
function removeRecentSearch(query: string): void {
const searches = getRecentSearches();
const filtered = searches.filter(s => s !== query);
localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(filtered));
}
function clearRecentSearches(): void {
localStorage.removeItem(RECENT_SEARCHES_KEY);
}
export async function initHomepage(): Promise<void> { export async function initHomepage(): Promise<void> {
// Load manifest for stats // Load manifest for stats
const manifest = await fetchData<Manifest>('manifest.json'); const manifest = await fetchData<Manifest>('manifest.json');
@@ -56,9 +128,11 @@ export async function initHomepage(): Promise<void> {
if (searchInput && resultsDiv) { if (searchInput && resultsDiv) {
const statusEl = document.getElementById("global-search-status"); const statusEl = document.getElementById("global-search-status");
let isShowingRecent = false;
const hideResults = (): void => { const hideResults = (): void => {
resultsDiv.classList.add("hidden"); resultsDiv.classList.add("hidden");
isShowingRecent = false;
}; };
const showResults = (): void => { const showResults = (): void => {
@@ -67,7 +141,7 @@ export async function initHomepage(): Promise<void> {
const getResultButtons = (): HTMLButtonElement[] => const getResultButtons = (): HTMLButtonElement[] =>
Array.from( Array.from(
resultsDiv.querySelectorAll<HTMLButtonElement>(".search-result") resultsDiv.querySelectorAll<HTMLButtonElement>(".search-result, .search-recent-item")
); );
const openResult = (resultEl: HTMLElement): void => { const openResult = (resultEl: HTMLElement): void => {
@@ -79,33 +153,125 @@ export async function initHomepage(): Promise<void> {
} }
}; };
// Render recent searches
const renderRecentSearches = (): void => {
const recent = getRecentSearches();
if (recent.length === 0) return;
const clockIcon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>`;
const xIcon = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
resultsDiv.innerHTML = `
<div class="search-recent-header">
<span>Recent Searches</span>
<button class="search-clear-recent" aria-label="Clear recent searches">Clear</button>
</div>
${recent.map(query => `
<button type="button" class="search-recent-item" data-query="${escapeHtml(query)}">
<span class="search-recent-icon">${clockIcon}</span>
<span class="search-recent-text">${escapeHtml(query)}</span>
<button type="button" class="search-recent-remove" data-query="${escapeHtml(query)}" aria-label="Remove from history">
${xIcon}
</button>
</button>
`).join('')}
`;
// Add click handlers for recent items
resultsDiv.querySelectorAll('.search-recent-item').forEach(item => {
item.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
if (target.closest('.search-recent-remove')) return;
const query = (item as HTMLElement).dataset.query;
if (query) {
searchInput.value = query;
searchInput.dispatchEvent(new Event('input'));
}
});
});
// Add click handlers for remove buttons
resultsDiv.querySelectorAll('.search-recent-remove').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const query = (btn as HTMLElement).dataset.query;
if (query) {
removeRecentSearch(query);
renderRecentSearches();
if (getRecentSearches().length === 0) {
hideResults();
}
}
});
});
// Add clear all handler
const clearBtn = resultsDiv.querySelector('.search-clear-recent');
clearBtn?.addEventListener('click', () => {
clearRecentSearches();
hideResults();
});
isShowingRecent = true;
showResults();
};
// Show recent searches on focus when empty
searchInput.addEventListener('focus', () => {
if (searchInput.value.trim().length === 0) {
renderRecentSearches();
}
});
searchInput.addEventListener('input', debounce(() => { searchInput.addEventListener('input', debounce(() => {
const query = searchInput.value.trim(); const query = searchInput.value.trim();
if (query.length < 2) { if (query.length < 2) {
resultsDiv.innerHTML = ''; if (query.length === 0) {
renderRecentSearches();
} else {
resultsDiv.innerHTML = '';
hideResults();
}
if (statusEl) { if (statusEl) {
statusEl.textContent = ''; statusEl.textContent = '';
} }
hideResults();
return; return;
} }
isShowingRecent = false;
const results = search.search(query).slice(0, 10); const results = search.search(query).slice(0, 10);
if (results.length === 0) { if (results.length === 0) {
resultsDiv.innerHTML = '<div class="search-result-empty">No results found</div>'; resultsDiv.innerHTML = `
<div class="search-result-empty">
<div class="search-result-empty-icon">
<svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/>
<path d="M21 21l-4.35-4.35"/>
<path d="M8 8l6 6M14 8l-6 6"/>
</svg>
</div>
<div class="search-result-empty-title">No results found</div>
<div class="search-result-empty-hint">Try different keywords or check your spelling</div>
</div>
`;
if (statusEl) { if (statusEl) {
statusEl.textContent = 'No results found.'; statusEl.textContent = 'No results found.';
} }
} else { } else {
resultsDiv.innerHTML = results.map(item => ` // Add to recent searches when user gets results
addRecentSearch(query);
resultsDiv.innerHTML = results.map(item => {
const iconName = getResourceIcon(item.type);
return `
<button type="button" class="search-result" data-path="${escapeHtml(item.path)}" data-type="${escapeHtml(item.type)}"> <button type="button" class="search-result" data-path="${escapeHtml(item.path)}" data-type="${escapeHtml(item.type)}">
<span class="search-result-type">${getResourceIcon(item.type)}</span> <span class="search-result-type" data-icon="${iconName}">${getIconSvg(iconName)}</span>
<div> <div>
<div class="search-result-title">${search.highlight(item.title, query)}</div> <div class="search-result-title">${search.highlight(item.title, query)}</div>
<div class="search-result-description">${truncate(item.description, 60)}</div> <div class="search-result-description">${truncate(item.description, 60)}</div>
</div> </div>
</button> </button>
`).join(''); `}).join('');
if (statusEl) { if (statusEl) {
statusEl.textContent = `${results.length} result${results.length === 1 ? '' : 's'} available.`; statusEl.textContent = `${results.length} result${results.length === 1 ? '' : 's'} available.`;
@@ -171,6 +337,15 @@ export async function initHomepage(): Promise<void> {
hideResults(); hideResults();
} }
}); });
// Cmd/Ctrl + K to focus search
document.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
searchInput.focus();
searchInput.select();
}
});
} }
} }

View File

@@ -344,18 +344,58 @@ export function formatResourceType(type: string): string {
} }
/** /**
* Get icon for resource type * Get icon for resource type (returns SVG icon name)
*/ */
export function getResourceIcon(type: string): string { export function getResourceIcon(type: string): string {
const icons: Record<string, string> = { const icons: Record<string, string> = {
agent: "🤖", agent: "robot",
instruction: "📋", instruction: "document",
skill: "", skill: "lightning",
hook: "🪝", hook: "hook",
workflow: "", workflow: "workflow",
plugin: "🔌", plugin: "plug",
}; };
return icons[type] || "📄"; return icons[type] || "document";
}
// Icon definitions with fill/stroke type info
const iconDefs: Record<string, { path: string; fill?: boolean }> = {
// Agent icon - GitHub Primer's agent-24
robot: {
fill: true,
path: '<path d="M22.5 13.919v-.278a5.097 5.097 0 0 0-4.961-5.086.858.858 0 0 1-.754-.497l-.149-.327A6.414 6.414 0 0 0 10.81 4a6.133 6.133 0 0 0-6.13 6.32l.019.628a.863.863 0 0 1-.67.869A3.263 3.263 0 0 0 1.5 14.996v.108A3.397 3.397 0 0 0 4.896 18.5h1.577a.75.75 0 0 1 0 1.5H4.896A4.896 4.896 0 0 1 0 15.104v-.108a4.761 4.761 0 0 1 3.185-4.493l-.004-.137A7.633 7.633 0 0 1 10.81 2.5a7.911 7.911 0 0 1 7.176 4.58C21.36 7.377 24 10.207 24 13.641v.278a.75.75 0 0 1-1.5 0Z"/><path d="m12.306 11.77 3.374 3.375a.749.749 0 0 1 0 1.061l-3.375 3.375-.057.051a.751.751 0 0 1-1.004-.051.751.751 0 0 1-.051-1.004l.051-.057 2.845-2.845-2.844-2.844a.75.75 0 1 1 1.061-1.061ZM22.5 19.8H18a.75.75 0 0 1 0-1.5h4.5a.75.75 0 0 1 0 1.5Z"/>',
},
document: {
path: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>',
},
lightning: {
path: '<path d="M13 2 4.09 12.11a1.23 1.23 0 0 0 .13 1.72l.16.14a1.23 1.23 0 0 0 1.52 0L13 9.5V22l8.91-10.11a1.23 1.23 0 0 0-.13-1.72l-.16-.14a1.23 1.23 0 0 0-1.52 0L13 14.5V2Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>',
},
// Hook icon - GitHub Primer's sync-24
hook: {
fill: true,
path: '<path d="M3.38 8A9.502 9.502 0 0 1 12 2.5a9.502 9.502 0 0 1 9.215 7.182.75.75 0 1 0 1.456-.364C21.473 4.539 17.15 1 12 1a10.995 10.995 0 0 0-9.5 5.452V4.75a.75.75 0 0 0-1.5 0V8.5a1 1 0 0 0 1 1h3.75a.75.75 0 0 0 0-1.5H3.38Zm-.595 6.318a.75.75 0 0 0-1.455.364C2.527 19.461 6.85 23 12 23c4.052 0 7.592-2.191 9.5-5.451v1.701a.75.75 0 0 0 1.5 0V15.5a1 1 0 0 0-1-1h-3.75a.75.75 0 0 0 0 1.5h2.37A9.502 9.502 0 0 1 12 21.5c-4.446 0-8.181-3.055-9.215-7.182Z"/>',
},
// Workflow icon - GitHub Primer's workflow-24
workflow: {
fill: true,
path: '<path d="M1 3a2 2 0 0 1 2-2h6.5a2 2 0 0 1 2 2v6.5a2 2 0 0 1-2 2H7v4.063C7 16.355 7.644 17 8.438 17H12.5v-2.5a2 2 0 0 1 2-2H21a2 2 0 0 1 2 2V21a2 2 0 0 1-2 2h-6.5a2 2 0 0 1-2-2v-2.5H8.437A2.939 2.939 0 0 1 5.5 15.562V11.5H3a2 2 0 0 1-2-2Zm2-.5a.5.5 0 0 0-.5.5v6.5a.5.5 0 0 0 .5.5h6.5a.5.5 0 0 0 .5-.5V3a.5.5 0 0 0-.5-.5ZM14.5 14a.5.5 0 0 0-.5.5V21a.5.5 0 0 0 .5.5H21a.5.5 0 0 0 .5-.5v-6.5a.5.5 0 0 0-.5-.5Z"/>',
},
// Plug icon - GitHub Primer's plug-24
plug: {
fill: true,
path: '<path d="M7 11.5H2.938c-.794 0-1.438.644-1.438 1.437v8.313a.75.75 0 0 1-1.5 0v-8.312A2.939 2.939 0 0 1 2.937 10H7V6.151c0-.897.678-1.648 1.57-1.74l6.055-.626 1.006-1.174A1.752 1.752 0 0 1 16.96 2h1.29c.966 0 1.75.784 1.75 1.75V6h3.25a.75.75 0 0 1 0 1.5H20V14h3.25a.75.75 0 0 1 0 1.5H20v2.25a1.75 1.75 0 0 1-1.75 1.75h-1.29a1.75 1.75 0 0 1-1.329-.611l-1.006-1.174-6.055-.627A1.749 1.749 0 0 1 7 15.348Zm9.77-7.913v.001l-1.201 1.4a.75.75 0 0 1-.492.258l-6.353.657a.25.25 0 0 0-.224.249v9.196a.25.25 0 0 0 .224.249l6.353.657c.191.02.368.112.493.258l1.2 1.401a.252.252 0 0 0 .19.087h1.29a.25.25 0 0 0 .25-.25v-14a.25.25 0 0 0-.25-.25h-1.29a.252.252 0 0 0-.19.087Z"/>',
},
};
/**
* Get SVG icon HTML for resource type
*/
export function getResourceIconSvg(type: string, size = 20): string {
const iconName = getResourceIcon(type);
const icon = iconDefs[iconName] || iconDefs.document;
const fill = icon.fill ? 'fill="currentColor"' : 'fill="none"';
return `<svg viewBox="0 0 24 24" width="${size}" height="${size}" ${fill} aria-hidden="true">${icon.path}</svg>`;
} }
/** /**

File diff suppressed because it is too large Load Diff

View File

@@ -14,8 +14,8 @@
--sl-color-gray-3: #60607a; --sl-color-gray-3: #60607a;
--sl-color-gray-4: #30304a; --sl-color-gray-4: #30304a;
--sl-color-gray-5: #1a1a2e; --sl-color-gray-5: #1a1a2e;
--sl-color-gray-6: #111120; --sl-color-gray-6: #0d0d12;
--sl-color-black: #0a0a0f; --sl-color-black: #0d0d12;
--sl-font-system: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, --sl-font-system: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif; Arial, sans-serif;
@@ -37,8 +37,13 @@
--sl-color-gray-3: #30304a; --sl-color-gray-3: #30304a;
--sl-color-gray-4: #60607a; --sl-color-gray-4: #60607a;
--sl-color-gray-5: #9898a6; --sl-color-gray-5: #9898a6;
--sl-color-gray-6: #d0d0da; --sl-color-gray-6: #f5f5f7;
--sl-color-black: #f0f0f5; --sl-color-black: #fafafa;
}
:root[data-theme="light"] header.header {
background: rgba(250, 250, 250, 0.6) !important;
border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important;
} }
/* ── Sidebar readability ───────────────────────────────────── */ /* ── Sidebar readability ───────────────────────────────────── */
@@ -60,6 +65,55 @@ header .site-title:hover {
color: var(--sl-color-accent-high) !important; color: var(--sl-color-accent-high) !important;
} }
/* ── Unified background ─────────────────────────────────────── */
/* Clean header - solid background */
header.header {
background: var(--sl-color-gray-6) !important;
border-bottom: 1px solid var(--sl-color-gray-5) !important;
z-index: 1000;
position: relative;
}
/* Header inner content for alignment */
header.header .header-inner {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 100%;
}
header.header .header-actions {
display: flex;
align-items: center;
gap: 12px;
}
/* Hide social icons in header - GitHub link moved to hero */
header .social-icons {
display: none !important;
}
/* Move theme toggle inside header - align with header elements */
.theme-toggle-container {
position: fixed !important;
top: 12px !important;
right: 80px !important;
z-index: 1001;
}
/* On custom splash pages, body already has the gradient bg.
Make Starlight's wrapper layers transparent so it shows through. */
body:has(#main-content) {
--sl-color-bg: transparent;
--sl-color-bg-nav: transparent;
}
/* ── Hide Starlight's built-in theme selector (dropdown) ─── */
/* We use our own custom ThemeToggle component instead */
starlight-theme-select {
display: none !important;
}
/* ── Browse Resources sidebar group ────────────────────────── */ /* ── Browse Resources sidebar group ────────────────────────── */
nav[aria-label="Main"] > ul > li:first-child details summary, nav[aria-label="Main"] > ul > li:first-child details summary,
nav[aria-label="Main"] > ul > li:first-child > a { nav[aria-label="Main"] > ul > li:first-child > a {