Consolidate listing pages with unified grid cards and modal system (#2101)

* Prototype extension details modal

- Add detail popup modal for extension cards with full metadata and gallery
- Implement image gallery with thumbnail strip and main image selection
- Add modal styling and positioning in global.css
- Connect card click handlers to open modal with extension data

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix accessibility issues with modal focus restoration

- Add missing listing-cards-page class to agents.astro page root
- Pass focusable button element to openCardDetailsModal instead of article
- Fixes focus restoration for keyboard users when closing modal
- Applied fix across all listing pages (agents, instructions, hooks, plugins, skills, workflows)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address remaining PR review feedback

- Fix extension modal ARIA state by setting aria-current to "true" and removing it when inactive
- Use focusable .resource-preview as modal trigger for extension thumbnail/click/keyboard paths
- Extract shared multi-select helpers into pages/select-utils.ts and reuse across instructions/hooks/plugins/workflows
- Remove unused card-model.ts to avoid dead/overlapping type definitions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Aaron Powell
2026-06-24 09:47:39 +10:00
committed by GitHub
parent ec8cb2a8ae
commit 8cdeb2d2ed
25 changed files with 1777 additions and 737 deletions
+112 -3
View File
@@ -2,7 +2,6 @@
* Modal functionality for file viewing
*/
import { marked } from "marked";
import {
fetchFileContent,
fetchData,
@@ -18,7 +17,6 @@ import {
sanitizeUrl,
REPO_IDENTIFIER,
} from "./utils";
import fm from "front-matter";
type ModalViewMode = "rendered" | "raw";
@@ -352,7 +350,11 @@ async function renderCurrentFileContent(): Promise<void> {
const container = ensureDivContent("modal-rendered-content");
if (!container) return;
const { body: markdownBody } = fm(currentFileContent);
const [{ marked }, { default: fm }] = await Promise.all([
import("marked"),
import("front-matter"),
]);
const { body: markdownBody } = fm<string>(currentFileContent);
container.innerHTML = marked(markdownBody, { async: false });
} else {
await renderHighlightedCode(currentFileContent, currentFilePath);
@@ -452,6 +454,19 @@ interface PluginsData {
let pluginsCache: PluginsData | null = null;
interface OpenCardDetailsRequest {
title: string;
description: string;
previewIcon?: string;
previewText?: string;
metaHtml?: string;
tagsHtml?: string;
actionsHtml?: string;
detailsHtml?: string;
contentClassName?: string;
trigger?: HTMLElement;
}
/**
* Get all focusable elements within a container
*/
@@ -731,6 +746,19 @@ export function setupModal(): void {
}
});
document.addEventListener("click", async (event) => {
const target = event.target as HTMLElement;
const openFileButton = target.closest<HTMLElement>("[data-open-file-path]");
if (!openFileButton) return;
const filePath = openFileButton.dataset.openFilePath;
if (!filePath) return;
event.preventDefault();
const fileType = openFileButton.dataset.openFileType || getResourceType(filePath);
await openFileModal(filePath, fileType, true, openFileButton);
});
// Check for deep link on initial load
handleHashChange();
}
@@ -894,6 +922,8 @@ export async function openFileModal(
const closeBtn = document.getElementById("close-modal");
if (!modal || !title) return;
modal.classList.remove("details-mode");
currentFilePath = filePath;
currentFileType = type;
currentViewMode = "raw";
@@ -989,6 +1019,85 @@ export async function openFileModal(
await renderCurrentFileContent();
}
export function openCardDetailsModal({
title,
description,
previewIcon = "📄",
previewText = "",
metaHtml = "",
tagsHtml = "",
actionsHtml = "",
detailsHtml = "",
contentClassName = "modal-card-details",
trigger,
}: OpenCardDetailsRequest): void {
const modal = document.getElementById("file-modal");
const modalTitle = document.getElementById("modal-title");
const closeBtn = document.getElementById("close-modal");
const modalBody = getModalBody();
if (!modal || !modalTitle || !modalBody) return;
triggerElement = trigger || (document.activeElement as HTMLElement);
if (!originalDocumentTitle) {
originalDocumentTitle = document.title;
}
currentFilePath = null;
currentFileContent = null;
currentFileType = "details";
currentViewMode = "raw";
hideSkillFileSwitcher();
modal.classList.add("details-mode");
modalTitle.textContent = title;
document.title = `${title} | Awesome GitHub Copilot`;
const content = ensureDivContent(contentClassName);
if (!content) return;
content.innerHTML =
detailsHtml ||
`
<div class="resource-details-body modal-card-details-body">
<div class="resource-details-preview">
<div class="resource-details-preview-icon" aria-hidden="true">${escapeHtml(previewIcon)}</div>
${
previewText
? `<p class="resource-details-preview-text">${escapeHtml(previewText)}</p>`
: ""
}
</div>
<div class="resource-details-content">
<p class="resource-details-description">${escapeHtml(description)}</p>
${
metaHtml
? `<div class="resource-meta resource-details-meta">${metaHtml}</div>`
: ""
}
${
tagsHtml
? `<div class="resource-keywords resource-details-tags">${tagsHtml}</div>`
: ""
}
${
actionsHtml
? `<div class="resource-actions resource-details-actions">${actionsHtml}</div>`
: ""
}
</div>
</div>
`;
modalBody.scrollTop = 0;
modal.classList.remove("hidden");
modal.classList.add("visible");
setTimeout(() => {
closeBtn?.focus();
}, 0);
}
/**
* Open plugin modal with item list
*/