mirror of
https://github.com/github/awesome-copilot.git
synced 2026-06-17 13:11:27 +00:00
chore: publish from staged
This commit is contained in:
@@ -42,7 +42,8 @@ jobs:
|
||||
if (parts[0] === EXTENSIONS_DIR && parts.length >= 2) {
|
||||
const extName = parts[1];
|
||||
// Skip the external-assets directory — it's not a canvas extension
|
||||
if (extName !== EXTERNAL_ASSETS_DIR) {
|
||||
// Also skip external.json and other files at extensions root level
|
||||
if (extName !== EXTERNAL_ASSETS_DIR && !extName.includes('.')) {
|
||||
changedExtDirs.add(path.join(EXTENSIONS_DIR, extName));
|
||||
}
|
||||
}
|
||||
|
||||
+452
-69
@@ -674,11 +674,33 @@ function generatePluginsData(gitDates) {
|
||||
/**
|
||||
* Generate canvas extensions metadata
|
||||
*/
|
||||
function getExtensionAssetInfo(extensionDir, relPath, ref) {
|
||||
function getImageMimeType(filePath) {
|
||||
const extension = path.extname(filePath).toLowerCase();
|
||||
const mimeByExtension = {
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".webp": "image/webp",
|
||||
".gif": "image/gif",
|
||||
};
|
||||
return mimeByExtension[extension] || "application/octet-stream";
|
||||
}
|
||||
|
||||
function resolveImageUrl(value, ref) {
|
||||
const normalized = normalizeText(value);
|
||||
if (!normalized) return null;
|
||||
if (/^https?:\/\//i.test(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
const repoPath = normalized.replace(/\\/g, "/").replace(/^\/+/, "");
|
||||
return buildRepoImageUrl(repoPath, ref);
|
||||
}
|
||||
|
||||
function getImageAssetFiles(extensionDir) {
|
||||
const assetDir = path.join(extensionDir, "assets");
|
||||
|
||||
if (!fs.existsSync(assetDir)) {
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
|
||||
const imageExtensions = new Set([
|
||||
@@ -689,7 +711,35 @@ function getExtensionAssetInfo(extensionDir, relPath, ref) {
|
||||
".gif",
|
||||
]);
|
||||
|
||||
const preferredNames = [
|
||||
return fs
|
||||
.readdirSync(assetDir)
|
||||
.filter((file) => imageExtensions.has(path.extname(file).toLowerCase()))
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function pickAssetFile(files, preferredNames) {
|
||||
const preferredLookup = new Set(preferredNames.map((name) => name.toLowerCase()));
|
||||
for (const file of files) {
|
||||
if (preferredLookup.has(file.toLowerCase())) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return files[0] || null;
|
||||
}
|
||||
|
||||
function getExtensionAssetInfo(extensionDir, relPath, ref) {
|
||||
const files = getImageAssetFiles(extensionDir);
|
||||
|
||||
if (files.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const iconAsset = pickAssetFile(files, [
|
||||
"icon.png",
|
||||
"icon.jpg",
|
||||
"icon.jpeg",
|
||||
"icon.webp",
|
||||
"icon.gif",
|
||||
"preview.png",
|
||||
"preview.jpg",
|
||||
"preview.jpeg",
|
||||
@@ -705,34 +755,52 @@ function getExtensionAssetInfo(extensionDir, relPath, ref) {
|
||||
"image.jpeg",
|
||||
"image.webp",
|
||||
"image.gif",
|
||||
];
|
||||
]);
|
||||
const galleryAsset = pickAssetFile(files, [
|
||||
"gallery.png",
|
||||
"gallery.jpg",
|
||||
"gallery.jpeg",
|
||||
"gallery.webp",
|
||||
"gallery.gif",
|
||||
"preview.png",
|
||||
"preview.jpg",
|
||||
"preview.jpeg",
|
||||
"preview.webp",
|
||||
"preview.gif",
|
||||
"screenshot.png",
|
||||
"screenshot.jpg",
|
||||
"screenshot.jpeg",
|
||||
"screenshot.webp",
|
||||
"screenshot.gif",
|
||||
"image.png",
|
||||
"image.jpg",
|
||||
"image.jpeg",
|
||||
"image.webp",
|
||||
"image.gif",
|
||||
]);
|
||||
|
||||
for (const candidate of preferredNames) {
|
||||
const candidatePath = path.join(assetDir, candidate);
|
||||
if (fs.existsSync(candidatePath)) {
|
||||
const assetPath = `${relPath}/assets/${candidate}`;
|
||||
return {
|
||||
assetPath,
|
||||
imageUrl: buildRepoImageUrl(assetPath, ref),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const files = fs
|
||||
.readdirSync(assetDir)
|
||||
.filter((file) => imageExtensions.has(path.extname(file).toLowerCase()))
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
if (files.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const assetFile = files[0];
|
||||
const assetPath = `${relPath}/assets/${assetFile}`;
|
||||
const iconFile = iconAsset || galleryAsset;
|
||||
const galleryFile = galleryAsset || iconAsset;
|
||||
const iconPath = iconFile ? `${relPath}/assets/${iconFile}` : null;
|
||||
const galleryPath = galleryFile ? `${relPath}/assets/${galleryFile}` : null;
|
||||
|
||||
return {
|
||||
assetPath,
|
||||
imageUrl: buildRepoImageUrl(assetPath, ref),
|
||||
screenshots: {
|
||||
icon: iconPath
|
||||
? {
|
||||
path: iconPath,
|
||||
type: getImageMimeType(iconPath),
|
||||
}
|
||||
: null,
|
||||
gallery: galleryPath
|
||||
? {
|
||||
path: galleryPath,
|
||||
type: getImageMimeType(galleryPath),
|
||||
}
|
||||
: null,
|
||||
},
|
||||
assetPath: iconPath,
|
||||
imageUrl: iconPath ? buildRepoImageUrl(iconPath, ref) : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -744,11 +812,174 @@ function buildRepoImageUrl(assetPath, ref) {
|
||||
return `https://raw.githubusercontent.com/github/awesome-copilot/${ref}/${encodedAssetPath}`;
|
||||
}
|
||||
|
||||
function generateExtensionsData(gitDates, commitSha) {
|
||||
const extensions = [];
|
||||
function extractCanvasMetadataFromSource(source) {
|
||||
const constants = new Map();
|
||||
const constantPattern =
|
||||
/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|`([^`$]*)`)\s*;/g;
|
||||
let constantMatch = constantPattern.exec(source);
|
||||
while (constantMatch) {
|
||||
const key = constantMatch[1];
|
||||
const value = constantMatch[2] ?? constantMatch[3] ?? constantMatch[4] ?? "";
|
||||
constants.set(key, value.replace(/\\n/g, "\n").trim());
|
||||
constantMatch = constantPattern.exec(source);
|
||||
}
|
||||
|
||||
function resolveExpression(expr) {
|
||||
const trimmed = normalizeText(expr);
|
||||
if (!trimmed) return null;
|
||||
if (
|
||||
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
||||
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
||||
) {
|
||||
return trimmed
|
||||
.slice(1, -1)
|
||||
.replace(/\\n/g, "\n")
|
||||
.replace(/\\"/g, '"')
|
||||
.replace(/\\'/g, "'");
|
||||
}
|
||||
if (trimmed.startsWith("`") && trimmed.endsWith("`") && !trimmed.includes("${")) {
|
||||
return trimmed.slice(1, -1);
|
||||
}
|
||||
return constants.get(trimmed) || null;
|
||||
}
|
||||
|
||||
function findMatchingBrace(startIndex) {
|
||||
let depth = 0;
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
let inTemplate = false;
|
||||
let escaped = false;
|
||||
for (let i = startIndex; i < source.length; i++) {
|
||||
const char = source[i];
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (char === "\\") {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (!inDouble && !inTemplate && char === "'" && !inSingle) {
|
||||
inSingle = true;
|
||||
continue;
|
||||
}
|
||||
if (inSingle && char === "'") {
|
||||
inSingle = false;
|
||||
continue;
|
||||
}
|
||||
if (!inSingle && !inTemplate && char === '"' && !inDouble) {
|
||||
inDouble = true;
|
||||
continue;
|
||||
}
|
||||
if (inDouble && char === '"') {
|
||||
inDouble = false;
|
||||
continue;
|
||||
}
|
||||
if (!inSingle && !inDouble && char === "`" && !inTemplate) {
|
||||
inTemplate = true;
|
||||
continue;
|
||||
}
|
||||
if (inTemplate && char === "`") {
|
||||
inTemplate = false;
|
||||
continue;
|
||||
}
|
||||
if (inSingle || inDouble || inTemplate) {
|
||||
continue;
|
||||
}
|
||||
if (char === "{") depth++;
|
||||
if (char === "}") {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function readProp(head, key) {
|
||||
const pattern = new RegExp(`\\b${key}\\s*:\\s*([^,\\n]+)`);
|
||||
const match = pattern.exec(head);
|
||||
return resolveExpression(match?.[1]);
|
||||
}
|
||||
|
||||
const canvases = [];
|
||||
let cursor = 0;
|
||||
while (cursor < source.length) {
|
||||
const createCanvasIndex = source.indexOf("createCanvas(", cursor);
|
||||
if (createCanvasIndex === -1) {
|
||||
break;
|
||||
}
|
||||
const objectStart = source.indexOf("{", createCanvasIndex);
|
||||
if (objectStart === -1) {
|
||||
break;
|
||||
}
|
||||
const objectEnd = findMatchingBrace(objectStart);
|
||||
if (objectEnd === -1) {
|
||||
break;
|
||||
}
|
||||
const objectContent = source.slice(objectStart + 1, objectEnd);
|
||||
const header = objectContent.slice(0, 1400);
|
||||
const id = readProp(header, "id");
|
||||
const displayName = readProp(header, "displayName");
|
||||
const description = readProp(header, "description");
|
||||
if (id || displayName || description) {
|
||||
canvases.push({
|
||||
id: id || null,
|
||||
displayName: displayName || null,
|
||||
description: description || null,
|
||||
});
|
||||
}
|
||||
cursor = objectEnd + 1;
|
||||
}
|
||||
|
||||
return canvases;
|
||||
}
|
||||
|
||||
function getExtensionCanvasFiles(extensionDir) {
|
||||
const queue = [extensionDir];
|
||||
const files = [];
|
||||
while (queue.length > 0) {
|
||||
const currentDir = queue.shift();
|
||||
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const absolutePath = path.join(currentDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
queue.push(absolutePath);
|
||||
} else if (entry.isFile() && entry.name.endsWith(".mjs")) {
|
||||
files.push(absolutePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return files.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function normalizeExternalScreenshotRole(value, ref) {
|
||||
if (!value) return null;
|
||||
if (typeof value === "string") {
|
||||
const type = getImageMimeType(value);
|
||||
return {
|
||||
path: value.replace(/\\/g, "/"),
|
||||
type,
|
||||
imageUrl: resolveImageUrl(value, ref),
|
||||
};
|
||||
}
|
||||
const pathValue = normalizeText(value.path);
|
||||
const urlValue = normalizeText(value.url);
|
||||
if (!pathValue && !urlValue) return null;
|
||||
const imagePath = pathValue ? pathValue.replace(/\\/g, "/") : null;
|
||||
const type = normalizeText(value.type) || getImageMimeType(imagePath || urlValue);
|
||||
const imageUrl = resolveImageUrl(urlValue || imagePath, ref);
|
||||
return {
|
||||
path: imagePath,
|
||||
type,
|
||||
imageUrl,
|
||||
};
|
||||
}
|
||||
|
||||
function generateCanvasManifest(gitDates, commitSha) {
|
||||
const items = [];
|
||||
|
||||
if (!fs.existsSync(EXTENSIONS_DIR)) {
|
||||
return { items: [] };
|
||||
return { items: [], filters: { keywords: [] } };
|
||||
}
|
||||
|
||||
const extensionDirs = fs
|
||||
@@ -761,32 +992,61 @@ function generateExtensionsData(gitDates, commitSha) {
|
||||
"extension.mjs"
|
||||
);
|
||||
return fs.existsSync(extensionEntryPoint);
|
||||
});
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
for (const dir of extensionDirs) {
|
||||
const relPath = `extensions/${dir.name}`;
|
||||
const assetInfo = getExtensionAssetInfo(
|
||||
path.join(EXTENSIONS_DIR, dir.name),
|
||||
relPath,
|
||||
commitSha
|
||||
);
|
||||
const extensionDir = path.join(EXTENSIONS_DIR, dir.name);
|
||||
const packageJsonPath = path.join(extensionDir, "package.json");
|
||||
const packageJson = fs.existsSync(packageJsonPath)
|
||||
? JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
|
||||
: {};
|
||||
const keywords = Array.isArray(packageJson.keywords)
|
||||
? [...new Set(packageJson.keywords.filter((keyword) => typeof keyword === "string").map((keyword) => keyword.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b))
|
||||
: [];
|
||||
const extensionDescription = normalizeText(packageJson.description, "Canvas extension");
|
||||
const extensionName = normalizeText(packageJson.name, dir.name);
|
||||
const extensionVersion = normalizeText(packageJson.version, "1.0.0");
|
||||
const screenshots = getExtensionAssetInfo(extensionDir, relPath, commitSha);
|
||||
const canvasFiles = getExtensionCanvasFiles(extensionDir);
|
||||
const canvases = [];
|
||||
for (const canvasFile of canvasFiles) {
|
||||
const source = fs.readFileSync(canvasFile, "utf-8");
|
||||
canvases.push(...extractCanvasMetadataFromSource(source));
|
||||
}
|
||||
const canvasEntries = canvases.length > 0
|
||||
? canvases
|
||||
: [{ id: dir.name, displayName: formatDisplayName(dir.name), description: extensionDescription }];
|
||||
const installUrl = `https://github.com/github/awesome-copilot/tree/${commitSha}/${relPath.replace(
|
||||
/\\/g,
|
||||
"/"
|
||||
)}`;
|
||||
|
||||
extensions.push({
|
||||
id: dir.name,
|
||||
name: formatDisplayName(dir.name),
|
||||
description: "Canvas extension",
|
||||
path: relPath,
|
||||
ref: commitSha,
|
||||
lastUpdated: getDirectoryLastUpdated(gitDates, relPath),
|
||||
imageUrl: assetInfo?.imageUrl || null,
|
||||
assetPath: assetInfo?.assetPath || null,
|
||||
installUrl: `https://github.com/github/awesome-copilot/tree/${commitSha}/${relPath.replace(
|
||||
/\\/g,
|
||||
"/"
|
||||
)}`,
|
||||
sourceUrl: null,
|
||||
external: false,
|
||||
});
|
||||
for (const canvas of canvasEntries) {
|
||||
const canvasId = normalizeText(canvas.id, dir.name);
|
||||
const canvasName = normalizeText(canvas.displayName, formatDisplayName(canvasId));
|
||||
const canvasDescription = normalizeText(extensionDescription, canvas.description);
|
||||
items.push({
|
||||
id: canvasId,
|
||||
canvasId,
|
||||
extensionId: dir.name,
|
||||
extensionName,
|
||||
name: canvasName,
|
||||
version: extensionVersion,
|
||||
description: canvasDescription,
|
||||
path: relPath,
|
||||
ref: commitSha,
|
||||
lastUpdated: getDirectoryLastUpdated(gitDates, relPath),
|
||||
screenshots: screenshots?.screenshots || { icon: null, gallery: null },
|
||||
imageUrl: screenshots?.imageUrl || null,
|
||||
assetPath: screenshots?.assetPath || null,
|
||||
installUrl,
|
||||
sourceUrl: null,
|
||||
external: false,
|
||||
keywords,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const externalJsonPath = path.join(EXTENSIONS_DIR, "external.json");
|
||||
@@ -805,27 +1065,58 @@ function generateExtensionsData(gitDates, commitSha) {
|
||||
}
|
||||
|
||||
const id = normalizeText(ext?.id || name.toLowerCase().replace(/\s+/g, "-"));
|
||||
let imageUrl = normalizeText(ext?.imageUrl);
|
||||
let assetPath = null;
|
||||
const imagePath = normalizeText(ext?.imagePath);
|
||||
if (!imageUrl && imagePath) {
|
||||
const repoAssetPath = imagePath.replace(/\\/g, "/");
|
||||
imageUrl = buildRepoImageUrl(repoAssetPath, commitSha);
|
||||
assetPath = repoAssetPath;
|
||||
}
|
||||
const keywords = Array.isArray(ext?.keywords)
|
||||
? [...new Set(ext.keywords.filter((keyword) => typeof keyword === "string").map((keyword) => keyword.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b))
|
||||
: Array.isArray(ext?.tags)
|
||||
? [...new Set(ext.tags.filter((keyword) => typeof keyword === "string").map((keyword) => keyword.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b))
|
||||
: [];
|
||||
const iconScreenshot =
|
||||
normalizeExternalScreenshotRole(ext?.screenshots?.icon, commitSha) ||
|
||||
normalizeExternalScreenshotRole(ext?.iconPath, commitSha) ||
|
||||
normalizeExternalScreenshotRole(ext?.imagePath, commitSha) ||
|
||||
normalizeExternalScreenshotRole(ext?.iconUrl, commitSha) ||
|
||||
normalizeExternalScreenshotRole(ext?.imageUrl, commitSha);
|
||||
const galleryScreenshot =
|
||||
normalizeExternalScreenshotRole(ext?.screenshots?.gallery, commitSha) ||
|
||||
normalizeExternalScreenshotRole(ext?.galleryPath, commitSha) ||
|
||||
normalizeExternalScreenshotRole(ext?.galleryUrl, commitSha) ||
|
||||
iconScreenshot;
|
||||
const screenshots = {
|
||||
icon: iconScreenshot
|
||||
? {
|
||||
path: iconScreenshot.path,
|
||||
type: iconScreenshot.type,
|
||||
}
|
||||
: null,
|
||||
gallery: galleryScreenshot
|
||||
? {
|
||||
path: galleryScreenshot.path,
|
||||
type: galleryScreenshot.type,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
const imageUrl = iconScreenshot?.imageUrl || null;
|
||||
const assetPath = iconScreenshot?.path || null;
|
||||
const canvasId = normalizeText(ext?.canvasId, id);
|
||||
|
||||
extensions.push({
|
||||
items.push({
|
||||
id,
|
||||
canvasId,
|
||||
extensionId: id,
|
||||
extensionName: name,
|
||||
name,
|
||||
version: normalizeText(ext?.version, "1.0.0"),
|
||||
description: normalizeText(ext?.description, "External canvas extension"),
|
||||
path: null,
|
||||
ref: null,
|
||||
lastUpdated: null,
|
||||
imageUrl: imageUrl || null,
|
||||
screenshots,
|
||||
imageUrl,
|
||||
assetPath,
|
||||
installUrl,
|
||||
sourceUrl: sourceUrl || null,
|
||||
external: true,
|
||||
keywords,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -834,11 +1125,98 @@ function generateExtensionsData(gitDates, commitSha) {
|
||||
}
|
||||
}
|
||||
|
||||
const sortedExtensions = extensions.sort((a, b) =>
|
||||
a.name.localeCompare(b.name)
|
||||
);
|
||||
const sortedItems = items.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const keywordFilters = [...new Set(sortedItems.flatMap((item) => item.keywords || []))]
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
return { items: sortedExtensions };
|
||||
return {
|
||||
items: sortedItems,
|
||||
filters: {
|
||||
keywords: keywordFilters,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function generateExtensionsData(canvasManifestData) {
|
||||
if (!canvasManifestData || !Array.isArray(canvasManifestData.items)) {
|
||||
return { items: [], filters: { keywords: [] } };
|
||||
}
|
||||
|
||||
const items = canvasManifestData.items.map((item) => ({
|
||||
...item,
|
||||
keywords: Array.isArray(item.keywords) ? item.keywords : [],
|
||||
screenshots: item.screenshots || { icon: null, gallery: null },
|
||||
}));
|
||||
const filters = {
|
||||
keywords: [...new Set(items.flatMap((item) => item.keywords))]
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.localeCompare(b)),
|
||||
};
|
||||
|
||||
return { items, filters };
|
||||
}
|
||||
|
||||
function writePerExtensionCanvasManifests(canvasManifestData) {
|
||||
const manifests = new Map();
|
||||
|
||||
function toExtensionRelativePath(assetPath, extensionId) {
|
||||
const normalizedPath = normalizeText(assetPath).replace(/\\/g, "/");
|
||||
if (!normalizedPath) return null;
|
||||
const prefix = `extensions/${extensionId}/`;
|
||||
return normalizedPath.startsWith(prefix)
|
||||
? normalizedPath.slice(prefix.length)
|
||||
: normalizedPath;
|
||||
}
|
||||
|
||||
function toRelativeScreenshots(screenshots, extensionId) {
|
||||
if (!screenshots) return { icon: null, gallery: null };
|
||||
const toRelativeEntry = (entry) =>
|
||||
entry
|
||||
? {
|
||||
...entry,
|
||||
path: toExtensionRelativePath(entry.path, extensionId),
|
||||
}
|
||||
: null;
|
||||
return {
|
||||
icon: toRelativeEntry(screenshots.icon),
|
||||
gallery: toRelativeEntry(screenshots.gallery),
|
||||
};
|
||||
}
|
||||
|
||||
for (const item of canvasManifestData.items || []) {
|
||||
if (!item || item.external || !item.extensionId || !item.path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We assume one canvas per extension folder.
|
||||
if (manifests.has(item.extensionId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
manifests.set(item.extensionId, {
|
||||
id: item.canvasId || item.id,
|
||||
name: item.name,
|
||||
description: item.description || "Canvas extension",
|
||||
version: item.version || "1.0.0",
|
||||
keywords: Array.isArray(item.keywords)
|
||||
? [...new Set(item.keywords)].sort((a, b) => a.localeCompare(b))
|
||||
: [],
|
||||
screenshots: toRelativeScreenshots(
|
||||
item.screenshots || { icon: null, gallery: null },
|
||||
item.extensionId
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
for (const [extensionId, manifest] of manifests.entries()) {
|
||||
const canvasManifestPath = path.join(
|
||||
EXTENSIONS_DIR,
|
||||
extensionId,
|
||||
"canvas.json"
|
||||
);
|
||||
fs.writeFileSync(canvasManifestPath, JSON.stringify(manifest, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1181,9 +1559,12 @@ async function main() {
|
||||
`✓ Generated ${plugins.length} plugins (${pluginsData.filters.tags.length} tags)`
|
||||
);
|
||||
|
||||
const extensionsData = generateExtensionsData(gitDates, commitSha);
|
||||
const canvasManifestData = generateCanvasManifest(gitDates, commitSha);
|
||||
const extensionsData = generateExtensionsData(canvasManifestData);
|
||||
const extensions = extensionsData.items;
|
||||
console.log(`✓ Generated ${extensions.length} extensions`);
|
||||
console.log(
|
||||
`✓ Generated ${extensions.length} extensions (${extensionsData.filters.keywords.length} keywords)`
|
||||
);
|
||||
|
||||
const toolsData = generateToolsData();
|
||||
const tools = toolsData.items;
|
||||
@@ -1248,6 +1629,8 @@ async function main() {
|
||||
JSON.stringify(extensionsData, null, 2)
|
||||
);
|
||||
|
||||
writePerExtensionCanvasManifests(canvasManifestData);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "tools.json"),
|
||||
JSON.stringify(toolsData, null, 2)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"id": "accessibility-kanban",
|
||||
"name": "Accessibility Kanban",
|
||||
"description": "Kanban board to manage accessibility issues, allow you to plan, track, and complete remediation work.",
|
||||
"version": "1.0.0",
|
||||
"keywords": [
|
||||
"accessibility",
|
||||
"github-issues",
|
||||
"issue-triage",
|
||||
"kanban-board",
|
||||
"planning-workflow",
|
||||
"status-tracking"
|
||||
],
|
||||
"screenshots": {
|
||||
"icon": {
|
||||
"path": "assets/preview.png",
|
||||
"type": "image/png"
|
||||
},
|
||||
"gallery": {
|
||||
"path": "assets/preview.png",
|
||||
"type": "image/png"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,14 @@
|
||||
"main": "extension.mjs",
|
||||
"dependencies": {
|
||||
"@github/copilot-sdk": "latest"
|
||||
}
|
||||
},
|
||||
"description": "Users drag accessibility issues across kanban lanes to plan, track, and complete remediation work.",
|
||||
"keywords": [
|
||||
"accessibility",
|
||||
"kanban-board",
|
||||
"issue-triage",
|
||||
"planning-workflow",
|
||||
"status-tracking",
|
||||
"github-issues"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"id": "backlog-swipe-triage",
|
||||
"name": "Backlog Swipe Triage",
|
||||
"description": "Quickly swipe through backlog issues to triage decisions like assign, needs-info, defer, close, or ignore.",
|
||||
"version": "1.0.0",
|
||||
"keywords": [
|
||||
"agent-assignment",
|
||||
"backlog-triage",
|
||||
"github-issues",
|
||||
"issue-prioritization",
|
||||
"swipe-interface",
|
||||
"workflow-automation"
|
||||
],
|
||||
"screenshots": {
|
||||
"icon": {
|
||||
"path": "assets/preview.png",
|
||||
"type": "image/png"
|
||||
},
|
||||
"gallery": {
|
||||
"path": "assets/preview.png",
|
||||
"type": "image/png"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,16 @@
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "extension.mjs",
|
||||
"description": "Swipe-driven backlog triage canvas for reviewing open issues and assigning implementation work.",
|
||||
"dependencies": {
|
||||
"@github/copilot-sdk": "1.0.1"
|
||||
}
|
||||
},
|
||||
"description": "Users quickly swipe through backlog issues to triage decisions like assign, needs-info, defer, close, or ignore.",
|
||||
"keywords": [
|
||||
"backlog-triage",
|
||||
"swipe-interface",
|
||||
"issue-prioritization",
|
||||
"github-issues",
|
||||
"agent-assignment",
|
||||
"workflow-automation"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"id": "chromium-control-canvas",
|
||||
"name": "Chromium Control Canvas",
|
||||
"description": "Opens a real Chromium window you can navigate and interact with from a Copilot canvas control panel and agent actions.",
|
||||
"version": "1.0.0",
|
||||
"keywords": [
|
||||
"browser-control",
|
||||
"chromium-browser",
|
||||
"interactive-canvas",
|
||||
"playwright-automation",
|
||||
"screenshots",
|
||||
"ui-testing",
|
||||
"web-navigation"
|
||||
],
|
||||
"screenshots": {
|
||||
"icon": {
|
||||
"path": "assets/preview.png",
|
||||
"type": "image/png"
|
||||
},
|
||||
"gallery": {
|
||||
"path": "assets/preview.png",
|
||||
"type": "image/png"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -561,7 +561,7 @@ const session = await joinSession({
|
||||
id: "chromium-control-canvas",
|
||||
displayName: "Chromium Control Canvas",
|
||||
description:
|
||||
"Control canvas for a real headful Chromium window driven by Playwright.",
|
||||
"Opens a real Chromium window you can navigate and interact with from a Copilot canvas control panel and agent actions.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
{
|
||||
"name": "chromium-control-canvas",
|
||||
"version": "1.0.0",
|
||||
"description": "GitHub Copilot canvas that drives a real headful Chromium window via Playwright.",
|
||||
"main": "extension.mjs",
|
||||
"keywords": [],
|
||||
"author": "Andrea Griffiths",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@github/copilot-sdk": "latest",
|
||||
"playwright": "^1.60.0"
|
||||
}
|
||||
},
|
||||
"description": "Opens a real Chromium window you can navigate and interact with from a Copilot canvas control panel and agent actions.",
|
||||
"keywords": [
|
||||
"chromium-browser",
|
||||
"playwright-automation",
|
||||
"browser-control",
|
||||
"interactive-canvas",
|
||||
"web-navigation",
|
||||
"screenshots",
|
||||
"ui-testing"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"id": "color-orb",
|
||||
"name": "Color Orb",
|
||||
"description": "A visual orb that users can ask the agent to recolor while showing a live activity log in the canvas.",
|
||||
"version": "1.0.0",
|
||||
"keywords": [
|
||||
"agent-actions",
|
||||
"color-picker",
|
||||
"interactive-demo",
|
||||
"realtime-updates",
|
||||
"sse-events",
|
||||
"visual-feedback"
|
||||
],
|
||||
"screenshots": {
|
||||
"icon": {
|
||||
"path": "assets/preview.png",
|
||||
"type": "image/png"
|
||||
},
|
||||
"gallery": {
|
||||
"path": "assets/preview.png",
|
||||
"type": "image/png"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,14 @@
|
||||
"main": "extension.mjs",
|
||||
"dependencies": {
|
||||
"@github/copilot-sdk": "latest"
|
||||
}
|
||||
},
|
||||
"description": "Gives users a visual orb they can ask the agent to recolor while showing a live activity log in the canvas.",
|
||||
"keywords": [
|
||||
"color-picker",
|
||||
"interactive-demo",
|
||||
"agent-actions",
|
||||
"realtime-updates",
|
||||
"sse-events",
|
||||
"visual-feedback"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"id": "diagram",
|
||||
"name": "Diagram Explorer",
|
||||
"description": "Render diagrams, click nodes to drill down, and view agent-generated explanations directly in the canvas.",
|
||||
"version": "1.0.0",
|
||||
"keywords": [
|
||||
"architecture-mapping",
|
||||
"canvas-navigation",
|
||||
"exploratory-analysis",
|
||||
"interactive-diagrams",
|
||||
"node-drilldown",
|
||||
"relationship-visualization"
|
||||
],
|
||||
"screenshots": {
|
||||
"icon": {
|
||||
"path": "assets/preview.png",
|
||||
"type": "image/png"
|
||||
},
|
||||
"gallery": {
|
||||
"path": "assets/preview.png",
|
||||
"type": "image/png"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,14 @@
|
||||
"main": "extension.mjs",
|
||||
"dependencies": {
|
||||
"@github/copilot-sdk": "latest"
|
||||
}
|
||||
},
|
||||
"description": "Lets users render diagrams, click nodes to drill down, and view agent-generated explanations directly in the canvas.",
|
||||
"keywords": [
|
||||
"interactive-diagrams",
|
||||
"architecture-mapping",
|
||||
"node-drilldown",
|
||||
"relationship-visualization",
|
||||
"exploratory-analysis",
|
||||
"canvas-navigation"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,21 @@
|
||||
"id": "coffilot",
|
||||
"name": "Coffilot",
|
||||
"description": "Java-focused Copilot canvas extension from jdubois.",
|
||||
"keywords": [
|
||||
"java",
|
||||
"canvas",
|
||||
"productivity"
|
||||
],
|
||||
"screenshots": {
|
||||
"icon": {
|
||||
"path": "extensions/external-assets/coffilot-preview.png",
|
||||
"type": "image/png"
|
||||
},
|
||||
"gallery": {
|
||||
"path": "extensions/external-assets/coffilot-preview.png",
|
||||
"type": "image/png"
|
||||
}
|
||||
},
|
||||
"installUrl": "https://github.com/jdubois/coffilot",
|
||||
"sourceUrl": "https://github.com/jdubois/coffilot",
|
||||
"imagePath": "extensions/external-assets/coffilot-preview.png"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"id": "feedback-themes",
|
||||
"name": "Feedback Themes",
|
||||
"description": "Explore grouped customer feedback signals by impact and drill into a theme to guide product next steps.",
|
||||
"version": "1.0.0",
|
||||
"keywords": [
|
||||
"customer-feedback",
|
||||
"impact-prioritization",
|
||||
"product-insights",
|
||||
"signal-grouping",
|
||||
"theme-analysis",
|
||||
"trend-discovery"
|
||||
],
|
||||
"screenshots": {
|
||||
"icon": {
|
||||
"path": "assets/preview.png",
|
||||
"type": "image/png"
|
||||
},
|
||||
"gallery": {
|
||||
"path": "assets/preview.png",
|
||||
"type": "image/png"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,14 @@
|
||||
"main": "extension.mjs",
|
||||
"dependencies": {
|
||||
"@github/copilot-sdk": "latest"
|
||||
}
|
||||
},
|
||||
"description": "Explore grouped customer feedback signals by impact and drill into a theme to guide product next steps.",
|
||||
"keywords": [
|
||||
"customer-feedback",
|
||||
"theme-analysis",
|
||||
"signal-grouping",
|
||||
"impact-prioritization",
|
||||
"product-insights",
|
||||
"trend-discovery"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"id": "gesture-review",
|
||||
"name": "Gesture PR Review",
|
||||
"description": "Review pull requests with a live camera feed and approve or reject using thumbs-up/thumbs-down gestures.",
|
||||
"version": "1.0.0",
|
||||
"keywords": [
|
||||
"camera-input",
|
||||
"gesture-control",
|
||||
"github-prs",
|
||||
"hands-free",
|
||||
"mediapipe",
|
||||
"pull-request-review"
|
||||
],
|
||||
"screenshots": {
|
||||
"icon": {
|
||||
"path": "assets/preview.png",
|
||||
"type": "image/png"
|
||||
},
|
||||
"gallery": {
|
||||
"path": "assets/preview.png",
|
||||
"type": "image/png"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,7 +179,7 @@ const canvas = createCanvas({
|
||||
id: "gesture-review",
|
||||
displayName: "Gesture PR Review",
|
||||
description:
|
||||
"Interactive PR review using hand gestures. Shows a live camera feed and detects thumbs up (approve) or thumbs down (reject) via MediaPipe hand tracking.",
|
||||
"Users review pull requests with a live camera feed and approve or reject using thumbs-up/thumbs-down gestures.",
|
||||
actions: [
|
||||
{
|
||||
name: "show_pr",
|
||||
|
||||
@@ -5,5 +5,14 @@
|
||||
"main": "extension.mjs",
|
||||
"dependencies": {
|
||||
"@github/copilot-sdk": "latest"
|
||||
}
|
||||
},
|
||||
"description": "Users review pull requests with a live camera feed and approve or reject using thumbs-up/thumbs-down gestures.",
|
||||
"keywords": [
|
||||
"pull-request-review",
|
||||
"gesture-control",
|
||||
"camera-input",
|
||||
"hands-free",
|
||||
"github-prs",
|
||||
"mediapipe"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"id": "release-notes-showcase",
|
||||
"name": "Release Notes Showcase",
|
||||
"description": "Compose and refine launch-ready release notes with contributor callouts and export-friendly output.",
|
||||
"version": "1.0.0",
|
||||
"keywords": [
|
||||
"changelog",
|
||||
"contributor-callouts",
|
||||
"email-export",
|
||||
"launch-summary",
|
||||
"product-updates",
|
||||
"release-notes"
|
||||
],
|
||||
"screenshots": {
|
||||
"icon": {
|
||||
"path": "assets/preview.png",
|
||||
"type": "image/png"
|
||||
},
|
||||
"gallery": {
|
||||
"path": "assets/preview.png",
|
||||
"type": "image/png"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,16 @@
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "extension.mjs",
|
||||
"description": "Release notes canvas for building polished release communications and exports.",
|
||||
"dependencies": {
|
||||
"@github/copilot-sdk": "1.0.1"
|
||||
}
|
||||
},
|
||||
"description": "Compose and refine launch-ready release notes with contributor callouts and export-friendly output.",
|
||||
"keywords": [
|
||||
"release-notes",
|
||||
"launch-summary",
|
||||
"changelog",
|
||||
"contributor-callouts",
|
||||
"product-updates",
|
||||
"email-export"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -621,7 +621,7 @@ export const releaseNotesShowcaseCanvas = createCanvas({
|
||||
id: CANVAS_ID,
|
||||
displayName: CANVAS_TITLE,
|
||||
description:
|
||||
"Presents release notes as a high-impact launch summary with contributor callouts and email-ready export output.",
|
||||
"Compose and refine launch-ready release notes with contributor callouts and export-friendly output.",
|
||||
inputSchema: releaseNotesInputSchema,
|
||||
actions: [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"id": "where-was-i",
|
||||
"name": "Where Was I?",
|
||||
"description": "Reconstruct your dev context (branch, commits, uncommitted work, PR clues) and trigger a resume prompt to continue quickly.",
|
||||
"version": "1.0.0",
|
||||
"keywords": [
|
||||
"branch-state",
|
||||
"developer-context",
|
||||
"git-history",
|
||||
"interrupt-recovery",
|
||||
"pull-request-context",
|
||||
"resume-work"
|
||||
],
|
||||
"screenshots": {
|
||||
"icon": {
|
||||
"path": "assets/preview.png",
|
||||
"type": "image/png"
|
||||
},
|
||||
"gallery": {
|
||||
"path": "assets/preview.png",
|
||||
"type": "image/png"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -666,7 +666,7 @@ const session = await joinSession({
|
||||
createCanvas({
|
||||
id: "where-was-i",
|
||||
displayName: "Where Was I?",
|
||||
description: "Interrupt Recovery — reconstructs your working context (branch, commits, changes, PRs) so you can resume after being pulled away.",
|
||||
description: "Reconstruct your dev context (branch, commits, uncommitted work, PR clues) and trigger a resume prompt to continue quickly.",
|
||||
actions: [
|
||||
{
|
||||
name: "refresh",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "where-was-i",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "extension.mjs",
|
||||
"description": "Reconstruct your dev context (branch, commits, uncommitted work, PR clues) and trigger a resume prompt to continue quickly.",
|
||||
"keywords": [
|
||||
"interrupt-recovery",
|
||||
"developer-context",
|
||||
"git-history",
|
||||
"branch-state",
|
||||
"resume-work",
|
||||
"pull-request-context"
|
||||
],
|
||||
"dependencies": {
|
||||
"@github/copilot-sdk": "latest"
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,13 @@ const initialItems = sortExtensions(extensionsData.items, 'title');
|
||||
<div class="listing-toolbar-row">
|
||||
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} extensions</div>
|
||||
<details class="listing-controls">
|
||||
<summary class="listing-controls-trigger btn btn-secondary btn-small">Sort</summary>
|
||||
<summary class="listing-controls-trigger btn btn-secondary btn-small">Sort & Filter</summary>
|
||||
<div class="listing-controls-panel">
|
||||
<div class="filters-bar" id="filters-bar">
|
||||
<div class="filter-group">
|
||||
<label for="filter-keyword">Keyword:</label>
|
||||
<select id="filter-keyword" multiple aria-label="Filter by keyword"></select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="sort-select">Sort:</label>
|
||||
<select id="sort-select" aria-label="Sort extensions">
|
||||
@@ -30,6 +34,7 @@ const initialItems = sortExtensions(extensionsData.items, 'title');
|
||||
<option value="lastUpdated">Recently Updated</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -2,11 +2,26 @@ import { escapeHtml, getGitHubUrl, getLastUpdatedHtml } from "../utils";
|
||||
|
||||
export interface RenderableExtension {
|
||||
id: string;
|
||||
canvasId?: string;
|
||||
extensionId?: string;
|
||||
extensionName?: string;
|
||||
name: string;
|
||||
path?: string | null;
|
||||
ref?: string | null;
|
||||
version?: string | null;
|
||||
description?: string;
|
||||
lastUpdated?: string | null;
|
||||
keywords?: string[];
|
||||
screenshots?: {
|
||||
icon?: {
|
||||
path?: string | null;
|
||||
type?: string | null;
|
||||
} | null;
|
||||
gallery?: {
|
||||
path?: string | null;
|
||||
type?: string | null;
|
||||
} | null;
|
||||
} | null;
|
||||
imageUrl?: string | null;
|
||||
assetPath?: string | null;
|
||||
installUrl?: string | null;
|
||||
@@ -69,6 +84,18 @@ export function renderExtensionsHtml(items: RenderableExtension[]): string {
|
||||
<div class="resource-description">${escapeHtml(
|
||||
item.description || "Canvas extension"
|
||||
)}</div>
|
||||
<div class="resource-keywords">
|
||||
${
|
||||
item.keywords && item.keywords.length > 0
|
||||
? item.keywords
|
||||
.map(
|
||||
(kw) =>
|
||||
`<span class="keyword-tag">${escapeHtml(kw)}</span>`
|
||||
)
|
||||
.join("")
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
<div class="resource-meta">
|
||||
${
|
||||
item.external
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
/**
|
||||
* Canvas extensions page functionality
|
||||
*/
|
||||
import {
|
||||
createChoices,
|
||||
getChoicesValues,
|
||||
setChoicesValues,
|
||||
type Choices,
|
||||
} from "../choices";
|
||||
import {
|
||||
copyToClipboard,
|
||||
fetchData,
|
||||
getQueryParam,
|
||||
getQueryParamValues,
|
||||
showToast,
|
||||
updateQueryParams,
|
||||
} from "../utils";
|
||||
@@ -17,14 +24,22 @@ import {
|
||||
|
||||
interface Extension extends RenderableExtension {
|
||||
lastUpdated?: string | null;
|
||||
keywords?: string[];
|
||||
}
|
||||
|
||||
interface ExtensionsData {
|
||||
items: Extension[];
|
||||
filters?: {
|
||||
keywords?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
let allItems: Extension[] = [];
|
||||
let currentSort: ExtensionSortOption = "title";
|
||||
let keywordSelect: Choices;
|
||||
let currentFilters = {
|
||||
keywords: [] as string[],
|
||||
};
|
||||
let actionHandlersReady = false;
|
||||
|
||||
function openPreviewModal(url: string, alt: string): void {
|
||||
@@ -51,13 +66,33 @@ function closePreviewModal(): void {
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
|
||||
function sortItems(items: Extension[]): Extension[] {
|
||||
return sortExtensions(items, currentSort);
|
||||
}
|
||||
|
||||
function getCountText(resultsCount: number): string {
|
||||
if (currentFilters.keywords.length === 0) {
|
||||
return `${resultsCount} extension${resultsCount === 1 ? "" : "s"}`;
|
||||
}
|
||||
|
||||
return `${resultsCount} of ${allItems.length} extensions (filtered by ${currentFilters.keywords.length} keyword${currentFilters.keywords.length === 1 ? "" : "s"})`;
|
||||
}
|
||||
|
||||
function applySortAndRender(): void {
|
||||
const countEl = document.getElementById("results-count");
|
||||
const results = sortExtensions(allItems, currentSort);
|
||||
let results = [...allItems];
|
||||
|
||||
if (currentFilters.keywords.length > 0) {
|
||||
results = results.filter((item) =>
|
||||
item.keywords?.some((keyword) => currentFilters.keywords.includes(keyword))
|
||||
);
|
||||
}
|
||||
|
||||
results = sortItems(results);
|
||||
|
||||
renderItems(results);
|
||||
if (countEl) {
|
||||
countEl.textContent = `${results.length} extension${results.length === 1 ? "" : "s"}`;
|
||||
countEl.textContent = getCountText(results.length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,12 +167,15 @@ function setupActionHandlers(list: HTMLElement | null): void {
|
||||
|
||||
function syncUrlState(): void {
|
||||
updateQueryParams({
|
||||
q: "",
|
||||
keyword: currentFilters.keywords,
|
||||
sort: currentSort === "title" ? "" : currentSort,
|
||||
});
|
||||
}
|
||||
|
||||
export async function initExtensionsPage(): Promise<void> {
|
||||
const list = document.getElementById("resource-list");
|
||||
const clearFiltersBtn = document.getElementById("clear-filters");
|
||||
const sortSelect = document.getElementById(
|
||||
"sort-select"
|
||||
) as HTMLSelectElement;
|
||||
@@ -154,19 +192,63 @@ export async function initExtensionsPage(): Promise<void> {
|
||||
|
||||
allItems = data.items;
|
||||
|
||||
const availableKeywords = (
|
||||
data.filters?.keywords ||
|
||||
Array.from(
|
||||
new Set(
|
||||
data.items.flatMap((item) =>
|
||||
Array.isArray(item.keywords) ? item.keywords : []
|
||||
)
|
||||
)
|
||||
)
|
||||
).sort((a, b) => a.localeCompare(b));
|
||||
|
||||
keywordSelect = createChoices("#filter-keyword", {
|
||||
placeholderValue: "All Keywords",
|
||||
});
|
||||
keywordSelect.setChoices(
|
||||
availableKeywords.map((keyword) => ({ value: keyword, label: keyword })),
|
||||
"value",
|
||||
"label",
|
||||
true
|
||||
);
|
||||
|
||||
const initialKeywords = getQueryParamValues("keyword").filter((keyword) =>
|
||||
availableKeywords.includes(keyword)
|
||||
);
|
||||
const initialSort = getQueryParam("sort");
|
||||
if (initialKeywords.length > 0) {
|
||||
currentFilters.keywords = initialKeywords;
|
||||
setChoicesValues(keywordSelect, initialKeywords);
|
||||
}
|
||||
if (initialSort === "lastUpdated") {
|
||||
currentSort = initialSort;
|
||||
if (sortSelect) sortSelect.value = initialSort;
|
||||
}
|
||||
|
||||
document.getElementById("filter-keyword")?.addEventListener("change", () => {
|
||||
currentFilters.keywords = getChoicesValues(keywordSelect);
|
||||
applySortAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
|
||||
sortSelect?.addEventListener("change", () => {
|
||||
currentSort = sortSelect.value as ExtensionSortOption;
|
||||
applySortAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
|
||||
clearFiltersBtn?.addEventListener("click", () => {
|
||||
currentFilters = { keywords: [] };
|
||||
currentSort = "title";
|
||||
keywordSelect.removeActiveItems();
|
||||
if (sortSelect) sortSelect.value = "title";
|
||||
applySortAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
|
||||
applySortAndRender();
|
||||
syncUrlState();
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
|
||||
@@ -1971,6 +1971,23 @@ body:has(#main-content) {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.resource-keywords {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.keyword-tag {
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
padding: 3px 8px;
|
||||
background-color: var(--color-bg-tertiary);
|
||||
border-radius: 10px;
|
||||
color: var(--color-text-muted);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.resource-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
Reference in New Issue
Block a user