Files
awesome-copilot/website/src/scripts/pages/extensions-render.ts
T
Aaron Powell 17b174fb0a Canvas manifest implementation for extensions (#2029)
* Add keyword display to extension cards on website

- Add .resource-keywords and .keyword-tag CSS styles for rendering keyword badges
- Update renderExtensionsHtml() to display keywords below extension description
- Keywords now visible on the website extensions page with styled badges
- Regenerate website data to include keyword metadata

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

* Canvas manifest implementation for all extensions

Add per-extension canvas manifests with:
- Structured canvas metadata (name, description, version, keywords)
- Screenshot definitions (icon and gallery with path/type)
- Relative paths for images within each extension directory

Enhance extension metadata:
- Generate meaningful descriptions from source analysis
- Extract and assign keywords for discoverability
- Store metadata in package.json and extension source files

Update website rendering and data generation:
- Include keywords in extension cards and search index
- Add per-extension canvas.json files for independent evolution
- Support screenshot metadata in manifest structure
- Generate extensions.json with full canonical paths for website

All 9 local canvas extensions now have complete manifests with descriptions, keywords, and screenshot references.

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

* Tweaking some descriptions

* Fix description priority to prefer package.json over in-source metadata

Reverse the priority in canvasDescription so that package.json descriptions
(which contain the enhanced, manually-curated descriptions) take precedence
over older in-source descriptions extracted from createCanvas(...) calls.

This prevents regression when npm run website:data regenerates outputs,
ensuring that committed canvas.json files maintain the current descriptions.

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

* Fix canvas validation to skip external.json file

The validation script was treating extensions/external.json as if it were
a directory, causing false validation failures. Added check to skip files
(identified by presence of dot in filename) and only validate actual
canvas extension directories.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-17 15:28:15 +10:00

131 lines
4.1 KiB
TypeScript

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;
sourceUrl?: string | null;
external?: boolean;
}
export type ExtensionSortOption = "title" | "lastUpdated";
export function sortExtensions<T extends RenderableExtension>(
items: T[],
sort: ExtensionSortOption
): T[] {
return [...items].sort((a, b) => {
if (sort === "lastUpdated") {
const dateA = a.lastUpdated ? new Date(a.lastUpdated).getTime() : 0;
const dateB = b.lastUpdated ? new Date(b.lastUpdated).getTime() : 0;
return dateB - dateA;
}
return a.name.localeCompare(b.name);
});
}
export function renderExtensionsHtml(items: RenderableExtension[]): string {
if (items.length === 0) {
return `
<div class="empty-state">
<h3>No extensions found</h3>
<p>No canvas extensions are available right now.</p>
</div>
`;
}
return items
.map((item) => {
const installUrl =
item.installUrl ||
(item.path && item.ref
? `https://github.com/github/awesome-copilot/tree/${item.ref}/${item.path.replace(
/\\/g,
"/"
)}`
: "");
const sourceUrl =
item.sourceUrl || (item.path ? getGitHubUrl(item.path) : "");
return `
<article class="resource-item" role="listitem">
<div class="resource-preview">
${
item.imageUrl
? `<button type="button" class="resource-thumbnail-btn" data-preview-url="${escapeHtml(item.imageUrl)}" data-preview-alt="${escapeHtml(item.name)} preview" aria-label="Open ${escapeHtml(item.name)} preview">
<img class="resource-thumbnail" src="${escapeHtml(item.imageUrl)}" alt="${escapeHtml(item.name)} preview" loading="lazy" />
</button>`
: `<div class="resource-thumbnail resource-thumbnail-placeholder" aria-hidden="true">Canvas</div>`
}
<div class="resource-info">
<div class="resource-title">${escapeHtml(item.name)}</div>
<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
? '<span class="resource-tag">External</span>'
: ""
}
${getLastUpdatedHtml(item.lastUpdated)}
</div>
</div>
</div>
<div class="resource-actions">
<button
class="btn btn-primary btn-small copy-install-url-btn"
data-install-url="${escapeHtml(installUrl)}"
title="Copy install URL"
${installUrl ? "" : "disabled"}
>
Install
</button>
${
sourceUrl
? `<a href="${escapeHtml(
sourceUrl
)}" class="btn btn-secondary btn-small" target="_blank" rel="noopener noreferrer" title="View source">Source</a>`
: ""
}
</div>
</article>
`;
})
.join("");
}