mirror of
https://github.com/github/awesome-copilot.git
synced 2026-07-14 01:51:02 +00:00
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:
@@ -2,20 +2,18 @@
|
||||
* Instructions page functionality
|
||||
*/
|
||||
import {
|
||||
createChoices,
|
||||
getChoicesValues,
|
||||
setChoicesValues,
|
||||
type Choices,
|
||||
} from '../choices';
|
||||
import {
|
||||
escapeHtml,
|
||||
fetchData,
|
||||
formatRelativeTime,
|
||||
getQueryParam,
|
||||
getQueryParamValues,
|
||||
setupDropdownCloseHandlers,
|
||||
getVSCodeInstallUrl,
|
||||
setupActionHandlers,
|
||||
setupDropdownCloseHandlers,
|
||||
updateQueryParams,
|
||||
} from '../utils';
|
||||
import { setupModal, openFileModal } from '../modal';
|
||||
import { openCardDetailsModal, setupModal } from '../modal';
|
||||
import { clearSelectValues, getSelectValues, setSelectValues } from './select-utils';
|
||||
import {
|
||||
renderInstructionsHtml,
|
||||
sortInstructions,
|
||||
@@ -37,12 +35,13 @@ interface InstructionsData {
|
||||
};
|
||||
}
|
||||
|
||||
const resourceType = 'instruction';
|
||||
let allItems: Instruction[] = [];
|
||||
let extensionSelect: Choices;
|
||||
let instructionByPath = new Map<string, Instruction>();
|
||||
let extensionSelectEl: HTMLSelectElement | null = null;
|
||||
let currentFilters = { extensions: [] as string[] };
|
||||
let currentSort: InstructionSortOption = 'title';
|
||||
let resourceListHandlersReady = false;
|
||||
let modalReady = false;
|
||||
|
||||
function sortItems(items: Instruction[]): Instruction[] {
|
||||
return sortInstructions(items, currentSort);
|
||||
@@ -53,11 +52,14 @@ function applyFiltersAndRender(): void {
|
||||
let results = [...allItems];
|
||||
|
||||
if (currentFilters.extensions.length > 0) {
|
||||
results = results.filter(item => {
|
||||
if (currentFilters.extensions.includes('(none)') && (!item.extensions || item.extensions.length === 0)) {
|
||||
results = results.filter((item) => {
|
||||
if (
|
||||
currentFilters.extensions.includes('(none)') &&
|
||||
(!item.extensions || item.extensions.length === 0)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return item.extensions?.some(ext => currentFilters.extensions.includes(ext));
|
||||
return item.extensions?.some((ext) => currentFilters.extensions.includes(ext));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,6 +80,53 @@ function renderItems(items: Instruction[]): void {
|
||||
list.innerHTML = renderInstructionsHtml(items);
|
||||
}
|
||||
|
||||
function openInstructionDetailsModal(path: string, trigger?: HTMLElement): void {
|
||||
const item = instructionByPath.get(path);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metaParts: string[] = [];
|
||||
const applyToText = Array.isArray(item.applyTo) ? item.applyTo.join(', ') : item.applyTo;
|
||||
if (applyToText) {
|
||||
metaParts.push(`<span class="resource-tag">applies to: ${escapeHtml(applyToText)}</span>`);
|
||||
}
|
||||
|
||||
metaParts.push(
|
||||
...(item.extensions || []).map(
|
||||
(extension) => `<span class="resource-tag tag-extension">${escapeHtml(extension)}</span>`
|
||||
)
|
||||
);
|
||||
|
||||
if (item.lastUpdated) {
|
||||
metaParts.push(`<span class="last-updated">Updated ${escapeHtml(formatRelativeTime(item.lastUpdated))}</span>`);
|
||||
}
|
||||
|
||||
const vscodeUrl = getVSCodeInstallUrl('instructions', path, false);
|
||||
const insidersUrl = getVSCodeInstallUrl('instructions', path, true);
|
||||
const actions = [
|
||||
vscodeUrl
|
||||
? `<a class="btn btn-primary btn-small" href="${escapeHtml(vscodeUrl)}" target="_blank" rel="noopener noreferrer">Install (VS Code)</a>`
|
||||
: '',
|
||||
insidersUrl
|
||||
? `<a class="btn btn-secondary btn-small" href="${escapeHtml(insidersUrl)}" target="_blank" rel="noopener noreferrer">Install (Insiders)</a>`
|
||||
: '',
|
||||
`<button class="btn btn-secondary btn-small" type="button" data-open-file-path="${escapeHtml(
|
||||
path
|
||||
)}" data-open-file-type="instruction">Source</button>`,
|
||||
].filter(Boolean);
|
||||
|
||||
openCardDetailsModal({
|
||||
title: item.title,
|
||||
description: item.description || 'No description',
|
||||
previewIcon: '📋',
|
||||
previewText: 'Instruction metadata and install options',
|
||||
metaHtml: metaParts.join(''),
|
||||
actionsHtml: actions.join(''),
|
||||
trigger,
|
||||
});
|
||||
}
|
||||
|
||||
function setupResourceListHandlers(list: HTMLElement | null): void {
|
||||
if (!list || resourceListHandlersReady) return;
|
||||
|
||||
@@ -88,9 +137,10 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
|
||||
}
|
||||
|
||||
const item = target.closest('.resource-item') as HTMLElement | null;
|
||||
const button = item?.querySelector('.resource-preview') as HTMLElement | undefined;
|
||||
const path = item?.dataset.path;
|
||||
if (path) {
|
||||
openFileModal(path, resourceType);
|
||||
openInstructionDetailsModal(path, button);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -108,7 +158,12 @@ function syncUrlState(): void {
|
||||
export async function initInstructionsPage(): Promise<void> {
|
||||
const list = document.getElementById('resource-list');
|
||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
||||
const sortSelect = document.getElementById('sort-select') as HTMLSelectElement;
|
||||
const sortSelect = document.getElementById('sort-select') as HTMLSelectElement | null;
|
||||
|
||||
if (!modalReady) {
|
||||
setupModal();
|
||||
modalReady = true;
|
||||
}
|
||||
|
||||
setupResourceListHandlers(list as HTMLElement | null);
|
||||
|
||||
@@ -119,24 +174,33 @@ export async function initInstructionsPage(): Promise<void> {
|
||||
}
|
||||
|
||||
allItems = data.items;
|
||||
instructionByPath = new Map(allItems.map((item) => [item.path, item]));
|
||||
|
||||
extensionSelect = createChoices('#filter-extension', { placeholderValue: 'All Extensions' });
|
||||
extensionSelect.setChoices(data.filters.extensions.map(e => ({ value: e, label: e })), 'value', 'label', true);
|
||||
extensionSelectEl = document.getElementById('filter-extension') as HTMLSelectElement | null;
|
||||
if (extensionSelectEl) {
|
||||
extensionSelectEl.innerHTML = '';
|
||||
data.filters.extensions.forEach((ext) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = ext;
|
||||
option.textContent = ext;
|
||||
extensionSelectEl?.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
const initialExtensions = getQueryParamValues('extension').filter(extension => data.filters.extensions.includes(extension));
|
||||
const initialExtensions = getQueryParamValues('extension').filter((extension) => data.filters.extensions.includes(extension));
|
||||
const initialSort = getQueryParam('sort');
|
||||
|
||||
if (initialExtensions.length > 0) {
|
||||
currentFilters.extensions = initialExtensions;
|
||||
setChoicesValues(extensionSelect, initialExtensions);
|
||||
setSelectValues(extensionSelectEl, initialExtensions);
|
||||
}
|
||||
if (initialSort === 'lastUpdated') {
|
||||
currentSort = initialSort;
|
||||
if (sortSelect) sortSelect.value = initialSort;
|
||||
}
|
||||
|
||||
document.getElementById('filter-extension')?.addEventListener('change', () => {
|
||||
currentFilters.extensions = getChoicesValues(extensionSelect);
|
||||
extensionSelectEl?.addEventListener('change', () => {
|
||||
currentFilters.extensions = getSelectValues(extensionSelectEl);
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
@@ -150,14 +214,13 @@ export async function initInstructionsPage(): Promise<void> {
|
||||
clearFiltersBtn?.addEventListener('click', () => {
|
||||
currentFilters = { extensions: [] };
|
||||
currentSort = 'title';
|
||||
extensionSelect.removeActiveItems();
|
||||
clearSelectValues(extensionSelectEl);
|
||||
if (sortSelect) sortSelect.value = 'title';
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
setupModal();
|
||||
setupDropdownCloseHandlers();
|
||||
setupActionHandlers();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user