-
-
${escapeHtml(previewIcon)}
- ${
- previewText
- ? `
${escapeHtml(previewText)}
`
- : ""
- }
-
-
-
${escapeHtml(description)}
- ${
- metaHtml
- ? `
${metaHtml}
`
- : ""
- }
- ${
- tagsHtml
- ? `
${tagsHtml}
`
- : ""
- }
- ${
- actionsHtml
- ? `
${actionsHtml}
`
- : ""
- }
-
-
- `;
-
- modalBody.scrollTop = 0;
- modal.classList.remove("hidden");
- modal.classList.add("visible");
-
- setTimeout(() => {
- closeBtn?.focus();
- }, 0);
-}
-
-/**
- * Open plugin modal with item list
- */
-async function openPluginModal(
- filePath: string,
- title: HTMLElement,
- modalContent: HTMLElement,
- installDropdown: HTMLElement | null,
- copyBtn: HTMLElement | null,
- downloadBtn: HTMLElement | null
-): Promise {
- // Hide install dropdown and copy/download for plugins
- if (installDropdown) installDropdown.style.display = "none";
- if (copyBtn) copyBtn.style.display = "none";
- if (downloadBtn) downloadBtn.style.display = "none";
-
- // Replace with a so plugin content isn't styled as preformatted text
- const modalBody = modalContent.parentElement;
- if (modalBody) {
- const div = document.createElement("div");
- div.id = "modal-content";
- div.innerHTML = '
Loading plugin...
';
- modalBody.replaceChild(div, modalContent);
- modalContent = div;
- } else {
- modalContent.innerHTML =
- '
Loading plugin...
';
- }
-
- // Load plugins data if not cached
- if (!pluginsCache) {
- pluginsCache = await fetchData
("plugins.json");
- }
-
- if (!pluginsCache) {
- modalContent.innerHTML =
- 'Failed to load plugin data.
';
- return;
- }
-
- // Find the plugin
- const plugin = pluginsCache.items.find((c) => c.path === filePath);
- if (!plugin) {
- modalContent.innerHTML =
- 'Plugin not found.
';
- return;
- }
-
- // Update title
- title.textContent = plugin.name;
- document.title = `${plugin.name} | Awesome GitHub Copilot`;
-
- // Render external plugin view (metadata + links) or local plugin view (items list)
- if (plugin.external) {
- renderExternalPluginModal(plugin, modalContent);
- } else {
- renderLocalPluginModal(plugin, modalContent);
- }
-}
-
-/**
- * Get the best URL for an external plugin, preferring the deep path within the repo
- */
-function getExternalPluginUrl(plugin: Plugin): string {
- // Sanitize URLs from JSON to prevent XSS via javascript:/data: schemes and
- // pin GitHub links to the source's ref/sha when available.
- return externalRepoUrl(plugin.source, [plugin.repository, plugin.homepage]);
-}
-
-/**
- * Render modal content for an external plugin (no local files)
- */
-function renderExternalPluginModal(
- plugin: Plugin,
- modalContent: HTMLElement
-): void {
- const authorHtml = plugin.author?.name
- ? ``
- : "";
-
- const repoHtml = plugin.repository
- ? ``
- : "";
-
- const homepageHtml =
- plugin.homepage && plugin.homepage !== plugin.repository
- ? ``
- : "";
-
- const licenseHtml = plugin.license
- ? `
- License
- ${escapeHtml(
- plugin.license
- )}
-
`
- : "";
-
- const sourceHtml = plugin.source?.repo
- ? `
- Source
- GitHub: ${escapeHtml(
- plugin.source.repo
- )}${
- plugin.source.path ? ` (${escapeHtml(plugin.source.path)})` : ""
- }
-
`
- : "";
-
- const repoUrl = getExternalPluginUrl(plugin);
-
- modalContent.innerHTML = `
-
-
${escapeHtml(
- plugin.description || ""
- )}
- ${
- plugin.tags && plugin.tags.length > 0
- ? `
- 🔗 External Plugin
- ${plugin.tags
- .map(
- (t) => `${escapeHtml(t)} `
- )
- .join("")}
-
`
- : `
- 🔗 External Plugin
-
`
- }
-
- ${authorHtml}
- ${repoHtml}
- ${homepageHtml}
- ${licenseHtml}
- ${sourceHtml}
-
-
-
- This is an external plugin maintained outside this repository. Browse the repository to see its contents and installation instructions.
-
-
- `;
-}
-
-/**
- * Render modal content for a local plugin (item list)
- */
-function renderLocalPluginModal(
- plugin: Plugin,
- modalContent: HTMLElement
-): void {
- modalContent.innerHTML = `
-
-
${escapeHtml(
- plugin.description || ""
- )}
- ${
- plugin.tags && plugin.tags.length > 0
- ? `
-
- ${plugin.tags
- .map((t) => `${escapeHtml(t)} `)
- .join("")}
-
- `
- : ""
- }
-
-
- ${plugin.items
- .map(
- (item) => `
-
-
${getResourceIconSvg(
- item.kind
- )}
-
-
${escapeHtml(
- item.path.split("/").pop() || item.path
- )}
- ${
- item.usage
- ? `
${escapeHtml(
- item.usage
- )}
`
- : ""
- }
-
-
${escapeHtml(item.kind)}
-
- `
- )
- .join("")}
-
-
- `;
-
- // Add click handlers to plugin items
- modalContent.querySelectorAll(".collection-item").forEach((el) => {
- el.addEventListener("click", () => {
- let path = (el as HTMLElement).dataset.path;
- const itemType = (el as HTMLElement).dataset.type;
-
- switch (itemType) {
- case "agent":
- // path = path.replace(".md", ".agent.md");
- break;
- case "skill":
- path = `${path}/SKILL.md`;
- break;
- }
-
- if (path && itemType) {
- openFileModal(path, itemType);
- }
- });
- });
-}
-
-/**
- * Close modal
- * @param updateUrl - Whether to update the URL hash (default: true)
- */
-export function closeModal(updateUrl = true): void {
- const modal = document.getElementById("file-modal");
- const installDropdown = document.getElementById("install-dropdown");
-
- if (modal) {
- modal.classList.add("hidden");
- modal.classList.remove("visible");
- }
- if (installDropdown) {
- installDropdown.classList.remove("open");
- }
-
- // Update URL for deep linking
- if (updateUrl) {
- updateHash(null);
- }
-
- // Restore original document title
- if (originalDocumentTitle) {
- document.title = originalDocumentTitle;
- originalDocumentTitle = null;
- }
-
- // Return focus to trigger element
- if (
- triggerElement &&
- triggerElement.isConnected &&
- typeof triggerElement.focus === "function"
- ) {
- triggerElement.focus();
- }
-
- currentFilePath = null;
- currentFileContent = null;
- currentFileType = null;
- currentViewMode = "raw";
- triggerElement = null;
- hideSkillFileSwitcher();
-}
-
-/**
- * Get current file path (for external use)
- */
-export function getCurrentFilePath(): string | null {
- return currentFilePath;
-}
-
-/**
- * Get current file content (for external use)
- */
-export function getCurrentFileContent(): string | null {
- return currentFileContent;
-}
diff --git a/website/src/scripts/pages/extensions-render.ts b/website/src/scripts/pages/extensions-render.ts
index 19a316b8..0c7a5cfc 100644
--- a/website/src/scripts/pages/extensions-render.ts
+++ b/website/src/scripts/pages/extensions-render.ts
@@ -48,6 +48,7 @@ export interface RenderableExtension {
installCommand?: string | null;
installUrl?: string | null;
sourceUrl?: string | null;
+ externalSource?: string | null;
external?: boolean;
author?: { name: string; url?: string } | null;
}
@@ -95,13 +96,20 @@ export function renderExtensionsHtml(items: RenderableExtension[]): string {
const sourceUrl = safeUrl(
item.sourceUrl || (item.path ? getGitHubUrl(item.path) : "")
);
+ const externalSource = (item.externalSource || "").trim();
const pluginId = item.pluginName || item.id;
const ghappInstallUrl =
- !item.external && pluginId
- ? `ghapp://plugins/install?source=${encodeURIComponent(
- `${pluginId}@awesome-copilot`
- )}`
- : "";
+ item.external
+ ? externalSource
+ ? `ghapp://plugins/marketplace/add?source=${encodeURIComponent(
+ externalSource
+ )}`
+ : ""
+ : pluginId
+ ? `ghapp://plugins/install?source=${encodeURIComponent(
+ `${pluginId}@awesome-copilot`
+ )}`
+ : "";
const previewImageUrl = safeUrl(item.imageUrl);
const previewMediaHtml = previewImageUrl
@@ -152,14 +160,18 @@ export function renderExtensionsHtml(items: RenderableExtension[]): string {
`
: ""
}
-
Copy URL
-
+ `
+ : ""
+ }
${
sourceUrl
? `(
- items: T[],
- sort: HookSortOption
-): 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.title.localeCompare(b.title);
- });
-}
-
-export function renderHooksHtml(items: RenderableHook[]): string {
- if (items.length === 0) {
- return renderEmptyStateHtml("No hooks found", "Try adjusting the selected filters.");
- }
-
- return items
- .map((item) => {
- const metaHtml = `
- ${item.hooks
- .map(
- (hook) => `${escapeHtml(hook)} `
- )
- .join("")}
- ${item.tags
- .map((tag) => `${escapeHtml(tag)} `)
- .join("")}
- ${
- item.assets.length > 0
- ? `${item.assets.length} asset${
- item.assets.length === 1 ? "" : "s"
- } `
- : ""
- }
- ${getLastUpdatedHtml(item.lastUpdated)}
- `;
-
- const actionsHtml = `
-
-
-
-
-
- Download
-
- GitHub
- `;
-
- return renderSharedCardHtml({
- title: item.title,
- description: item.description || "No description",
- href: getHookDetailUrl(item.id),
- articleAttributes: {
- "data-path": item.readmeFile,
- "data-hook-id": item.id,
- },
- metaHtml,
- actionsHtml,
- });
- })
- .join("");
-}
diff --git a/website/src/scripts/pages/hooks.ts b/website/src/scripts/pages/hooks.ts
deleted file mode 100644
index 3a15d473..00000000
--- a/website/src/scripts/pages/hooks.ts
+++ /dev/null
@@ -1,199 +0,0 @@
-/**
- * Hooks page functionality
- */
-import {
- fetchData,
- getQueryParam,
- getQueryParamValues,
- showToast,
- downloadZipBundle,
- updateQueryParams,
-} from '../utils';
-import { clearSelectValues, getSelectValues, setSelectValues } from './select-utils';
-import {
- renderHooksHtml,
- sortHooks,
- type HookSortOption,
- type RenderableHook,
-} from './hooks-render';
-
-interface Hook extends RenderableHook {}
-
-interface HooksData {
- items: Hook[];
- filters: {
- tags: string[];
- };
-}
-
-let allItems: Hook[] = [];
-let tagSelectEl: HTMLSelectElement | null = null;
-let currentFilters = {
- tags: [] as string[],
-};
-let currentSort: HookSortOption = 'title';
-let resourceListHandlersReady = false;
-
-function sortItems(items: Hook[]): Hook[] {
- return sortHooks(items, currentSort);
-}
-
-function applyFiltersAndRender(): void {
- const countEl = document.getElementById('results-count');
- let results = [...allItems];
-
- if (currentFilters.tags.length > 0) {
- results = results.filter((item) => item.tags.some((tag) => currentFilters.tags.includes(tag)));
- }
-
- results = sortItems(results);
-
- renderItems(results);
- let countText = `${results.length} hook${results.length === 1 ? '' : 's'}`;
- if (currentFilters.tags.length > 0) {
- countText = `${results.length} of ${allItems.length} hooks (filtered by ${currentFilters.tags.length} tag${currentFilters.tags.length > 1 ? 's' : ''})`;
- }
- if (countEl) countEl.textContent = countText;
-}
-
-function renderItems(items: Hook[]): void {
- const list = document.getElementById('resource-list');
- if (!list) return;
-
- list.innerHTML = renderHooksHtml(items);
-}
-
-async function downloadHook(hookId: string, btn: HTMLButtonElement): Promise {
- const hook = allItems.find((item) => item.id === hookId);
- if (!hook) {
- showToast('Hook not found.', 'error');
- return;
- }
-
- const files = [
- { name: 'README.md', path: hook.readmeFile },
- ...hook.assets.map((asset) => ({
- name: asset,
- path: `${hook.path}/${asset}`,
- })),
- ];
-
- if (files.length === 0) {
- showToast('No files found for this hook.', 'error');
- return;
- }
-
- const originalContent = btn.innerHTML;
- btn.disabled = true;
- btn.innerHTML = ' Preparing...';
-
- try {
- await downloadZipBundle(hook.id, files);
-
- btn.innerHTML = ' Downloaded!';
- setTimeout(() => {
- btn.disabled = false;
- btn.innerHTML = originalContent;
- }, 2000);
- } catch (error) {
- const message = error instanceof Error ? error.message : 'Download failed.';
- showToast(message, 'error');
- btn.innerHTML = ' Failed';
- setTimeout(() => {
- btn.disabled = false;
- btn.innerHTML = originalContent;
- }, 2000);
- }
-}
-
-function setupResourceListHandlers(list: HTMLElement | null): void {
- if (!list || resourceListHandlersReady) return;
-
- list.addEventListener('click', (event) => {
- const target = event.target as HTMLElement;
- const downloadButton = target.closest('.download-hook-btn') as HTMLButtonElement | null;
- if (downloadButton) {
- event.preventDefault();
- event.stopPropagation();
- const hookId = downloadButton.dataset.hookId;
- if (hookId) downloadHook(hookId, downloadButton);
- return;
- }
- });
-
- resourceListHandlersReady = true;
-}
-
-function syncUrlState(): void {
- updateQueryParams({
- q: '',
- hook: [],
- tag: currentFilters.tags,
- sort: currentSort === 'title' ? '' : currentSort,
- });
-}
-
-export async function initHooksPage(): Promise {
- const list = document.getElementById('resource-list');
- const clearFiltersBtn = document.getElementById('clear-filters');
- const sortSelect = document.getElementById('sort-select') as HTMLSelectElement | null;
-
- setupResourceListHandlers(list as HTMLElement | null);
-
- const data = await fetchData('hooks.json');
- if (!data || !data.items) {
- if (list) list.innerHTML = '
Failed to load data ';
- return;
- }
-
- allItems = data.items;
-
- tagSelectEl = document.getElementById('filter-tag') as HTMLSelectElement | null;
- if (tagSelectEl) {
- tagSelectEl.innerHTML = '';
- data.filters.tags.forEach((tag) => {
- const option = document.createElement('option');
- option.value = tag;
- option.textContent = tag;
- tagSelectEl?.appendChild(option);
- });
- }
-
- const initialTags = getQueryParamValues('tag').filter((tag) => data.filters.tags.includes(tag));
- const initialSort = getQueryParam('sort');
-
- if (initialTags.length > 0) {
- currentFilters.tags = initialTags;
- setSelectValues(tagSelectEl, initialTags);
- }
- if (initialSort === 'lastUpdated') {
- currentSort = initialSort;
- if (sortSelect) sortSelect.value = initialSort;
- }
-
- tagSelectEl?.addEventListener('change', () => {
- currentFilters.tags = getSelectValues(tagSelectEl);
- applyFiltersAndRender();
- syncUrlState();
- });
-
- sortSelect?.addEventListener('change', () => {
- currentSort = sortSelect.value as HookSortOption;
- applyFiltersAndRender();
- syncUrlState();
- });
-
- clearFiltersBtn?.addEventListener('click', () => {
- currentFilters = { tags: [] };
- currentSort = 'title';
- clearSelectValues(tagSelectEl);
- if (sortSelect) sortSelect.value = 'title';
- applyFiltersAndRender();
- syncUrlState();
- });
-
- applyFiltersAndRender();
-}
-
-// Auto-initialize when DOM is ready
-document.addEventListener('DOMContentLoaded', initHooksPage);
diff --git a/website/src/scripts/pages/samples-render.ts b/website/src/scripts/pages/samples-render.ts
index 87ebde3b..44eb677e 100644
--- a/website/src/scripts/pages/samples-render.ts
+++ b/website/src/scripts/pages/samples-render.ts
@@ -40,6 +40,19 @@ export interface CookbookRecipeMatch {
highlightedName?: string;
}
+function buildCookbookRecipeUrl(
+ cookbookId: string,
+ languageId: string,
+ recipeId: string,
+ filePath?: string | null
+): string {
+ const basePath = `/learning-hub/cookbook/${encodeURIComponent(
+ cookbookId
+ )}/${encodeURIComponent(languageId)}/${encodeURIComponent(recipeId)}/`;
+ if (!filePath) return basePath;
+ return `${basePath}#file=${encodeURIComponent(filePath)}`;
+}
+
export function getRecipeResultsCountText(
filteredCount: number,
totalCount: number
@@ -211,27 +224,32 @@ function renderRecipeCard(
${
variant
? `
-
-
+
View Recipe
-
+
${
variant.example
- ? `
-
-
-
-
- View Example
-
+
+
+
+ View Example
+
`
- : ""
+ : ""
}
{
search = new FuzzySearch(allRecipes);
// Setup UI
- setupModal();
+ handleLegacyCookbookHashLink();
setupFilters();
setupSearch();
- setupRecipeListeners();
+ setupInteractionListeners();
updateResultsCount();
} catch (error) {
console.error("Failed to initialize samples page:", error);
@@ -259,38 +258,16 @@ function renderCookbooks(): void {
});
// Setup event listeners
- setupRecipeListeners();
+ setupInteractionListeners();
}
/**
- * Setup event listeners for recipe interactions
+ * Setup event listeners for cookbook interactions
*/
-function setupRecipeListeners(): void {
- // View recipe buttons
- document.querySelectorAll(".view-recipe-btn").forEach((btn) => {
- btn.addEventListener("click", async (e) => {
- e.stopPropagation();
- const docPath = (btn as HTMLElement).dataset.doc;
- if (docPath) {
- await showRecipeContent(docPath, "recipe");
- }
- });
- });
-
- // View example buttons
- document.querySelectorAll(".view-example-btn").forEach((btn) => {
- btn.addEventListener("click", async (e) => {
- e.stopPropagation();
- const examplePath = (btn as HTMLElement).dataset.example;
- if (examplePath) {
- await showRecipeContent(examplePath, "example");
- }
- });
- });
-
+function setupInteractionListeners(): void {
// Language tab clicks
document.querySelectorAll(".lang-tab").forEach((tab) => {
- tab.addEventListener("click", (e) => {
+ tab.addEventListener("click", () => {
const langId = (tab as HTMLElement).dataset.lang;
if (langId) {
selectedLanguage = langId;
@@ -307,15 +284,60 @@ function setupRecipeListeners(): void {
}
/**
- * Show recipe/example content in modal
+ * Redirect legacy cookbook #file= links to canonical cookbook routes.
*/
-async function showRecipeContent(
- filePath: string,
- type: "recipe" | "example"
-): Promise {
- // Use existing modal infrastructure
- const { openFileModal } = await import("../modal");
- await openFileModal(filePath, type);
+function handleLegacyCookbookHashLink(): void {
+ const hashMatch = window.location.hash.match(/^#file=(.+)$/);
+ if (!hashMatch) return;
+
+ let filePath: string;
+ try {
+ filePath = decodeURIComponent(hashMatch[1]);
+ } catch {
+ return;
+ }
+
+ const parsed = parseCookbookPath(filePath);
+ if (!parsed) return;
+
+ const nextUrl = `/learning-hub/cookbook/${encodeURIComponent(
+ parsed.cookbook
+ )}/${encodeURIComponent(parsed.language)}/${encodeURIComponent(
+ parsed.recipe
+ )}/${parsed.example ? `#file=${encodeURIComponent(filePath)}` : ""}`;
+
+ window.location.replace(nextUrl);
+}
+
+function parseCookbookPath(filePath: string): {
+ cookbook: string;
+ language: string;
+ recipe: string;
+ example: boolean;
+} | null {
+ const docMatch = filePath.match(/^cookbook\/([^/]+)\/([^/]+)\/([^/]+)\.md$/);
+ if (docMatch) {
+ return {
+ cookbook: docMatch[1],
+ language: docMatch[2],
+ recipe: docMatch[3],
+ example: false,
+ };
+ }
+
+ const exampleMatch = filePath.match(
+ /^cookbook\/([^/]+)\/([^/]+)\/recipe\/([^/.]+)\.[^/.]+$/
+ );
+ if (exampleMatch) {
+ return {
+ cookbook: exampleMatch[1],
+ language: exampleMatch[2],
+ recipe: exampleMatch[3],
+ example: true,
+ };
+ }
+
+ return null;
}
/**
diff --git a/website/src/scripts/pages/tools-render.ts b/website/src/scripts/pages/tools-render.ts
deleted file mode 100644
index 30b46533..00000000
--- a/website/src/scripts/pages/tools-render.ts
+++ /dev/null
@@ -1,203 +0,0 @@
-import { escapeHtml } from "../utils";
-
-export interface RenderableTool {
- id: string;
- name: string;
- title: string;
- description: string;
- category: string;
- featured: boolean;
- requirements: string[];
- features: string[];
- links: {
- blog?: string;
- vscode?: string;
- "vscode-insiders"?: string;
- "visual-studio"?: string;
- github?: string;
- documentation?: string;
- marketplace?: string;
- npm?: string;
- pypi?: string;
- };
- configuration?: {
- type: string;
- content: string;
- } | null;
- tags: string[];
-}
-
-export type ToolSortOption = "featured" | "title";
-
-export function sortTools(
- tools: T[],
- sort: ToolSortOption
-): T[] {
- return [...tools].sort((a, b) => {
- if (sort === "featured") {
- if (a.featured && !b.featured) return -1;
- if (!a.featured && b.featured) return 1;
- }
-
- return a.title.localeCompare(b.title);
- });
-}
-
-function formatMultilineText(text: string): string {
- return escapeHtml(text).replace(/\r?\n/g, " ");
-}
-
-function sanitizeToolUrl(url: string): string {
- try {
- const protocol = new URL(url).protocol;
- if (
- protocol === "http:" ||
- protocol === "https:" ||
- protocol === "vscode:" ||
- protocol === "vscode-insiders:"
- ) {
- return escapeHtml(url);
- }
- } catch {
- return "#";
- }
-
- return "#";
-}
-
-function getToolActionLink(
- href: string | undefined,
- label: string,
- className: string
-): string {
- if (!href) return "";
- return `${label} `;
-}
-
-export function renderToolsHtml(
- tools: RenderableTool[]
-): string {
- if (tools.length === 0) {
- return `
-
-
No tools found
-
Try a different category or clear the current filters
-
- `;
- }
-
- return tools
- .map((tool) => {
- const badges: string[] = [];
- if (tool.featured) {
- badges.push('Featured ');
- }
- badges.push(
- `${escapeHtml(tool.category)} `
- );
-
- const features =
- tool.features && tool.features.length > 0
- ? ``
- : "";
-
- const requirements =
- tool.requirements && tool.requirements.length > 0
- ? ``
- : "";
-
- const tags =
- tool.tags && tool.tags.length > 0
- ? `
- ${tool.tags
- .map((tag) => `${escapeHtml(tag)} `)
- .join("")}
-
`
- : "";
-
- const config = tool.configuration
- ? ``
- : "";
-
- const actions = [
- getToolActionLink(tool.links.blog, "📖 Blog", "btn btn-secondary"),
- getToolActionLink(
- tool.links.marketplace,
- "🏪 Marketplace",
- "btn btn-secondary"
- ),
- getToolActionLink(tool.links.npm, "📦 npm", "btn btn-secondary"),
- getToolActionLink(tool.links.pypi, "🐍 PyPI", "btn btn-secondary"),
- getToolActionLink(
- tool.links.documentation,
- "📚 Docs",
- "btn btn-secondary"
- ),
- getToolActionLink(tool.links.github, "GitHub", "btn btn-secondary"),
- getToolActionLink(
- tool.links.vscode,
- "Install in VS Code",
- "btn btn-primary"
- ),
- getToolActionLink(
- tool.links["vscode-insiders"],
- "VS Code Insiders",
- "btn btn-outline"
- ),
- getToolActionLink(
- tool.links["visual-studio"],
- "Visual Studio",
- "btn btn-outline"
- ),
- ].filter(Boolean);
-
- const actionsHtml =
- actions.length > 0
- ? `${actions.join("")}
`
- : "";
-
- return `
-
- `;
- })
- .join("");
-}
diff --git a/website/src/scripts/pages/tools.ts b/website/src/scripts/pages/tools.ts
deleted file mode 100644
index 8519e8c8..00000000
--- a/website/src/scripts/pages/tools.ts
+++ /dev/null
@@ -1,211 +0,0 @@
-/**
- * Tools page functionality
- */
-import {
- fetchData,
- getQueryParam,
- updateQueryParams,
-} from "../utils";
-import {
- renderToolsHtml,
- sortTools,
- type ToolSortOption,
-} from "./tools-render";
-
-export interface Tool {
- id: string;
- name: string;
- title: string;
- description: string;
- category: string;
- featured: boolean;
- requirements: string[];
- features: string[];
- links: {
- blog?: string;
- vscode?: string;
- "vscode-insiders"?: string;
- "visual-studio"?: string;
- github?: string;
- documentation?: string;
- marketplace?: string;
- npm?: string;
- pypi?: string;
- };
- configuration?: {
- type: string;
- content: string;
- };
- tags: string[];
-}
-
-interface ToolsData {
- items: Tool[];
- filters: {
- categories: string[];
- tags: string[];
- };
-}
-
-let allItems: Tool[] = [];
-let currentFilters = {
- categories: [] as string[],
-};
-let currentSort: ToolSortOption = "featured";
-let copyHandlersReady = false;
-let initialized = false;
-
-function sortItems(items: Tool[]): Tool[] {
- return sortTools(items, currentSort);
-}
-
-function getCountText(resultsCount: number): string {
- if (currentFilters.categories.length === 0) {
- return `${resultsCount} tool${resultsCount === 1 ? "" : "s"}`;
- }
-
- return `${resultsCount} of ${allItems.length} tools (filtered by ${currentFilters.categories.length} categor${currentFilters.categories.length === 1 ? "y" : "ies"})`;
-}
-
-function applyFiltersAndRender(): void {
- const countEl = document.getElementById("results-count");
- let results = [...allItems];
-
- if (currentFilters.categories.length > 0) {
- results = results.filter((item) =>
- currentFilters.categories.includes(item.category)
- );
- }
-
- results = sortItems(results);
-
- renderTools(results);
- if (countEl) countEl.textContent = getCountText(results.length);
-}
-
-function renderTools(tools: Tool[]): void {
- const container = document.getElementById("tools-list");
- if (!container) return;
- container.innerHTML = renderToolsHtml(tools);
-}
-
-function syncUrlState(): void {
- updateQueryParams({
- q: "",
- category: currentFilters.categories,
- sort: currentSort === "featured" ? "" : currentSort,
- });
-}
-
-function setupCopyConfigHandlers(): void {
- if (copyHandlersReady) return;
-
- document.addEventListener("click", async (event) => {
- const button = (event.target as HTMLElement).closest(
- ".copy-config-btn"
- ) as HTMLButtonElement | null;
- if (!button) return;
-
- event.stopPropagation();
- const config = decodeURIComponent(button.dataset.config || "");
- try {
- await navigator.clipboard.writeText(config);
- button.classList.add("copied");
- const originalHtml = button.innerHTML;
- button.innerHTML = `
-
-
-
- Copied!
- `;
- setTimeout(() => {
- button.classList.remove("copied");
- button.innerHTML = originalHtml;
- }, 2000);
- } catch (err) {
- console.error("Failed to copy:", err);
- }
- });
-
- copyHandlersReady = true;
-}
-
-export async function initToolsPage(): Promise {
- if (initialized) return;
- initialized = true;
-
- const categoryFilter = document.getElementById(
- "filter-category"
- ) as HTMLSelectElement;
- const clearFiltersBtn = document.getElementById("clear-filters");
- const sortSelect = document.getElementById("sort-select") as HTMLSelectElement;
-
- const data = await fetchData("tools.json");
- if (!data || !data.items) {
- const container = document.getElementById("tools-list");
- if (container)
- container.innerHTML =
- '
Failed to load tools ';
- return;
- }
-
- // Map items to include title for FuzzySearch
- allItems = data.items.map((item) => ({
- ...item,
- title: item.name, // FuzzySearch uses title
- }));
-
- // Populate category filter
- if (categoryFilter && data.filters.categories) {
- categoryFilter.innerHTML =
- 'All Categories ' +
- data.filters.categories
- .map(
- (c) => `${c} `
- )
- .join("");
-
- const initialCategory = getQueryParam("category");
- if (initialCategory && data.filters.categories.includes(initialCategory)) {
- currentFilters.categories = [initialCategory];
- categoryFilter.value = initialCategory;
- }
-
- categoryFilter.addEventListener("change", () => {
- currentFilters.categories = categoryFilter.value
- ? [categoryFilter.value]
- : [];
- applyFiltersAndRender();
- syncUrlState();
- });
- }
-
- const initialSort = getQueryParam("sort");
- if (initialSort === "title") {
- currentSort = initialSort;
- if (sortSelect) sortSelect.value = initialSort;
- }
- sortSelect?.addEventListener("change", () => {
- currentSort = sortSelect.value as ToolSortOption;
- applyFiltersAndRender();
- syncUrlState();
- });
-
- applyFiltersAndRender();
- syncUrlState();
-
- // Clear filters
- clearFiltersBtn?.addEventListener("click", () => {
- currentFilters = { categories: [] };
- currentSort = "featured";
- if (categoryFilter) categoryFilter.value = "";
- if (sortSelect) sortSelect.value = "featured";
- applyFiltersAndRender();
- syncUrlState();
- });
-
- setupCopyConfigHandlers();
-}
-
-// Auto-initialize when DOM is ready
-document.addEventListener("DOMContentLoaded", initToolsPage);
diff --git a/website/src/scripts/pages/workflows-render.ts b/website/src/scripts/pages/workflows-render.ts
deleted file mode 100644
index b2b60bcc..00000000
--- a/website/src/scripts/pages/workflows-render.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import {
- escapeHtml,
- getActionButtonsHtml,
- getGitHubUrl,
- getLastUpdatedHtml,
-} from '../utils';
-import { renderEmptyStateHtml, renderSharedCardHtml } from './card-render';
-
-export interface RenderableWorkflow {
- id: string;
- title: string;
- description?: string;
- path: string;
- triggers: string[];
- lastUpdated?: string | null;
-}
-
-export type WorkflowSortOption = 'title' | 'lastUpdated';
-
-/**
- * Build the URL of a workflow's dedicated detail page.
- */
-export function getWorkflowDetailUrl(id: string): string {
- return `/workflow/${id}/`;
-}
-
-export function sortWorkflows(
- items: T[],
- sort: WorkflowSortOption
-): 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.title.localeCompare(b.title);
- });
-}
-
-export function renderWorkflowsHtml(
- items: RenderableWorkflow[]
-): string {
- if (items.length === 0) {
- return renderEmptyStateHtml('No workflows found', 'Try adjusting the selected filters.');
- }
-
- return items
- .map((item) => {
- const metaHtml = `
- ${item.triggers
- .map((trigger) => `${escapeHtml(trigger)} `)
- .join('')}
- ${getLastUpdatedHtml(item.lastUpdated)}
- `;
-
- const actionsHtml = `
- ${getActionButtonsHtml(item.path)}
- GitHub
- `;
-
- return renderSharedCardHtml({
- title: item.title,
- description: item.description || 'No description',
- href: getWorkflowDetailUrl(item.id),
- articleAttributes: {
- 'data-path': item.path,
- },
- metaHtml,
- actionsHtml,
- });
- })
- .join('');
-}
diff --git a/website/src/scripts/pages/workflows.ts b/website/src/scripts/pages/workflows.ts
deleted file mode 100644
index a74c522d..00000000
--- a/website/src/scripts/pages/workflows.ts
+++ /dev/null
@@ -1,139 +0,0 @@
-/**
- * Workflows page functionality
- */
-import {
- fetchData,
- getQueryParam,
- getQueryParamValues,
- setupActionHandlers,
- updateQueryParams,
-} from '../utils';
-import { clearSelectValues, getSelectValues, setSelectValues } from './select-utils';
-import {
- renderWorkflowsHtml,
- sortWorkflows,
- type RenderableWorkflow,
- type WorkflowSortOption,
-} from './workflows-render';
-
-interface Workflow extends RenderableWorkflow {
- id: string;
- path: string;
- triggers: string[];
- lastUpdated?: string | null;
-}
-
-interface WorkflowsData {
- items: Workflow[];
- filters: {
- triggers: string[];
- };
-}
-
-let allItems: Workflow[] = [];
-let triggerSelectEl: HTMLSelectElement | null = null;
-let currentFilters = {
- triggers: [] as string[],
-};
-let currentSort: WorkflowSortOption = 'title';
-
-function sortItems(items: Workflow[]): Workflow[] {
- return sortWorkflows(items, currentSort);
-}
-
-function applyFiltersAndRender(): void {
- const countEl = document.getElementById('results-count');
- let results = [...allItems];
-
- if (currentFilters.triggers.length > 0) {
- results = results.filter((item) => item.triggers.some((trigger) => currentFilters.triggers.includes(trigger)));
- }
-
- results = sortItems(results);
-
- renderItems(results);
- let countText = `${results.length} workflow${results.length === 1 ? '' : 's'}`;
- if (currentFilters.triggers.length > 0) {
- countText = `${results.length} of ${allItems.length} workflows (filtered by ${currentFilters.triggers.length} trigger${currentFilters.triggers.length > 1 ? 's' : ''})`;
- }
- if (countEl) countEl.textContent = countText;
-}
-
-function renderItems(items: Workflow[]): void {
- const list = document.getElementById('resource-list');
- if (!list) return;
-
- list.innerHTML = renderWorkflowsHtml(items);
-}
-
-function syncUrlState(): void {
- updateQueryParams({
- q: '',
- trigger: currentFilters.triggers,
- sort: currentSort === 'title' ? '' : currentSort,
- });
-}
-
-export async function initWorkflowsPage(): Promise {
- const list = document.getElementById('resource-list');
- const clearFiltersBtn = document.getElementById('clear-filters');
- const sortSelect = document.getElementById('sort-select') as HTMLSelectElement | null;
-
- const data = await fetchData('workflows.json');
- if (!data || !data.items) {
- if (list) list.innerHTML = '
Failed to load data ';
- return;
- }
-
- allItems = data.items;
-
- triggerSelectEl = document.getElementById('filter-trigger') as HTMLSelectElement | null;
- if (triggerSelectEl) {
- triggerSelectEl.innerHTML = '';
- data.filters.triggers.forEach((trigger) => {
- const option = document.createElement('option');
- option.value = trigger;
- option.textContent = trigger;
- triggerSelectEl?.appendChild(option);
- });
- }
-
- const initialTriggers = getQueryParamValues('trigger').filter((trigger) => data.filters.triggers.includes(trigger));
- const initialSort = getQueryParam('sort');
-
- if (initialTriggers.length > 0) {
- currentFilters.triggers = initialTriggers;
- setSelectValues(triggerSelectEl, initialTriggers);
- }
- if (initialSort === 'lastUpdated') {
- currentSort = initialSort;
- if (sortSelect) sortSelect.value = initialSort;
- }
-
- triggerSelectEl?.addEventListener('change', () => {
- currentFilters.triggers = getSelectValues(triggerSelectEl);
- applyFiltersAndRender();
- syncUrlState();
- });
-
- sortSelect?.addEventListener('change', () => {
- currentSort = sortSelect.value as WorkflowSortOption;
- applyFiltersAndRender();
- syncUrlState();
- });
-
- clearFiltersBtn?.addEventListener('click', () => {
- currentFilters = { triggers: [] };
- currentSort = 'title';
- clearSelectValues(triggerSelectEl);
- if (sortSelect) sortSelect.value = 'title';
- applyFiltersAndRender();
- syncUrlState();
- });
-
- applyFiltersAndRender();
- setupActionHandlers();
-}
-
-// Auto-initialize when DOM is ready
-document.addEventListener('DOMContentLoaded', initWorkflowsPage);
diff --git a/website/src/styles/global.css b/website/src/styles/global.css
index 2a238908..5a6347ba 100644
--- a/website/src/styles/global.css
+++ b/website/src/styles/global.css
@@ -945,436 +945,10 @@ body:has(#main-content) {
cursor: not-allowed;
}
-/* Modal */
-.modal {
- position: fixed;
- z-index: 1000001;
- top: 0;
- left: 0;
- width: 100%;
- height: 100vh;
- background-color: rgba(0, 0, 0, 0.6);
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 24px;
-}
-
-.modal.hidden, .hidden {
+.hidden {
display: none;
}
-.modal-content {
- background: var(--color-bg-secondary);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius-lg);
- width: 100%;
- max-width: 800px;
- max-height: 85vh;
- display: flex;
- flex-direction: column;
- box-shadow: var(--shadow-lg);
- overflow: hidden;
-}
-
-.modal-header {
- display: flex;
- flex-direction: column;
- gap: 14px;
- padding: 16px 20px;
- border-bottom: 1px solid var(--color-border);
-}
-
-.modal-header-top {
- gap: 16px;
- min-width: 0;
-}
-
-.modal-header-top h3 {
- font-size: 16px;
- font-weight: 600;
- color: var(--color-text-emphasis);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- min-width: 0;
- flex: 1;
-}
-
-.modal-file-switcher {
- display: flex;
- align-items: center;
- gap: 10px;
- min-width: 0;
-}
-
-.modal-file-switcher.hidden {
- display: none;
-}
-
-.modal-file-switcher label {
- font-size: 12px;
- font-weight: 600;
- color: var(--color-text-muted);
- text-transform: uppercase;
- letter-spacing: 0.04em;
-}
-
-.modal-file-switcher-label {
- font-size: 12px;
- font-weight: 600;
- color: var(--color-text-muted);
- text-transform: uppercase;
- letter-spacing: 0.04em;
-}
-
-.modal-file-dropdown {
- position: relative;
- display: inline-flex;
- min-width: min(320px, 100%);
- max-width: min(420px, 100%);
- flex: 1;
-}
-
-.modal-file-button {
- justify-content: flex-start;
- min-width: 0;
- flex: 1;
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- border-right: 1px solid var(--color-border);
-}
-
-.modal-file-button span {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.modal-file-toggle {
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
- padding: 10px;
- min-width: auto;
-}
-
-.modal-file-toggle svg {
- transition: transform var(--transition);
-}
-
-.modal-file-dropdown.open .modal-file-toggle svg {
- transform: rotate(180deg);
-}
-
-.modal-file-menu {
- position: absolute;
- top: 100%;
- left: 0;
- right: 0;
- margin-top: 4px;
- background: var(--color-bg-secondary);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius);
- box-shadow: var(--shadow-md);
- z-index: 1000;
- opacity: 0;
- visibility: hidden;
- transform: translateY(-8px);
- transition: all var(--transition);
- max-height: 280px;
- overflow-y: auto;
-}
-
-.modal-file-dropdown.open .modal-file-menu {
- opacity: 1;
- visibility: visible;
- transform: translateY(0);
-}
-
-.modal-file-menu-item {
- width: 100%;
- display: block;
- padding: 10px 14px;
- text-align: left;
- background: transparent;
- border: none;
- color: var(--color-text);
- font-size: 13px;
- cursor: pointer;
- transition: background var(--transition);
- margin: 0;
-}
-
-.modal-file-menu-item:hover,
-.modal-file-menu-item:focus-visible {
- background: var(--color-bg-tertiary);
- outline: none;
-}
-
-.modal-file-menu-item.active {
- background: color-mix(in srgb, var(--color-accent) 14%, transparent);
- color: var(--color-text-emphasis);
- font-weight: 600;
-}
-
-.modal-file-menu-item:first-child {
- border-radius: var(--border-radius) var(--border-radius) 0 0;
-}
-
-.modal-file-menu-item:last-child {
- border-radius: 0 0 var(--border-radius) var(--border-radius);
-}
-
-.modal-actions {
- display: flex;
- flex-wrap: wrap;
- justify-content: flex-end;
- align-items: center;
- gap: 8px;
-}
-
-.modal-actions button, .modal-actions div {
- margin: 0;
-}
-
-.modal-body {
- flex: 1;
- overflow: auto;
- padding: 0;
-}
-
-.modal-body pre {
- margin: 0;
- padding: 24px;
- font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
- font-size: 13px;
- line-height: 1.6;
- white-space: pre-wrap;
- word-wrap: break-word;
- background: var(--color-bg);
- color: var(--color-text);
- min-height: 200px;
-}
-
-.modal.details-mode .modal-content {
- max-width: 980px;
-}
-
-.modal.details-mode #modal-file-switcher,
-.modal.details-mode #install-command-btn,
-.modal.details-mode #copy-btn,
-.modal.details-mode #download-btn,
-.modal.details-mode #share-btn,
-.modal.details-mode #render-btn,
-.modal.details-mode #raw-btn,
-.modal.details-mode #install-dropdown {
- display: none !important;
-}
-
-.modal.details-mode .modal-card-details {
- padding: 18px;
-}
-
-.modal.details-mode .modal-card-details .resource-details-body {
- margin: 0;
-}
-
-.modal-rendered-content {
- padding: 24px;
- min-height: 200px;
- background: var(--color-bg);
- color: var(--color-text);
- line-height: 1.6;
-}
-
-.modal-rendered-content > :first-child {
- margin-top: 0;
-}
-
-.modal-rendered-content > :last-child {
- margin-bottom: 0;
-}
-
-.modal-code-content .shiki {
- margin: 0 !important;
- padding: 24px;
- min-height: 200px;
- border-radius: 0;
-}
-
-/* Collection Modal View */
-.collection-view {
- padding: 20px 24px;
-}
-
-.collection-description {
- font-size: 14px;
- color: var(--color-text);
- margin-bottom: 16px;
- line-height: 1.5;
-}
-
-.collection-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- margin-bottom: 16px;
-}
-
-.collection-items-header {
- padding: 10px 0;
- border-bottom: 1px solid var(--color-border);
- margin-bottom: 8px;
- color: var(--color-text-muted);
- font-size: 13px;
-}
-
-.collection-items-list {
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-.collection-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 10px 12px;
- background: var(--color-bg);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius);
- cursor: pointer;
- transition: all var(--transition);
-}
-
-.collection-item:hover {
- background: var(--color-bg-tertiary);
- border-color: var(--color-accent);
-}
-
-.collection-item-icon {
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- width: 20px;
- height: 20px;
-}
-
-.collection-item-icon svg {
- width: 16px;
- height: 16px;
-}
-
-.collection-item-info {
- flex: 1;
- min-width: 0;
-}
-
-.collection-item-name {
- font-size: 13px;
- font-weight: 500;
- color: var(--color-text);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.collection-item-usage {
- font-size: 12px;
- color: var(--color-text-muted);
- margin-top: 2px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.collection-item-type {
- font-size: 11px;
- color: var(--color-text-muted);
- background: var(--color-bg-tertiary);
- padding: 2px 6px;
- border-radius: 4px;
- flex-shrink: 0;
- text-transform: capitalize;
-}
-
-.collection-loading,
-.collection-error {
- padding: 40px;
- text-align: center;
- color: var(--color-text-muted);
-}
-
-.collection-error {
- color: var(--color-error);
-}
-
-/* External plugin badge */
-.resource-tag-external {
- background: var(--color-accent);
- color: var(--color-bg);
- font-weight: 500;
-}
-
-/* External plugin modal metadata */
-.external-plugin-metadata {
- display: flex;
- flex-direction: column;
- gap: 8px;
- margin-bottom: 20px;
- padding: 12px 16px;
- background: var(--color-bg-tertiary);
- border-radius: var(--border-radius);
- border: 1px solid var(--color-border);
-}
-
-.external-plugin-meta-row {
- display: flex;
- align-items: baseline;
- gap: 12px;
- font-size: 13px;
- line-height: 1.5;
-}
-
-.external-plugin-meta-label {
- color: var(--color-text-muted);
- font-weight: 500;
- min-width: 80px;
- flex-shrink: 0;
-}
-
-.external-plugin-meta-value {
- color: var(--color-text);
- word-break: break-all;
-}
-
-.external-plugin-meta-value a {
- color: var(--color-accent);
- text-decoration: none;
-}
-
-.external-plugin-meta-value a:hover {
- text-decoration: underline;
-}
-
-.external-plugin-cta {
- margin-bottom: 16px;
-}
-
-.external-plugin-repo-btn {
- display: inline-flex;
- align-items: center;
- gap: 6px;
-}
-
-.external-plugin-note {
- font-size: 13px;
- color: var(--color-text-muted);
- font-style: italic;
- line-height: 1.5;
-}
-
/* Page Layouts */
.page-header {
padding: 40px 0 28px;
@@ -3496,34 +3070,6 @@ header .theme-toggle-container {
transform: scale(0.95);
}
-/* ============================================
- MODAL ENTRANCE/EXIT ANIMATION
- ============================================ */
-
-.modal {
- opacity: 0;
- transition: opacity 0.2s ease;
-}
-
-.modal.visible {
- opacity: 1;
-}
-
-.modal.hidden {
- display: none;
-}
-
-.modal-content {
- transform: scale(0.95) translateY(10px);
- opacity: 0;
- transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s ease;
-}
-
-.modal.visible .modal-content {
- transform: scale(1) translateY(0);
- opacity: 1;
-}
-
/* ============================================
PAGE HEADER ENHANCEMENT
============================================ */