mirror of
https://github.com/github/awesome-copilot.git
synced 2026-03-14 13:15:13 +00:00
More website tweaks (#977)
* Some layout tweaks * SSR resource listing pages Render resource listing pages in Astro for first paint and hydrate client filtering/search behavior on top. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fixing font path * removing feature plugin reference as we don't track that anymore * button alignment * rendering markdown * Improve skills modal file browsing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Improving the layout of the search/filter section --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
* Modal functionality for file viewing
|
||||
*/
|
||||
|
||||
import { marked } from "marked";
|
||||
import {
|
||||
fetchFileContent,
|
||||
fetchData,
|
||||
@@ -15,11 +16,15 @@ import {
|
||||
getResourceIcon,
|
||||
sanitizeUrl,
|
||||
} from "./utils";
|
||||
import fm from "front-matter";
|
||||
|
||||
type ModalViewMode = "rendered" | "raw";
|
||||
|
||||
// Modal state
|
||||
let currentFilePath: string | null = null;
|
||||
let currentFileContent: string | null = null;
|
||||
let currentFileType: string | null = null;
|
||||
let currentViewMode: ModalViewMode = "raw";
|
||||
let triggerElement: HTMLElement | null = null;
|
||||
let originalDocumentTitle: string | null = null;
|
||||
|
||||
@@ -35,6 +40,22 @@ interface ResourceData {
|
||||
|
||||
const resourceDataCache: Record<string, ResourceData | null> = {};
|
||||
|
||||
interface SkillFile {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface SkillItem extends ResourceItem {
|
||||
skillFile: string;
|
||||
files: SkillFile[];
|
||||
}
|
||||
|
||||
interface SkillsData {
|
||||
items: SkillItem[];
|
||||
}
|
||||
|
||||
let skillsCache: SkillsData | null | undefined;
|
||||
|
||||
const RESOURCE_TYPE_TO_JSON: Record<string, string> = {
|
||||
agent: "agents.json",
|
||||
instruction: "instructions.json",
|
||||
@@ -66,17 +87,313 @@ async function resolveResourceTitle(
|
||||
const item = data.items.find((i) => i.path === filePath);
|
||||
if (item) return item.title;
|
||||
|
||||
// For skills/hooks, the modal receives the file path (e.g. skills/foo/SKILL.md)
|
||||
// but JSON stores the folder path (e.g. skills/foo)
|
||||
const parentPath = filePath.substring(0, filePath.lastIndexOf("/"));
|
||||
if (parentPath) {
|
||||
const parentItem = data.items.find((i) => i.path === parentPath);
|
||||
// For skills/hooks, bundled files live under the resource folder while
|
||||
// JSON stores the folder path itself (for example, skills/foo).
|
||||
const collectionRootPath =
|
||||
type === "skill"
|
||||
? getCollectionRootPath(filePath, "skills")
|
||||
: type === "hook"
|
||||
? getCollectionRootPath(filePath, "hooks")
|
||||
: filePath.substring(0, filePath.lastIndexOf("/"));
|
||||
|
||||
if (collectionRootPath) {
|
||||
const parentItem = data.items.find((i) => i.path === collectionRootPath);
|
||||
if (parentItem) return parentItem.title;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function getFileName(filePath: string): string {
|
||||
return filePath.split("/").pop() || filePath;
|
||||
}
|
||||
|
||||
function isMarkdownFile(filePath: string): boolean {
|
||||
return /\.(md|markdown|mdx)$/i.test(filePath);
|
||||
}
|
||||
|
||||
function getCollectionRootPath(filePath: string, collectionName: string): string | null {
|
||||
const segments = filePath.split("/");
|
||||
const collectionIndex = segments.indexOf(collectionName);
|
||||
if (collectionIndex === -1 || segments.length <= collectionIndex + 1) {
|
||||
return null;
|
||||
}
|
||||
return segments.slice(0, collectionIndex + 2).join("/");
|
||||
}
|
||||
|
||||
function getSkillRootPath(filePath: string): string | null {
|
||||
return getCollectionRootPath(filePath, "skills");
|
||||
}
|
||||
|
||||
async function getSkillsData(): Promise<SkillsData | null> {
|
||||
if (skillsCache === undefined) {
|
||||
skillsCache = await fetchData<SkillsData>("skills.json");
|
||||
}
|
||||
|
||||
return skillsCache;
|
||||
}
|
||||
|
||||
async function getSkillItemByFilePath(filePath: string): Promise<SkillItem | null> {
|
||||
if (getResourceType(filePath) !== "skill") return null;
|
||||
|
||||
const skillsData = await getSkillsData();
|
||||
if (!skillsData) return null;
|
||||
|
||||
const rootPath = getSkillRootPath(filePath);
|
||||
if (!rootPath) return null;
|
||||
|
||||
return (
|
||||
skillsData.items.find(
|
||||
(item) =>
|
||||
item.path === rootPath ||
|
||||
item.skillFile === filePath ||
|
||||
item.files.some((file) => file.path === filePath)
|
||||
) || null
|
||||
);
|
||||
}
|
||||
|
||||
function updateModalTitle(titleText: string, filePath: string): void {
|
||||
const title = document.getElementById("modal-title");
|
||||
if (title) {
|
||||
title.textContent = titleText;
|
||||
}
|
||||
|
||||
const fileName = getFileName(filePath);
|
||||
document.title =
|
||||
titleText === fileName
|
||||
? `${titleText} | Awesome GitHub Copilot`
|
||||
: `${titleText} · ${fileName} | Awesome GitHub Copilot`;
|
||||
}
|
||||
|
||||
function getModalBody(): HTMLElement | null {
|
||||
return document.querySelector<HTMLElement>(".modal-body");
|
||||
}
|
||||
|
||||
function getModalContent(): HTMLElement | null {
|
||||
return document.getElementById("modal-content");
|
||||
}
|
||||
|
||||
function ensurePreContent(): HTMLPreElement | null {
|
||||
let modalContent = getModalContent();
|
||||
if (!modalContent) return null;
|
||||
|
||||
if (modalContent.tagName === "PRE") {
|
||||
modalContent.className = "";
|
||||
if (!modalContent.querySelector("code")) {
|
||||
modalContent.innerHTML = "<code></code>";
|
||||
}
|
||||
return modalContent as HTMLPreElement;
|
||||
}
|
||||
|
||||
const modalBody = getModalBody();
|
||||
if (!modalBody) return null;
|
||||
|
||||
const pre = document.createElement("pre");
|
||||
pre.id = "modal-content";
|
||||
pre.innerHTML = "<code></code>";
|
||||
modalBody.replaceChild(pre, modalContent);
|
||||
return pre;
|
||||
}
|
||||
|
||||
function ensureDivContent(className: string): HTMLDivElement | null {
|
||||
let modalContent = getModalContent();
|
||||
if (!modalContent) return null;
|
||||
|
||||
if (modalContent.tagName === "DIV") {
|
||||
modalContent.className = className;
|
||||
return modalContent as HTMLDivElement;
|
||||
}
|
||||
|
||||
const modalBody = getModalBody();
|
||||
if (!modalBody) return null;
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.id = "modal-content";
|
||||
div.className = className;
|
||||
modalBody.replaceChild(div, modalContent);
|
||||
return div;
|
||||
}
|
||||
|
||||
function renderPlainText(content: string): void {
|
||||
const pre = ensurePreContent();
|
||||
const codeEl = pre?.querySelector("code");
|
||||
if (codeEl) {
|
||||
codeEl.textContent = content;
|
||||
}
|
||||
}
|
||||
|
||||
const EXTENSION_LANGUAGE_MAP: Record<string, string> = {
|
||||
bicep: "bicep",
|
||||
cjs: "javascript",
|
||||
css: "css",
|
||||
cs: "csharp",
|
||||
go: "go",
|
||||
html: "html",
|
||||
java: "java",
|
||||
js: "javascript",
|
||||
json: "json",
|
||||
jsx: "jsx",
|
||||
md: "md",
|
||||
markdown: "md",
|
||||
mdx: "mdx",
|
||||
mjs: "javascript",
|
||||
ps1: "powershell",
|
||||
psm1: "powershell",
|
||||
py: "python",
|
||||
rb: "ruby",
|
||||
rs: "rust",
|
||||
scss: "scss",
|
||||
sh: "bash",
|
||||
sql: "sql",
|
||||
toml: "toml",
|
||||
ts: "typescript",
|
||||
tsx: "tsx",
|
||||
txt: "text",
|
||||
xml: "xml",
|
||||
yaml: "yaml",
|
||||
yml: "yaml",
|
||||
};
|
||||
|
||||
const FILE_NAME_LANGUAGE_MAP: Record<string, string> = {
|
||||
dockerfile: "dockerfile",
|
||||
makefile: "makefile",
|
||||
};
|
||||
|
||||
function getLanguageForFile(filePath: string): string {
|
||||
const fileName = getFileName(filePath);
|
||||
const lowerFileName = fileName.toLowerCase();
|
||||
|
||||
if (FILE_NAME_LANGUAGE_MAP[lowerFileName]) {
|
||||
return FILE_NAME_LANGUAGE_MAP[lowerFileName];
|
||||
}
|
||||
|
||||
const extension = lowerFileName.includes(".")
|
||||
? lowerFileName.split(".").pop()
|
||||
: "";
|
||||
|
||||
if (extension && EXTENSION_LANGUAGE_MAP[extension]) {
|
||||
return EXTENSION_LANGUAGE_MAP[extension];
|
||||
}
|
||||
|
||||
return "text";
|
||||
}
|
||||
|
||||
async function renderHighlightedCode(content: string, filePath: string): Promise<void> {
|
||||
try {
|
||||
const { codeToHtml } = await import("shiki");
|
||||
const container = ensureDivContent("modal-code-content");
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = await codeToHtml(content, {
|
||||
lang: getLanguageForFile(filePath),
|
||||
themes: {
|
||||
light: "github-light",
|
||||
dark: "github-dark",
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
renderPlainText(content);
|
||||
}
|
||||
}
|
||||
|
||||
function updateViewButtons(): void {
|
||||
const renderBtn = document.getElementById("render-btn");
|
||||
const rawBtn = document.getElementById("raw-btn");
|
||||
const markdownFile = currentFilePath ? isMarkdownFile(currentFilePath) : false;
|
||||
|
||||
if (!renderBtn || !rawBtn) return;
|
||||
|
||||
if (!markdownFile) {
|
||||
renderBtn.classList.add("hidden");
|
||||
rawBtn.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentViewMode === "rendered") {
|
||||
renderBtn.classList.add("hidden");
|
||||
rawBtn.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
rawBtn.classList.add("hidden");
|
||||
renderBtn.classList.remove("hidden");
|
||||
}
|
||||
|
||||
async function renderCurrentFileContent(): Promise<void> {
|
||||
if (!currentFilePath) return;
|
||||
|
||||
updateViewButtons();
|
||||
|
||||
if (!currentFileContent) {
|
||||
renderPlainText(
|
||||
"Failed to load file content. Click the button below to view on GitHub."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMarkdownFile(currentFilePath) && currentViewMode === "rendered") {
|
||||
const container = ensureDivContent("modal-rendered-content");
|
||||
if (!container) return;
|
||||
|
||||
const { body: markdownBody } = fm(currentFileContent);
|
||||
container.innerHTML = marked(markdownBody, { async: false });
|
||||
} else {
|
||||
await renderHighlightedCode(currentFileContent, currentFilePath);
|
||||
}
|
||||
|
||||
const modalBody = getModalBody();
|
||||
if (modalBody) {
|
||||
modalBody.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
async function configureSkillFileSwitcher(filePath: string): Promise<void> {
|
||||
const switcher = document.getElementById("modal-file-switcher");
|
||||
const fileButtonLabel = document.getElementById("modal-file-button-label");
|
||||
const menu = document.getElementById("modal-file-menu");
|
||||
|
||||
if (!switcher || !fileButtonLabel || !menu) return;
|
||||
|
||||
const skillItem = await getSkillItemByFilePath(filePath);
|
||||
if (currentFilePath !== filePath) return;
|
||||
|
||||
if (!skillItem || skillItem.files.length <= 1) {
|
||||
switcher.classList.add("hidden");
|
||||
fileButtonLabel.textContent = "";
|
||||
menu.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
fileButtonLabel.textContent = getFileName(filePath);
|
||||
menu.innerHTML = skillItem.files
|
||||
.map(
|
||||
(file) =>
|
||||
`<button type="button" class="modal-file-menu-item${
|
||||
file.path === filePath ? " active" : ""
|
||||
}" data-path="${escapeHtml(file.path)}" role="menuitemradio" aria-checked="${
|
||||
file.path === filePath ? "true" : "false"
|
||||
}">${escapeHtml(file.name)}</button>`
|
||||
)
|
||||
.join("");
|
||||
switcher.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function hideSkillFileSwitcher(): void {
|
||||
const switcher = document.getElementById("modal-file-switcher");
|
||||
const fileButtonLabel = document.getElementById("modal-file-button-label");
|
||||
const menu = document.getElementById("modal-file-menu");
|
||||
const dropdown = document.getElementById("modal-file-dropdown");
|
||||
const fileButton = document.getElementById("modal-file-button");
|
||||
const fileToggle = document.getElementById("modal-file-toggle");
|
||||
|
||||
switcher?.classList.add("hidden");
|
||||
dropdown?.classList.remove("open");
|
||||
fileButton?.setAttribute("aria-expanded", "false");
|
||||
fileToggle?.setAttribute("aria-expanded", "false");
|
||||
if (fileButtonLabel) fileButtonLabel.textContent = "";
|
||||
if (menu) menu.innerHTML = "";
|
||||
}
|
||||
|
||||
// Plugin data cache
|
||||
interface PluginItem {
|
||||
path: string;
|
||||
@@ -170,10 +487,16 @@ export function setupModal(): void {
|
||||
const copyBtn = document.getElementById("copy-btn");
|
||||
const downloadBtn = document.getElementById("download-btn");
|
||||
const shareBtn = document.getElementById("share-btn");
|
||||
const renderBtn = document.getElementById("render-btn");
|
||||
const rawBtn = document.getElementById("raw-btn");
|
||||
const fileDropdown = document.getElementById("modal-file-dropdown");
|
||||
const fileButton = document.getElementById("modal-file-button");
|
||||
const fileToggle = document.getElementById("modal-file-toggle");
|
||||
const fileMenu = document.getElementById("modal-file-menu");
|
||||
|
||||
if (!modal) return;
|
||||
|
||||
closeBtn?.addEventListener("click", closeModal);
|
||||
closeBtn?.addEventListener("click", () => closeModal());
|
||||
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target === modal) closeModal();
|
||||
@@ -219,12 +542,124 @@ export function setupModal(): void {
|
||||
}
|
||||
});
|
||||
|
||||
renderBtn?.addEventListener("click", async () => {
|
||||
currentViewMode = "rendered";
|
||||
await renderCurrentFileContent();
|
||||
});
|
||||
|
||||
rawBtn?.addEventListener("click", async () => {
|
||||
currentViewMode = "raw";
|
||||
await renderCurrentFileContent();
|
||||
});
|
||||
|
||||
const setFileMenuOpen = (isOpen: boolean): void => {
|
||||
if (!fileDropdown) return;
|
||||
fileDropdown.classList.toggle("open", isOpen);
|
||||
fileButton?.setAttribute("aria-expanded", String(isOpen));
|
||||
fileToggle?.setAttribute("aria-expanded", String(isOpen));
|
||||
};
|
||||
|
||||
const toggleFileMenu = (event: Event): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const isOpen = !fileDropdown?.classList.contains("open");
|
||||
setFileMenuOpen(Boolean(isOpen));
|
||||
if (isOpen) {
|
||||
fileMenu
|
||||
?.querySelector<HTMLElement>(".modal-file-menu-item.active, .modal-file-menu-item")
|
||||
?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
fileButton?.addEventListener("click", toggleFileMenu);
|
||||
fileToggle?.addEventListener("click", toggleFileMenu);
|
||||
|
||||
fileButton?.addEventListener("keydown", (e) => {
|
||||
if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
|
||||
toggleFileMenu(e);
|
||||
}
|
||||
});
|
||||
|
||||
fileToggle?.addEventListener("keydown", (e) => {
|
||||
if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
|
||||
toggleFileMenu(e);
|
||||
}
|
||||
});
|
||||
|
||||
fileMenu?.addEventListener("click", async (event) => {
|
||||
const target = (event.target as HTMLElement).closest<HTMLButtonElement>(
|
||||
".modal-file-menu-item"
|
||||
);
|
||||
const targetPath = target?.dataset.path;
|
||||
if (!target || !targetPath || !currentFileType) return;
|
||||
setFileMenuOpen(false);
|
||||
await openFileModal(
|
||||
targetPath,
|
||||
currentFileType,
|
||||
true,
|
||||
triggerElement || undefined
|
||||
);
|
||||
});
|
||||
|
||||
fileMenu?.addEventListener("keydown", async (event) => {
|
||||
const items = Array.from(
|
||||
fileMenu.querySelectorAll<HTMLButtonElement>(".modal-file-menu-item")
|
||||
);
|
||||
const currentIndex = items.findIndex((item) => item === event.target);
|
||||
|
||||
switch (event.key) {
|
||||
case "ArrowDown":
|
||||
event.preventDefault();
|
||||
if (currentIndex >= 0 && currentIndex < items.length - 1) {
|
||||
items[currentIndex + 1].focus();
|
||||
}
|
||||
break;
|
||||
case "ArrowUp":
|
||||
event.preventDefault();
|
||||
if (currentIndex > 0) {
|
||||
items[currentIndex - 1].focus();
|
||||
} else {
|
||||
fileButton?.focus();
|
||||
}
|
||||
break;
|
||||
case "Escape":
|
||||
event.preventDefault();
|
||||
setFileMenuOpen(false);
|
||||
fileButton?.focus();
|
||||
break;
|
||||
case "Tab":
|
||||
setFileMenuOpen(false);
|
||||
break;
|
||||
case "Enter":
|
||||
case " ":
|
||||
if (currentIndex >= 0 && currentFileType) {
|
||||
const targetPath = items[currentIndex].dataset.path;
|
||||
if (!targetPath) return;
|
||||
event.preventDefault();
|
||||
setFileMenuOpen(false);
|
||||
await openFileModal(
|
||||
targetPath,
|
||||
currentFileType,
|
||||
true,
|
||||
triggerElement || undefined
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Setup install dropdown toggle
|
||||
setupInstallDropdown("install-dropdown");
|
||||
|
||||
// Handle browser back/forward navigation
|
||||
window.addEventListener("hashchange", handleHashChange);
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (fileDropdown && !fileDropdown.contains(e.target as Node)) {
|
||||
setFileMenuOpen(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Check for deep link on initial load
|
||||
handleHashChange();
|
||||
}
|
||||
@@ -372,8 +807,6 @@ export async function openFileModal(
|
||||
): Promise<void> {
|
||||
const modal = document.getElementById("file-modal");
|
||||
const title = document.getElementById("modal-title");
|
||||
let modalContent = document.getElementById("modal-content");
|
||||
const contentEl = modalContent?.querySelector("code");
|
||||
const installDropdown = document.getElementById("install-dropdown");
|
||||
const installBtnMain = document.getElementById(
|
||||
"install-btn-main"
|
||||
@@ -387,38 +820,29 @@ export async function openFileModal(
|
||||
const copyBtn = document.getElementById("copy-btn");
|
||||
const downloadBtn = document.getElementById("download-btn");
|
||||
const closeBtn = document.getElementById("close-modal");
|
||||
|
||||
if (!modal || !title || !modalContent) return;
|
||||
if (!modal || !title) return;
|
||||
|
||||
currentFilePath = filePath;
|
||||
currentFileType = type;
|
||||
currentViewMode = "raw";
|
||||
|
||||
// Track trigger element for focus return
|
||||
triggerElement = trigger || (document.activeElement as HTMLElement);
|
||||
triggerElement =
|
||||
trigger || triggerElement || (document.activeElement as HTMLElement);
|
||||
|
||||
// Update URL for deep linking
|
||||
if (updateUrl) {
|
||||
updateHash(filePath);
|
||||
}
|
||||
|
||||
// Show modal with loading state
|
||||
const fallbackName = filePath.split("/").pop() || filePath;
|
||||
title.textContent = fallbackName;
|
||||
modal.classList.remove("hidden");
|
||||
|
||||
// Update document title to reflect the open file
|
||||
if (!originalDocumentTitle) {
|
||||
originalDocumentTitle = document.title;
|
||||
}
|
||||
document.title = `${fallbackName} | Awesome GitHub Copilot`;
|
||||
|
||||
// Resolve the proper title from JSON data asynchronously
|
||||
resolveResourceTitle(filePath, type).then((resolvedTitle) => {
|
||||
if (currentFilePath === filePath) {
|
||||
title.textContent = resolvedTitle;
|
||||
document.title = `${resolvedTitle} | Awesome GitHub Copilot`;
|
||||
}
|
||||
});
|
||||
// Show modal with loading state
|
||||
const fallbackName = getFileName(filePath);
|
||||
updateModalTitle(fallbackName, filePath);
|
||||
modal.classList.remove("hidden");
|
||||
|
||||
// Set focus to close button for accessibility
|
||||
setTimeout(() => {
|
||||
@@ -427,6 +851,9 @@ export async function openFileModal(
|
||||
|
||||
// Handle plugins differently - show as item list
|
||||
if (type === "plugin") {
|
||||
const modalContent = getModalContent();
|
||||
if (!modalContent) return;
|
||||
hideSkillFileSwitcher();
|
||||
await openPluginModal(
|
||||
filePath,
|
||||
title,
|
||||
@@ -438,27 +865,12 @@ export async function openFileModal(
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular file modal
|
||||
if (contentEl) {
|
||||
contentEl.textContent = "Loading...";
|
||||
}
|
||||
|
||||
// Show copy/download buttons for regular files
|
||||
if (copyBtn) copyBtn.style.display = "inline-flex";
|
||||
if (downloadBtn) downloadBtn.style.display = "inline-flex";
|
||||
|
||||
// Restore pre/code structure if it was replaced by plugin view
|
||||
if (modalContent.tagName !== 'PRE') {
|
||||
const modalBody = modalContent.parentElement;
|
||||
if (modalBody) {
|
||||
const pre = document.createElement("pre");
|
||||
pre.id = "modal-content";
|
||||
pre.innerHTML = "<code></code>";
|
||||
modalBody.replaceChild(pre, modalContent);
|
||||
modalContent = pre;
|
||||
}
|
||||
}
|
||||
const codeEl = modalContent.querySelector("code");
|
||||
renderPlainText("Loading...");
|
||||
hideSkillFileSwitcher();
|
||||
updateViewButtons();
|
||||
|
||||
// Setup install dropdown
|
||||
const vscodeUrl = getVSCodeInstallUrl(type, filePath, false);
|
||||
@@ -474,16 +886,19 @@ export async function openFileModal(
|
||||
installDropdown.style.display = "none";
|
||||
}
|
||||
|
||||
// Fetch and display content
|
||||
const fileContent = await fetchFileContent(filePath);
|
||||
currentFileContent = fileContent;
|
||||
const [resolvedTitle, fileContent] = await Promise.all([
|
||||
resolveResourceTitle(filePath, type),
|
||||
fetchFileContent(filePath),
|
||||
type === "skill" ? configureSkillFileSwitcher(filePath) : Promise.resolve(),
|
||||
]);
|
||||
|
||||
if (fileContent && codeEl) {
|
||||
codeEl.textContent = fileContent;
|
||||
} else if (codeEl) {
|
||||
codeEl.textContent =
|
||||
"Failed to load file content. Click the button below to view on GitHub.";
|
||||
if (currentFilePath !== filePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateModalTitle(resolvedTitle, filePath);
|
||||
currentFileContent = fileContent;
|
||||
await renderCurrentFileContent();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -511,7 +926,8 @@ async function openPluginModal(
|
||||
modalBody.replaceChild(div, modalContent);
|
||||
modalContent = div;
|
||||
} else {
|
||||
modalContent.innerHTML = '<div class="collection-loading">Loading plugin...</div>';
|
||||
modalContent.innerHTML =
|
||||
'<div class="collection-loading">Loading plugin...</div>';
|
||||
}
|
||||
|
||||
// Load plugins data if not cached
|
||||
@@ -551,7 +967,9 @@ async function openPluginModal(
|
||||
function getExternalPluginUrl(plugin: Plugin): string {
|
||||
if (plugin.source?.source === "github" && plugin.source.repo) {
|
||||
const base = `https://github.com/${plugin.source.repo}`;
|
||||
return plugin.source.path ? `${base}/tree/main/${plugin.source.path}` : base;
|
||||
return plugin.source.path
|
||||
? `${base}/tree/main/${plugin.source.path}`
|
||||
: base;
|
||||
}
|
||||
// Sanitize URLs from JSON to prevent XSS via javascript:/data: schemes
|
||||
return sanitizeUrl(plugin.repository || plugin.homepage);
|
||||
@@ -569,7 +987,11 @@ function renderExternalPluginModal(
|
||||
<span class="external-plugin-meta-label">Author</span>
|
||||
<span class="external-plugin-meta-value">${
|
||||
plugin.author.url
|
||||
? `<a href="${sanitizeUrl(plugin.author.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(plugin.author.name)}</a>`
|
||||
? `<a href="${sanitizeUrl(
|
||||
plugin.author.url
|
||||
)}" target="_blank" rel="noopener noreferrer">${escapeHtml(
|
||||
plugin.author.name
|
||||
)}</a>`
|
||||
: escapeHtml(plugin.author.name)
|
||||
}</span>
|
||||
</div>`
|
||||
@@ -578,7 +1000,11 @@ function renderExternalPluginModal(
|
||||
const repoHtml = plugin.repository
|
||||
? `<div class="external-plugin-meta-row">
|
||||
<span class="external-plugin-meta-label">Repository</span>
|
||||
<span class="external-plugin-meta-value"><a href="${sanitizeUrl(plugin.repository)}" target="_blank" rel="noopener noreferrer">${escapeHtml(plugin.repository)}</a></span>
|
||||
<span class="external-plugin-meta-value"><a href="${sanitizeUrl(
|
||||
plugin.repository
|
||||
)}" target="_blank" rel="noopener noreferrer">${escapeHtml(
|
||||
plugin.repository
|
||||
)}</a></span>
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
@@ -586,21 +1012,31 @@ function renderExternalPluginModal(
|
||||
plugin.homepage && plugin.homepage !== plugin.repository
|
||||
? `<div class="external-plugin-meta-row">
|
||||
<span class="external-plugin-meta-label">Homepage</span>
|
||||
<span class="external-plugin-meta-value"><a href="${sanitizeUrl(plugin.homepage)}" target="_blank" rel="noopener noreferrer">${escapeHtml(plugin.homepage)}</a></span>
|
||||
<span class="external-plugin-meta-value"><a href="${sanitizeUrl(
|
||||
plugin.homepage
|
||||
)}" target="_blank" rel="noopener noreferrer">${escapeHtml(
|
||||
plugin.homepage
|
||||
)}</a></span>
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
const licenseHtml = plugin.license
|
||||
? `<div class="external-plugin-meta-row">
|
||||
<span class="external-plugin-meta-label">License</span>
|
||||
<span class="external-plugin-meta-value">${escapeHtml(plugin.license)}</span>
|
||||
<span class="external-plugin-meta-value">${escapeHtml(
|
||||
plugin.license
|
||||
)}</span>
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
const sourceHtml = plugin.source?.repo
|
||||
? `<div class="external-plugin-meta-row">
|
||||
<span class="external-plugin-meta-label">Source</span>
|
||||
<span class="external-plugin-meta-value">GitHub: ${escapeHtml(plugin.source.repo)}${plugin.source.path ? ` (${escapeHtml(plugin.source.path)})` : ""}</span>
|
||||
<span class="external-plugin-meta-value">GitHub: ${escapeHtml(
|
||||
plugin.source.repo
|
||||
)}${
|
||||
plugin.source.path ? ` (${escapeHtml(plugin.source.path)})` : ""
|
||||
}</span>
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
@@ -608,12 +1044,18 @@ function renderExternalPluginModal(
|
||||
|
||||
modalContent.innerHTML = `
|
||||
<div class="collection-view">
|
||||
<div class="collection-description">${escapeHtml(plugin.description || "")}</div>
|
||||
<div class="collection-description">${escapeHtml(
|
||||
plugin.description || ""
|
||||
)}</div>
|
||||
${
|
||||
plugin.tags && plugin.tags.length > 0
|
||||
? `<div class="collection-tags">
|
||||
<span class="resource-tag resource-tag-external">🔗 External Plugin</span>
|
||||
${plugin.tags.map((t) => `<span class="resource-tag">${escapeHtml(t)}</span>`).join("")}
|
||||
${plugin.tags
|
||||
.map(
|
||||
(t) => `<span class="resource-tag">${escapeHtml(t)}</span>`
|
||||
)
|
||||
.join("")}
|
||||
</div>`
|
||||
: `<div class="collection-tags">
|
||||
<span class="resource-tag resource-tag-external">🔗 External Plugin</span>
|
||||
@@ -627,7 +1069,9 @@ function renderExternalPluginModal(
|
||||
${sourceHtml}
|
||||
</div>
|
||||
<div class="external-plugin-cta">
|
||||
<a href="${sanitizeUrl(repoUrl)}" class="btn btn-primary external-plugin-repo-btn" target="_blank" rel="noopener noreferrer">
|
||||
<a href="${sanitizeUrl(
|
||||
repoUrl
|
||||
)}" class="btn btn-primary external-plugin-repo-btn" target="_blank" rel="noopener noreferrer">
|
||||
View Repository →
|
||||
</a>
|
||||
</div>
|
||||
@@ -745,7 +1189,9 @@ export function closeModal(updateUrl = true): void {
|
||||
currentFilePath = null;
|
||||
currentFileContent = null;
|
||||
currentFileType = null;
|
||||
currentViewMode = "raw";
|
||||
triggerElement = null;
|
||||
hideSkillFileSwitcher();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user