mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-20 02:15:12 +00:00
Refactor code for consistency and readability
- Standardized string quotes to double quotes across multiple files. - Improved formatting and indentation for better readability. - Added a function to format multiline text in tools rendering. - Enhanced dropdown and action button handlers for better event management. - Updated the theme application logic to initialize on page load. - Refactored utility functions for consistency and clarity. - Improved error handling and user feedback in download and share functionalities.
This commit is contained in:
23
.github/workflows/deploy-website.yml
vendored
23
.github/workflows/deploy-website.yml
vendored
@@ -8,14 +8,15 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
paths:
|
paths:
|
||||||
- 'website/**'
|
- "website/**"
|
||||||
- 'agents/**'
|
- "agents/**"
|
||||||
- 'prompts/**'
|
- "prompts/**"
|
||||||
- 'instructions/**'
|
- "instructions/**"
|
||||||
- 'skills/**'
|
- "skills/**"
|
||||||
- 'collections/**'
|
- "collections/**"
|
||||||
- 'eng/generate-website-data.mjs'
|
- "cookbook/**"
|
||||||
- '.github/workflows/deploy-website.yml'
|
- "eng/generate-website-data.mjs"
|
||||||
|
- ".github/workflows/deploy-website.yml"
|
||||||
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -43,8 +44,8 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: "20"
|
||||||
cache: 'npm'
|
cache: "npm"
|
||||||
|
|
||||||
- name: Install root dependencies
|
- name: Install root dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -66,7 +67,7 @@ jobs:
|
|||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: './website/dist'
|
path: "./website/dist"
|
||||||
|
|
||||||
# Deployment job
|
# Deployment job
|
||||||
deploy:
|
deploy:
|
||||||
|
|||||||
@@ -7,21 +7,20 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path, { dirname } from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { dirname } from "path";
|
|
||||||
import {
|
import {
|
||||||
AGENTS_DIR,
|
AGENTS_DIR,
|
||||||
INSTRUCTIONS_DIR,
|
|
||||||
PROMPTS_DIR,
|
|
||||||
SKILLS_DIR,
|
|
||||||
COLLECTIONS_DIR,
|
COLLECTIONS_DIR,
|
||||||
COOKBOOK_DIR,
|
COOKBOOK_DIR,
|
||||||
|
INSTRUCTIONS_DIR,
|
||||||
|
PROMPTS_DIR,
|
||||||
ROOT_FOLDER,
|
ROOT_FOLDER,
|
||||||
|
SKILLS_DIR,
|
||||||
} from "./constants.mjs";
|
} from "./constants.mjs";
|
||||||
import {
|
import {
|
||||||
parseFrontmatter,
|
|
||||||
parseCollectionYaml,
|
parseCollectionYaml,
|
||||||
|
parseFrontmatter,
|
||||||
parseSkillMetadata,
|
parseSkillMetadata,
|
||||||
parseYamlFile,
|
parseYamlFile,
|
||||||
} from "./yaml-parser.mjs";
|
} from "./yaml-parser.mjs";
|
||||||
@@ -63,17 +62,6 @@ function extractTitle(filePath, frontmatter) {
|
|||||||
.join(" ");
|
.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get file content (for preview/full content)
|
|
||||||
*/
|
|
||||||
function getFileContent(filePath) {
|
|
||||||
try {
|
|
||||||
return fs.readFileSync(filePath, "utf8");
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate agents metadata
|
* Generate agents metadata
|
||||||
*/
|
*/
|
||||||
@@ -90,7 +78,9 @@ function generateAgentsData() {
|
|||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const filePath = path.join(AGENTS_DIR, file);
|
const filePath = path.join(AGENTS_DIR, file);
|
||||||
const frontmatter = parseFrontmatter(filePath);
|
const frontmatter = parseFrontmatter(filePath);
|
||||||
const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/");
|
const relativePath = path
|
||||||
|
.relative(ROOT_FOLDER, filePath)
|
||||||
|
.replace(/\\/g, "/");
|
||||||
|
|
||||||
const model = frontmatter?.model || null;
|
const model = frontmatter?.model || null;
|
||||||
const tools = frontmatter?.tools || [];
|
const tools = frontmatter?.tools || [];
|
||||||
@@ -146,7 +136,9 @@ function generatePromptsData() {
|
|||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const filePath = path.join(PROMPTS_DIR, file);
|
const filePath = path.join(PROMPTS_DIR, file);
|
||||||
const frontmatter = parseFrontmatter(filePath);
|
const frontmatter = parseFrontmatter(filePath);
|
||||||
const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/");
|
const relativePath = path
|
||||||
|
.relative(ROOT_FOLDER, filePath)
|
||||||
|
.replace(/\\/g, "/");
|
||||||
|
|
||||||
const tools = frontmatter?.tools || [];
|
const tools = frontmatter?.tools || [];
|
||||||
tools.forEach((t) => allTools.add(t));
|
tools.forEach((t) => allTools.add(t));
|
||||||
@@ -181,12 +173,15 @@ function parseApplyToPatterns(applyTo) {
|
|||||||
|
|
||||||
// Handle array format
|
// Handle array format
|
||||||
if (Array.isArray(applyTo)) {
|
if (Array.isArray(applyTo)) {
|
||||||
return applyTo.map(p => p.trim()).filter(p => p.length > 0);
|
return applyTo.map((p) => p.trim()).filter((p) => p.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle string format (comma-separated)
|
// Handle string format (comma-separated)
|
||||||
if (typeof applyTo === 'string') {
|
if (typeof applyTo === "string") {
|
||||||
return applyTo.split(',').map(p => p.trim()).filter(p => p.length > 0);
|
return applyTo
|
||||||
|
.split(",")
|
||||||
|
.map((p) => p.trim())
|
||||||
|
.filter((p) => p.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
@@ -203,7 +198,7 @@ function extractExtensionFromPattern(pattern) {
|
|||||||
// Match patterns like **/*.{ts,tsx}
|
// Match patterns like **/*.{ts,tsx}
|
||||||
const braceMatch = pattern.match(/\*\.\{([^}]+)\}$/);
|
const braceMatch = pattern.match(/\*\.\{([^}]+)\}$/);
|
||||||
if (braceMatch) {
|
if (braceMatch) {
|
||||||
return braceMatch[1].split(',').map(ext => `.${ext.trim()}`);
|
return braceMatch[1].split(",").map((ext) => `.${ext.trim()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -225,7 +220,9 @@ function generateInstructionsData() {
|
|||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const filePath = path.join(INSTRUCTIONS_DIR, file);
|
const filePath = path.join(INSTRUCTIONS_DIR, file);
|
||||||
const frontmatter = parseFrontmatter(filePath);
|
const frontmatter = parseFrontmatter(filePath);
|
||||||
const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/");
|
const relativePath = path
|
||||||
|
.relative(ROOT_FOLDER, filePath)
|
||||||
|
.replace(/\\/g, "/");
|
||||||
|
|
||||||
const applyToRaw = frontmatter?.applyTo || null;
|
const applyToRaw = frontmatter?.applyTo || null;
|
||||||
const applyToPatterns = parseApplyToPatterns(applyToRaw);
|
const applyToPatterns = parseApplyToPatterns(applyToRaw);
|
||||||
@@ -237,7 +234,7 @@ function generateInstructionsData() {
|
|||||||
const ext = extractExtensionFromPattern(pattern);
|
const ext = extractExtensionFromPattern(pattern);
|
||||||
if (ext) {
|
if (ext) {
|
||||||
if (Array.isArray(ext)) {
|
if (Array.isArray(ext)) {
|
||||||
ext.forEach(e => {
|
ext.forEach((e) => {
|
||||||
extensions.push(e);
|
extensions.push(e);
|
||||||
allExtensions.add(e);
|
allExtensions.add(e);
|
||||||
});
|
});
|
||||||
@@ -260,7 +257,9 @@ function generateInstructionsData() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedInstructions = instructions.sort((a, b) => a.title.localeCompare(b.title));
|
const sortedInstructions = instructions.sort((a, b) =>
|
||||||
|
a.title.localeCompare(b.title)
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: sortedInstructions,
|
items: sortedInstructions,
|
||||||
@@ -277,16 +276,42 @@ function generateInstructionsData() {
|
|||||||
function categorizeSkill(name, description) {
|
function categorizeSkill(name, description) {
|
||||||
const text = `${name} ${description}`.toLowerCase();
|
const text = `${name} ${description}`.toLowerCase();
|
||||||
|
|
||||||
if (text.includes('azure') || text.includes('appinsights')) return 'Azure';
|
if (text.includes("azure") || text.includes("appinsights")) return "Azure";
|
||||||
if (text.includes('github') || text.includes('gh-cli') || text.includes('git-commit') || text.includes('git ')) return 'Git & GitHub';
|
if (
|
||||||
if (text.includes('vscode') || text.includes('vs code')) return 'VS Code';
|
text.includes("github") ||
|
||||||
if (text.includes('test') || text.includes('qa') || text.includes('playwright')) return 'Testing';
|
text.includes("gh-cli") ||
|
||||||
if (text.includes('microsoft') || text.includes('m365') || text.includes('workiq')) return 'Microsoft';
|
text.includes("git-commit") ||
|
||||||
if (text.includes('cli') || text.includes('command')) return 'CLI Tools';
|
text.includes("git ")
|
||||||
if (text.includes('diagram') || text.includes('plantuml') || text.includes('visual')) return 'Diagrams';
|
)
|
||||||
if (text.includes('nuget') || text.includes('dotnet') || text.includes('.net')) return '.NET';
|
return "Git & GitHub";
|
||||||
|
if (text.includes("vscode") || text.includes("vs code")) return "VS Code";
|
||||||
|
if (
|
||||||
|
text.includes("test") ||
|
||||||
|
text.includes("qa") ||
|
||||||
|
text.includes("playwright")
|
||||||
|
)
|
||||||
|
return "Testing";
|
||||||
|
if (
|
||||||
|
text.includes("microsoft") ||
|
||||||
|
text.includes("m365") ||
|
||||||
|
text.includes("workiq")
|
||||||
|
)
|
||||||
|
return "Microsoft";
|
||||||
|
if (text.includes("cli") || text.includes("command")) return "CLI Tools";
|
||||||
|
if (
|
||||||
|
text.includes("diagram") ||
|
||||||
|
text.includes("plantuml") ||
|
||||||
|
text.includes("visual")
|
||||||
|
)
|
||||||
|
return "Diagrams";
|
||||||
|
if (
|
||||||
|
text.includes("nuget") ||
|
||||||
|
text.includes("dotnet") ||
|
||||||
|
text.includes(".net")
|
||||||
|
)
|
||||||
|
return ".NET";
|
||||||
|
|
||||||
return 'Other';
|
return "Other";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -296,7 +321,7 @@ function generateSkillsData() {
|
|||||||
const skills = [];
|
const skills = [];
|
||||||
|
|
||||||
if (!fs.existsSync(SKILLS_DIR)) {
|
if (!fs.existsSync(SKILLS_DIR)) {
|
||||||
return { items: [], filters: { categories: [], hasAssets: ['Yes', 'No'] } };
|
return { items: [], filters: { categories: [], hasAssets: ["Yes", "No"] } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const folders = fs
|
const folders = fs
|
||||||
@@ -310,7 +335,9 @@ function generateSkillsData() {
|
|||||||
const metadata = parseSkillMetadata(skillPath);
|
const metadata = parseSkillMetadata(skillPath);
|
||||||
|
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
const relativePath = path.relative(ROOT_FOLDER, skillPath).replace(/\\/g, "/");
|
const relativePath = path
|
||||||
|
.relative(ROOT_FOLDER, skillPath)
|
||||||
|
.replace(/\\/g, "/");
|
||||||
const category = categorizeSkill(metadata.name, metadata.description);
|
const category = categorizeSkill(metadata.name, metadata.description);
|
||||||
allCategories.add(category);
|
allCategories.add(category);
|
||||||
|
|
||||||
@@ -342,7 +369,7 @@ function generateSkillsData() {
|
|||||||
items: sortedSkills,
|
items: sortedSkills,
|
||||||
filters: {
|
filters: {
|
||||||
categories: Array.from(allCategories).sort(),
|
categories: Array.from(allCategories).sort(),
|
||||||
hasAssets: ['Yes', 'No'],
|
hasAssets: ["Yes", "No"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -373,7 +400,7 @@ function getSkillFiles(skillPath, relativePath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
walkDir(skillPath, '');
|
walkDir(skillPath, "");
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,7 +424,9 @@ function generateCollectionsData() {
|
|||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const filePath = path.join(COLLECTIONS_DIR, file);
|
const filePath = path.join(COLLECTIONS_DIR, file);
|
||||||
const data = parseCollectionYaml(filePath);
|
const data = parseCollectionYaml(filePath);
|
||||||
const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/");
|
const relativePath = path
|
||||||
|
.relative(ROOT_FOLDER, filePath)
|
||||||
|
.replace(/\\/g, "/");
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
const tags = data.tags || [];
|
const tags = data.tags || [];
|
||||||
@@ -498,7 +527,13 @@ function generateToolsData() {
|
|||||||
/**
|
/**
|
||||||
* Generate a combined index for search
|
* Generate a combined index for search
|
||||||
*/
|
*/
|
||||||
function generateSearchIndex(agents, prompts, instructions, skills, collections) {
|
function generateSearchIndex(
|
||||||
|
agents,
|
||||||
|
prompts,
|
||||||
|
instructions,
|
||||||
|
skills,
|
||||||
|
collections
|
||||||
|
) {
|
||||||
const index = [];
|
const index = [];
|
||||||
|
|
||||||
for (const agent of agents) {
|
for (const agent of agents) {
|
||||||
@@ -508,7 +543,9 @@ function generateSearchIndex(agents, prompts, instructions, skills, collections)
|
|||||||
title: agent.title,
|
title: agent.title,
|
||||||
description: agent.description,
|
description: agent.description,
|
||||||
path: agent.path,
|
path: agent.path,
|
||||||
searchText: `${agent.title} ${agent.description} ${agent.tools.join(" ")}`.toLowerCase(),
|
searchText: `${agent.title} ${agent.description} ${agent.tools.join(
|
||||||
|
" "
|
||||||
|
)}`.toLowerCase(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,7 +567,9 @@ function generateSearchIndex(agents, prompts, instructions, skills, collections)
|
|||||||
title: instruction.title,
|
title: instruction.title,
|
||||||
description: instruction.description,
|
description: instruction.description,
|
||||||
path: instruction.path,
|
path: instruction.path,
|
||||||
searchText: `${instruction.title} ${instruction.description} ${instruction.applyTo || ""}`.toLowerCase(),
|
searchText: `${instruction.title} ${instruction.description} ${
|
||||||
|
instruction.applyTo || ""
|
||||||
|
}`.toLowerCase(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,7 +592,9 @@ function generateSearchIndex(agents, prompts, instructions, skills, collections)
|
|||||||
description: collection.description,
|
description: collection.description,
|
||||||
path: collection.path,
|
path: collection.path,
|
||||||
tags: collection.tags,
|
tags: collection.tags,
|
||||||
searchText: `${collection.name} ${collection.description} ${collection.tags.join(" ")}`.toLowerCase(),
|
searchText: `${collection.name} ${
|
||||||
|
collection.description
|
||||||
|
} ${collection.tags.join(" ")}`.toLowerCase(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,34 +608,46 @@ function generateSamplesData() {
|
|||||||
const cookbookYamlPath = path.join(COOKBOOK_DIR, "cookbook.yml");
|
const cookbookYamlPath = path.join(COOKBOOK_DIR, "cookbook.yml");
|
||||||
|
|
||||||
if (!fs.existsSync(cookbookYamlPath)) {
|
if (!fs.existsSync(cookbookYamlPath)) {
|
||||||
console.warn("Warning: cookbook/cookbook.yml not found, skipping samples generation");
|
console.warn(
|
||||||
return { cookbooks: [], totalRecipes: 0, totalCookbooks: 0, filters: { languages: [], tags: [] } };
|
"Warning: cookbook/cookbook.yml not found, skipping samples generation"
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
cookbooks: [],
|
||||||
|
totalRecipes: 0,
|
||||||
|
totalCookbooks: 0,
|
||||||
|
filters: { languages: [], tags: [] },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const cookbookManifest = parseYamlFile(cookbookYamlPath);
|
const cookbookManifest = parseYamlFile(cookbookYamlPath);
|
||||||
if (!cookbookManifest || !cookbookManifest.cookbooks) {
|
if (!cookbookManifest || !cookbookManifest.cookbooks) {
|
||||||
console.warn("Warning: Invalid cookbook.yml format");
|
console.warn("Warning: Invalid cookbook.yml format");
|
||||||
return { cookbooks: [], totalRecipes: 0, totalCookbooks: 0, filters: { languages: [], tags: [] } };
|
return {
|
||||||
|
cookbooks: [],
|
||||||
|
totalRecipes: 0,
|
||||||
|
totalCookbooks: 0,
|
||||||
|
filters: { languages: [], tags: [] },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const allLanguages = new Set();
|
const allLanguages = new Set();
|
||||||
const allTags = new Set();
|
const allTags = new Set();
|
||||||
let totalRecipes = 0;
|
let totalRecipes = 0;
|
||||||
|
|
||||||
const cookbooks = cookbookManifest.cookbooks.map(cookbook => {
|
const cookbooks = cookbookManifest.cookbooks.map((cookbook) => {
|
||||||
// Collect languages
|
// Collect languages
|
||||||
cookbook.languages.forEach(lang => allLanguages.add(lang.id));
|
cookbook.languages.forEach((lang) => allLanguages.add(lang.id));
|
||||||
|
|
||||||
// Process recipes and add file paths
|
// Process recipes and add file paths
|
||||||
const recipes = cookbook.recipes.map(recipe => {
|
const recipes = cookbook.recipes.map((recipe) => {
|
||||||
// Collect tags
|
// Collect tags
|
||||||
if (recipe.tags) {
|
if (recipe.tags) {
|
||||||
recipe.tags.forEach(tag => allTags.add(tag));
|
recipe.tags.forEach((tag) => allTags.add(tag));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build variants with file paths for each language
|
// Build variants with file paths for each language
|
||||||
const variants = {};
|
const variants = {};
|
||||||
cookbook.languages.forEach(lang => {
|
cookbook.languages.forEach((lang) => {
|
||||||
const docPath = `${cookbook.path}/${lang.id}/${recipe.id}.md`;
|
const docPath = `${cookbook.path}/${lang.id}/${recipe.id}.md`;
|
||||||
const examplePath = `${cookbook.path}/${lang.id}/recipe/${recipe.id}${lang.extension}`;
|
const examplePath = `${cookbook.path}/${lang.id}/recipe/${recipe.id}${lang.extension}`;
|
||||||
|
|
||||||
@@ -605,7 +658,7 @@ function generateSamplesData() {
|
|||||||
if (fs.existsSync(docFullPath)) {
|
if (fs.existsSync(docFullPath)) {
|
||||||
variants[lang.id] = {
|
variants[lang.id] = {
|
||||||
doc: docPath,
|
doc: docPath,
|
||||||
example: fs.existsSync(exampleFullPath) ? examplePath : null
|
example: fs.existsSync(exampleFullPath) ? examplePath : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -617,7 +670,7 @@ function generateSamplesData() {
|
|||||||
name: recipe.name,
|
name: recipe.name,
|
||||||
description: recipe.description,
|
description: recipe.description,
|
||||||
tags: recipe.tags || [],
|
tags: recipe.tags || [],
|
||||||
variants
|
variants,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -628,7 +681,7 @@ function generateSamplesData() {
|
|||||||
path: cookbook.path,
|
path: cookbook.path,
|
||||||
featured: cookbook.featured || false,
|
featured: cookbook.featured || false,
|
||||||
languages: cookbook.languages,
|
languages: cookbook.languages,
|
||||||
recipes
|
recipes,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -638,8 +691,8 @@ function generateSamplesData() {
|
|||||||
totalCookbooks: cookbooks.length,
|
totalCookbooks: cookbooks.length,
|
||||||
filters: {
|
filters: {
|
||||||
languages: Array.from(allLanguages).sort(),
|
languages: Array.from(allLanguages).sort(),
|
||||||
tags: Array.from(allTags).sort()
|
tags: Array.from(allTags).sort(),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,32 +707,52 @@ async function main() {
|
|||||||
// Generate all data
|
// Generate all data
|
||||||
const agentsData = generateAgentsData();
|
const agentsData = generateAgentsData();
|
||||||
const agents = agentsData.items;
|
const agents = agentsData.items;
|
||||||
console.log(`✓ Generated ${agents.length} agents (${agentsData.filters.models.length} models, ${agentsData.filters.tools.length} tools)`);
|
console.log(
|
||||||
|
`✓ Generated ${agents.length} agents (${agentsData.filters.models.length} models, ${agentsData.filters.tools.length} tools)`
|
||||||
|
);
|
||||||
|
|
||||||
const promptsData = generatePromptsData();
|
const promptsData = generatePromptsData();
|
||||||
const prompts = promptsData.items;
|
const prompts = promptsData.items;
|
||||||
console.log(`✓ Generated ${prompts.length} prompts (${promptsData.filters.tools.length} tools)`);
|
console.log(
|
||||||
|
`✓ Generated ${prompts.length} prompts (${promptsData.filters.tools.length} tools)`
|
||||||
|
);
|
||||||
|
|
||||||
const instructionsData = generateInstructionsData();
|
const instructionsData = generateInstructionsData();
|
||||||
const instructions = instructionsData.items;
|
const instructions = instructionsData.items;
|
||||||
console.log(`✓ Generated ${instructions.length} instructions (${instructionsData.filters.extensions.length} extensions)`);
|
console.log(
|
||||||
|
`✓ Generated ${instructions.length} instructions (${instructionsData.filters.extensions.length} extensions)`
|
||||||
|
);
|
||||||
|
|
||||||
const skillsData = generateSkillsData();
|
const skillsData = generateSkillsData();
|
||||||
const skills = skillsData.items;
|
const skills = skillsData.items;
|
||||||
console.log(`✓ Generated ${skills.length} skills (${skillsData.filters.categories.length} categories)`);
|
console.log(
|
||||||
|
`✓ Generated ${skills.length} skills (${skillsData.filters.categories.length} categories)`
|
||||||
|
);
|
||||||
|
|
||||||
const collectionsData = generateCollectionsData();
|
const collectionsData = generateCollectionsData();
|
||||||
const collections = collectionsData.items;
|
const collections = collectionsData.items;
|
||||||
console.log(`✓ Generated ${collections.length} collections (${collectionsData.filters.tags.length} tags)`);
|
console.log(
|
||||||
|
`✓ Generated ${collections.length} collections (${collectionsData.filters.tags.length} tags)`
|
||||||
|
);
|
||||||
|
|
||||||
const toolsData = generateToolsData();
|
const toolsData = generateToolsData();
|
||||||
const tools = toolsData.items;
|
const tools = toolsData.items;
|
||||||
console.log(`✓ Generated ${tools.length} tools (${toolsData.filters.categories.length} categories)`);
|
console.log(
|
||||||
|
`✓ Generated ${tools.length} tools (${toolsData.filters.categories.length} categories)`
|
||||||
|
);
|
||||||
|
|
||||||
const samplesData = generateSamplesData();
|
const samplesData = generateSamplesData();
|
||||||
console.log(`✓ Generated ${samplesData.totalRecipes} recipes in ${samplesData.totalCookbooks} cookbooks (${samplesData.filters.languages.length} languages, ${samplesData.filters.tags.length} tags)`);
|
console.log(
|
||||||
|
`✓ Generated ${samplesData.totalRecipes} recipes in ${samplesData.totalCookbooks} cookbooks (${samplesData.filters.languages.length} languages, ${samplesData.filters.tags.length} tags)`
|
||||||
|
);
|
||||||
|
|
||||||
const searchIndex = generateSearchIndex(agents, prompts, instructions, skills, collections);
|
const searchIndex = generateSearchIndex(
|
||||||
|
agents,
|
||||||
|
prompts,
|
||||||
|
instructions,
|
||||||
|
skills,
|
||||||
|
collections
|
||||||
|
);
|
||||||
console.log(`✓ Generated search index with ${searchIndex.length} items`);
|
console.log(`✓ Generated search index with ${searchIndex.length} items`);
|
||||||
|
|
||||||
// Write JSON files
|
// Write JSON files
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { defineConfig } from "astro/config";
|
|||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: "https://github.github.io",
|
site: "https://github.github.io/awesome-copilot",
|
||||||
base: "/",
|
base: "/awesome-copilot/",
|
||||||
output: "static",
|
output: "static",
|
||||||
integrations: [sitemap()],
|
integrations: [sitemap()],
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
@@ -24,6 +24,12 @@
|
|||||||
--color-text: #e4e4ec;
|
--color-text: #e4e4ec;
|
||||||
--color-text-muted: #9090a8;
|
--color-text-muted: #9090a8;
|
||||||
--color-text-emphasis: #ffffff;
|
--color-text-emphasis: #ffffff;
|
||||||
|
--color-text-primary: var(--color-text);
|
||||||
|
--color-text-secondary: var(--color-text-muted);
|
||||||
|
--color-bg-primary: var(--color-bg);
|
||||||
|
--color-primary: var(--color-accent);
|
||||||
|
--color-purple-light: #C898FD;
|
||||||
|
--color-purple-dark: #43179E;
|
||||||
--color-link: #B870FF;
|
--color-link: #B870FF;
|
||||||
--color-link-hover: #C898FD;
|
--color-link-hover: #C898FD;
|
||||||
--color-accent: #8534F3;
|
--color-accent: #8534F3;
|
||||||
@@ -46,12 +52,14 @@
|
|||||||
--border-radius-lg: 16px;
|
--border-radius-lg: 16px;
|
||||||
--border-radius-xl: 24px;
|
--border-radius-xl: 24px;
|
||||||
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2), 0 2px 4px -2px rgba(0, 0, 0, 0.2);
|
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2), 0 2px 4px -2px rgba(0, 0, 0, 0.2);
|
||||||
|
--shadow-md: 0 12px 24px -10px rgba(0, 0, 0, 0.4);
|
||||||
--shadow-lg: 0 20px 40px -12px rgba(0, 0, 0, 0.5);
|
--shadow-lg: 0 20px 40px -12px rgba(0, 0, 0, 0.5);
|
||||||
--shadow-glow: 0 0 40px -10px rgba(133, 52, 243, 0.5);
|
--shadow-glow: 0 0 40px -10px rgba(133, 52, 243, 0.5);
|
||||||
--transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
--transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
--transition-slow: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
--transition-slow: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
--container-width: 1200px;
|
--container-width: 1200px;
|
||||||
--header-height: 72px;
|
--header-height: 72px;
|
||||||
|
--font-mono: 'Monaspace Argon NF', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light theme */
|
/* Light theme */
|
||||||
@@ -735,6 +743,17 @@ a:hover {
|
|||||||
border-color: var(--color-border);
|
border-color: var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover {
|
||||||
|
background: var(--color-bg-tertiary);
|
||||||
|
border-color: var(--color-link);
|
||||||
|
}
|
||||||
|
|
||||||
.btn-icon {
|
.btn-icon {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -1360,6 +1379,7 @@ a:hover {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,80 +5,161 @@ interface Props {
|
|||||||
activeNav?: string;
|
activeNav?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, description = 'Community-driven collection of custom agents, prompts, and instructions for GitHub Copilot', activeNav = '' } = Astro.props;
|
const {
|
||||||
|
title,
|
||||||
|
description = "Community-driven collection of custom agents, prompts, and instructions for GitHub Copilot",
|
||||||
|
activeNav = "",
|
||||||
|
} = Astro.props;
|
||||||
const base = import.meta.env.BASE_URL;
|
const base = import.meta.env.BASE_URL;
|
||||||
---
|
---
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>{title} - Awesome GitHub Copilot</title>
|
<title>{title} - Awesome GitHub Copilot</title>
|
||||||
<meta name="description" content={description}>
|
<meta name="description" content={description} />
|
||||||
<link rel="stylesheet" href={`${base}styles/global.css`}>
|
<link rel="stylesheet" href={`${base}styles/global.css`} />
|
||||||
<link rel="icon" href={`${base}images/Copilot_Icon_Black.svg`} type="image/svg+xml">
|
<link
|
||||||
<script>
|
rel="icon"
|
||||||
// Theme toggle - inline for immediate effect (prevent flash)
|
href={`${base}images/Copilot_Icon_Black.svg`}
|
||||||
(function() {
|
type="image/svg+xml"
|
||||||
const theme = localStorage.getItem('theme') ||
|
/>
|
||||||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
</head>
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
<body data-base-path={base}>
|
||||||
})();
|
<a href="#main-content" class="skip-link">Skip to main content</a>
|
||||||
</script>
|
<header class="site-header">
|
||||||
</head>
|
<div class="container">
|
||||||
<body data-base-path={base}>
|
<div class="header-content">
|
||||||
<a href="#main-content" class="skip-link">Skip to main content</a>
|
<a href={base} class="logo">
|
||||||
<header class="site-header">
|
<img
|
||||||
<div class="container">
|
src={`${base}images/Copilot_Icon_White.svg`}
|
||||||
<div class="header-content">
|
alt=""
|
||||||
<a href={base} class="logo">
|
class="logo-icon logo-icon-dark"
|
||||||
<img src={`${base}images/Copilot_Icon_White.svg`} alt="" class="logo-icon logo-icon-dark" width="32" height="32" aria-hidden="true" />
|
width="32"
|
||||||
<img src={`${base}images/Copilot_Icon_Black.svg`} alt="" class="logo-icon logo-icon-light" width="32" height="32" aria-hidden="true" />
|
height="32"
|
||||||
<span class="logo-text">Awesome Copilot</span>
|
aria-hidden="true"
|
||||||
</a>
|
/>
|
||||||
<nav class="main-nav" aria-label="Main navigation">
|
<img
|
||||||
<a href={`${base}agents/`} class:list={[{ active: activeNav === 'agents' }]}>Agents</a>
|
src={`${base}images/Copilot_Icon_Black.svg`}
|
||||||
<a href={`${base}prompts/`} class:list={[{ active: activeNav === 'prompts' }]}>Prompts</a>
|
alt=""
|
||||||
<a href={`${base}instructions/`} class:list={[{ active: activeNav === 'instructions' }]}>Instructions</a>
|
class="logo-icon logo-icon-light"
|
||||||
<a href={`${base}skills/`} class:list={[{ active: activeNav === 'skills' }]}>Skills</a>
|
width="32"
|
||||||
<a href={`${base}collections/`} class:list={[{ active: activeNav === 'collections' }]}>Collections</a>
|
height="32"
|
||||||
<a href={`${base}tools/`} class:list={[{ active: activeNav === 'tools' }]}>Tools</a>
|
aria-hidden="true"
|
||||||
<a href={`${base}samples/`} class:list={[{ active: activeNav === 'samples' }]}>Samples</a>
|
/>
|
||||||
</nav>
|
<span class="logo-text">Awesome Copilot</span>
|
||||||
<div class="header-actions">
|
|
||||||
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
|
|
||||||
<svg class="icon-sun" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
||||||
<path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0V.75A.75.75 0 0 1 8 0zm0 13a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 8 13zM2.343 2.343a.75.75 0 0 1 1.061 0l1.06 1.061a.75.75 0 0 1-1.06 1.06l-1.06-1.06a.75.75 0 0 1 0-1.06zm9.193 9.193a.75.75 0 0 1 1.06 0l1.061 1.06a.75.75 0 0 1-1.06 1.061l-1.061-1.06a.75.75 0 0 1 0-1.061zM0 8a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 8zm13 0a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 0 1.5h-1.5A.75.75 0 0 1 13 8zM2.343 13.657a.75.75 0 0 1 0-1.061l1.06-1.06a.75.75 0 0 1 1.061 1.06l-1.06 1.06a.75.75 0 0 1-1.061 0zm9.193-9.193a.75.75 0 0 1 0-1.06l1.061-1.061a.75.75 0 0 1 1.06 1.06l-1.06 1.061a.75.75 0 0 1-1.061 0z"/>
|
|
||||||
</svg>
|
|
||||||
<svg class="icon-moon" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
||||||
<path d="M9.598 1.591a.75.75 0 0 1 .785-.175 7 7 0 1 1-8.967 8.967.75.75 0 0 1 .961-.96 5.5 5.5 0 0 0 7.046-7.046.75.75 0 0 1 .175-.786zm1.616 1.945a7 7 0 0 1-7.678 7.678 5.5 5.5 0 1 0 7.678-7.678z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<a href="https://github.com/github/awesome-copilot" class="github-link" target="_blank" rel="noopener" aria-label="View on GitHub">
|
|
||||||
<svg viewBox="0 0 16 16" width="24" height="24" fill="currentColor" aria-hidden="true">
|
|
||||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path>
|
|
||||||
</svg>
|
|
||||||
</a>
|
</a>
|
||||||
|
<nav class="main-nav" aria-label="Main navigation">
|
||||||
|
<a
|
||||||
|
href={`${base}agents/`}
|
||||||
|
class:list={[{ active: activeNav === "agents" }]}>Agents</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={`${base}prompts/`}
|
||||||
|
class:list={[{ active: activeNav === "prompts" }]}>Prompts</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={`${base}instructions/`}
|
||||||
|
class:list={[{ active: activeNav === "instructions" }]}
|
||||||
|
>Instructions</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={`${base}skills/`}
|
||||||
|
class:list={[{ active: activeNav === "skills" }]}>Skills</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={`${base}collections/`}
|
||||||
|
class:list={[{ active: activeNav === "collections" }]}
|
||||||
|
>Collections</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={`${base}tools/`}
|
||||||
|
class:list={[{ active: activeNav === "tools" }]}>Tools</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={`${base}samples/`}
|
||||||
|
class:list={[{ active: activeNav === "samples" }]}>Samples</a
|
||||||
|
>
|
||||||
|
</nav>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button
|
||||||
|
id="theme-toggle"
|
||||||
|
class="theme-toggle"
|
||||||
|
aria-label="Toggle theme"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="icon-sun"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0V.75A.75.75 0 0 1 8 0zm0 13a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 8 13zM2.343 2.343a.75.75 0 0 1 1.061 0l1.06 1.061a.75.75 0 0 1-1.06 1.06l-1.06-1.06a.75.75 0 0 1 0-1.06zm9.193 9.193a.75.75 0 0 1 1.06 0l1.061 1.06a.75.75 0 0 1-1.06 1.061l-1.061-1.06a.75.75 0 0 1 0-1.061zM0 8a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 8zm13 0a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 0 1.5h-1.5A.75.75 0 0 1 13 8zM2.343 13.657a.75.75 0 0 1 0-1.061l1.06-1.06a.75.75 0 0 1 1.061 1.06l-1.06 1.06a.75.75 0 0 1-1.061 0zm9.193-9.193a.75.75 0 0 1 0-1.06l1.061-1.061a.75.75 0 0 1 1.06 1.06l-1.06 1.061a.75.75 0 0 1-1.061 0z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
class="icon-moon"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M9.598 1.591a.75.75 0 0 1 .785-.175 7 7 0 1 1-8.967 8.967.75.75 0 0 1 .961-.96 5.5 5.5 0 0 0 7.046-7.046.75.75 0 0 1 .175-.786zm1.616 1.945a7 7 0 0 1-7.678 7.678 5.5 5.5 0 1 0 7.678-7.678z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href="https://github.com/github/awesome-copilot"
|
||||||
|
class="github-link"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
aria-label="View on GitHub"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
</header>
|
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<p>
|
<p>
|
||||||
<a href="https://github.com/github/awesome-copilot" target="_blank" rel="noopener">GitHub</a> ·
|
<a
|
||||||
<a href="https://github.com/github/awesome-copilot/blob/main/CONTRIBUTING.md" target="_blank" rel="noopener">Contribute</a> ·
|
href="https://github.com/github/awesome-copilot"
|
||||||
<a href="https://github.com/github/awesome-copilot/blob/main/LICENSE" target="_blank" rel="noopener">MIT License</a>
|
target="_blank"
|
||||||
</p>
|
rel="noopener">GitHub</a
|
||||||
</div>
|
> ·
|
||||||
</footer>
|
<a
|
||||||
|
href="https://github.com/github/awesome-copilot/blob/main/CONTRIBUTING.md"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener">Contribute</a
|
||||||
|
> ·
|
||||||
|
<a
|
||||||
|
href="https://github.com/github/awesome-copilot/blob/main/LICENSE"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener">MIT License</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import '../scripts/theme';
|
import "../scripts/theme";
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -2,7 +2,18 @@
|
|||||||
* Modal functionality for file viewing
|
* Modal functionality for file viewing
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { fetchFileContent, fetchData, getVSCodeInstallUrl, copyToClipboard, showToast, downloadFile, shareFile, getResourceType, escapeHtml, getResourceIcon } from './utils';
|
import {
|
||||||
|
fetchFileContent,
|
||||||
|
fetchData,
|
||||||
|
getVSCodeInstallUrl,
|
||||||
|
copyToClipboard,
|
||||||
|
showToast,
|
||||||
|
downloadFile,
|
||||||
|
shareFile,
|
||||||
|
getResourceType,
|
||||||
|
escapeHtml,
|
||||||
|
getResourceIcon,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
// Modal state
|
// Modal state
|
||||||
let currentFilePath: string | null = null;
|
let currentFilePath: string | null = null;
|
||||||
@@ -37,23 +48,24 @@ let collectionsCache: CollectionsData | null = null;
|
|||||||
*/
|
*/
|
||||||
function getFocusableElements(container: HTMLElement): HTMLElement[] {
|
function getFocusableElements(container: HTMLElement): HTMLElement[] {
|
||||||
const focusableSelectors = [
|
const focusableSelectors = [
|
||||||
'button:not([disabled])',
|
"button:not([disabled])",
|
||||||
'a[href]',
|
"a[href]",
|
||||||
'input:not([disabled])',
|
"input:not([disabled])",
|
||||||
'select:not([disabled])',
|
"select:not([disabled])",
|
||||||
'textarea:not([disabled])',
|
"textarea:not([disabled])",
|
||||||
'[tabindex]:not([tabindex="-1"])'
|
'[tabindex]:not([tabindex="-1"])',
|
||||||
].join(', ');
|
].join(", ");
|
||||||
|
|
||||||
return Array.from(container.querySelectorAll<HTMLElement>(focusableSelectors))
|
return Array.from(
|
||||||
.filter(el => el.offsetParent !== null); // Filter out hidden elements
|
container.querySelectorAll<HTMLElement>(focusableSelectors)
|
||||||
|
).filter((el) => el.offsetParent !== null); // Filter out hidden elements
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle keyboard navigation within modal (focus trap)
|
* Handle keyboard navigation within modal (focus trap)
|
||||||
*/
|
*/
|
||||||
function handleModalKeydown(e: KeyboardEvent, modal: HTMLElement): void {
|
function handleModalKeydown(e: KeyboardEvent, modal: HTMLElement): void {
|
||||||
if (e.key === 'Tab') {
|
if (e.key === "Tab") {
|
||||||
const focusableElements = getFocusableElements(modal);
|
const focusableElements = getFocusableElements(modal);
|
||||||
if (focusableElements.length === 0) return;
|
if (focusableElements.length === 0) return;
|
||||||
|
|
||||||
@@ -80,23 +92,23 @@ function handleModalKeydown(e: KeyboardEvent, modal: HTMLElement): void {
|
|||||||
* Setup modal functionality
|
* Setup modal functionality
|
||||||
*/
|
*/
|
||||||
export function setupModal(): void {
|
export function setupModal(): void {
|
||||||
const modal = document.getElementById('file-modal');
|
const modal = document.getElementById("file-modal");
|
||||||
const closeBtn = document.getElementById('close-modal');
|
const closeBtn = document.getElementById("close-modal");
|
||||||
const copyBtn = document.getElementById('copy-btn');
|
const copyBtn = document.getElementById("copy-btn");
|
||||||
const downloadBtn = document.getElementById('download-btn');
|
const downloadBtn = document.getElementById("download-btn");
|
||||||
const shareBtn = document.getElementById('share-btn');
|
const shareBtn = document.getElementById("share-btn");
|
||||||
|
|
||||||
if (!modal) return;
|
if (!modal) return;
|
||||||
|
|
||||||
closeBtn?.addEventListener('click', closeModal);
|
closeBtn?.addEventListener("click", closeModal);
|
||||||
|
|
||||||
modal.addEventListener('click', (e) => {
|
modal.addEventListener("click", (e) => {
|
||||||
if (e.target === modal) closeModal();
|
if (e.target === modal) closeModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (!modal.classList.contains('hidden')) {
|
if (!modal.classList.contains("hidden")) {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === "Escape") {
|
||||||
closeModal();
|
closeModal();
|
||||||
} else {
|
} else {
|
||||||
handleModalKeydown(e, modal);
|
handleModalKeydown(e, modal);
|
||||||
@@ -104,32 +116,41 @@ export function setupModal(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
copyBtn?.addEventListener('click', async () => {
|
copyBtn?.addEventListener("click", async () => {
|
||||||
if (currentFileContent) {
|
if (currentFileContent) {
|
||||||
const success = await copyToClipboard(currentFileContent);
|
const success = await copyToClipboard(currentFileContent);
|
||||||
showToast(success ? 'Copied to clipboard!' : 'Failed to copy', success ? 'success' : 'error');
|
showToast(
|
||||||
|
success ? "Copied to clipboard!" : "Failed to copy",
|
||||||
|
success ? "success" : "error"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
downloadBtn?.addEventListener('click', async () => {
|
downloadBtn?.addEventListener("click", async () => {
|
||||||
if (currentFilePath) {
|
if (currentFilePath) {
|
||||||
const success = await downloadFile(currentFilePath);
|
const success = await downloadFile(currentFilePath);
|
||||||
showToast(success ? 'Download started!' : 'Download failed', success ? 'success' : 'error');
|
showToast(
|
||||||
|
success ? "Download started!" : "Download failed",
|
||||||
|
success ? "success" : "error"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
shareBtn?.addEventListener('click', async () => {
|
shareBtn?.addEventListener("click", async () => {
|
||||||
if (currentFilePath) {
|
if (currentFilePath) {
|
||||||
const success = await shareFile(currentFilePath);
|
const success = await shareFile(currentFilePath);
|
||||||
showToast(success ? 'Link copied to clipboard!' : 'Failed to copy link', success ? 'success' : 'error');
|
showToast(
|
||||||
|
success ? "Link copied to clipboard!" : "Failed to copy link",
|
||||||
|
success ? "success" : "error"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup install dropdown toggle
|
// Setup install dropdown toggle
|
||||||
setupInstallDropdown('install-dropdown');
|
setupInstallDropdown("install-dropdown");
|
||||||
|
|
||||||
// Handle browser back/forward navigation
|
// Handle browser back/forward navigation
|
||||||
window.addEventListener('hashchange', handleHashChange);
|
window.addEventListener("hashchange", handleHashChange);
|
||||||
|
|
||||||
// Check for deep link on initial load
|
// Check for deep link on initial load
|
||||||
handleHashChange();
|
handleHashChange();
|
||||||
@@ -141,13 +162,13 @@ export function setupModal(): void {
|
|||||||
function handleHashChange(): void {
|
function handleHashChange(): void {
|
||||||
const hash = window.location.hash;
|
const hash = window.location.hash;
|
||||||
|
|
||||||
if (hash && hash.startsWith('#file=')) {
|
if (hash && hash.startsWith("#file=")) {
|
||||||
const filePath = decodeURIComponent(hash.slice(6));
|
const filePath = decodeURIComponent(hash.slice(6));
|
||||||
if (filePath && filePath !== currentFilePath) {
|
if (filePath && filePath !== currentFilePath) {
|
||||||
const type = getResourceType(filePath);
|
const type = getResourceType(filePath);
|
||||||
openFileModal(filePath, type, false); // Don't update hash since we're responding to it
|
openFileModal(filePath, type, false); // Don't update hash since we're responding to it
|
||||||
}
|
}
|
||||||
} else if (!hash || hash === '#') {
|
} else if (!hash || hash === "#") {
|
||||||
// No hash or empty hash - close modal if open
|
// No hash or empty hash - close modal if open
|
||||||
if (currentFilePath) {
|
if (currentFilePath) {
|
||||||
closeModal(false); // Don't update hash since we're responding to it
|
closeModal(false); // Don't update hash since we're responding to it
|
||||||
@@ -162,11 +183,15 @@ function updateHash(filePath: string | null): void {
|
|||||||
if (filePath) {
|
if (filePath) {
|
||||||
const newHash = `#file=${encodeURIComponent(filePath)}`;
|
const newHash = `#file=${encodeURIComponent(filePath)}`;
|
||||||
if (window.location.hash !== newHash) {
|
if (window.location.hash !== newHash) {
|
||||||
history.pushState(null, '', newHash);
|
history.pushState(null, "", newHash);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (window.location.hash) {
|
if (window.location.hash) {
|
||||||
history.pushState(null, '', window.location.pathname + window.location.search);
|
history.pushState(
|
||||||
|
null,
|
||||||
|
"",
|
||||||
|
window.location.pathname + window.location.search
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,15 +203,18 @@ export function setupInstallDropdown(containerId: string): void {
|
|||||||
const container = document.getElementById(containerId);
|
const container = document.getElementById(containerId);
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const toggle = container.querySelector<HTMLButtonElement>('.install-btn-toggle');
|
const toggle = container.querySelector<HTMLButtonElement>(
|
||||||
const menu = container.querySelector('.install-dropdown-menu');
|
".install-btn-toggle"
|
||||||
const menuItems = container.querySelectorAll<HTMLAnchorElement>('.install-dropdown-menu a');
|
);
|
||||||
|
const menuItems = container.querySelectorAll<HTMLAnchorElement>(
|
||||||
|
".install-dropdown-menu a"
|
||||||
|
);
|
||||||
|
|
||||||
toggle?.addEventListener('click', (e) => {
|
toggle?.addEventListener("click", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const isOpen = container.classList.toggle('open');
|
const isOpen = container.classList.toggle("open");
|
||||||
toggle.setAttribute('aria-expanded', String(isOpen));
|
toggle.setAttribute("aria-expanded", String(isOpen));
|
||||||
|
|
||||||
// Focus first menu item when opening
|
// Focus first menu item when opening
|
||||||
if (isOpen && menuItems.length > 0) {
|
if (isOpen && menuItems.length > 0) {
|
||||||
@@ -195,11 +223,11 @@ export function setupInstallDropdown(containerId: string): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Keyboard navigation for dropdown
|
// Keyboard navigation for dropdown
|
||||||
toggle?.addEventListener('keydown', (e) => {
|
toggle?.addEventListener("keydown", (e) => {
|
||||||
if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
|
if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
container.classList.add('open');
|
container.classList.add("open");
|
||||||
toggle.setAttribute('aria-expanded', 'true');
|
toggle.setAttribute("aria-expanded", "true");
|
||||||
if (menuItems.length > 0) {
|
if (menuItems.length > 0) {
|
||||||
menuItems[0].focus();
|
menuItems[0].focus();
|
||||||
}
|
}
|
||||||
@@ -208,15 +236,15 @@ export function setupInstallDropdown(containerId: string): void {
|
|||||||
|
|
||||||
// Keyboard navigation within menu
|
// Keyboard navigation within menu
|
||||||
menuItems.forEach((item, index) => {
|
menuItems.forEach((item, index) => {
|
||||||
item.addEventListener('keydown', (e) => {
|
item.addEventListener("keydown", (e) => {
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case 'ArrowDown':
|
case "ArrowDown":
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (index < menuItems.length - 1) {
|
if (index < menuItems.length - 1) {
|
||||||
menuItems[index + 1].focus();
|
menuItems[index + 1].focus();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'ArrowUp':
|
case "ArrowUp":
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
menuItems[index - 1].focus();
|
menuItems[index - 1].focus();
|
||||||
@@ -224,34 +252,34 @@ export function setupInstallDropdown(containerId: string): void {
|
|||||||
toggle?.focus();
|
toggle?.focus();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'Escape':
|
case "Escape":
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
container.classList.remove('open');
|
container.classList.remove("open");
|
||||||
toggle?.setAttribute('aria-expanded', 'false');
|
toggle?.setAttribute("aria-expanded", "false");
|
||||||
toggle?.focus();
|
toggle?.focus();
|
||||||
break;
|
break;
|
||||||
case 'Tab':
|
case "Tab":
|
||||||
// Close menu on tab out
|
// Close menu on tab out
|
||||||
container.classList.remove('open');
|
container.classList.remove("open");
|
||||||
toggle?.setAttribute('aria-expanded', 'false');
|
toggle?.setAttribute("aria-expanded", "false");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
// Close dropdown when clicking outside
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener("click", (e) => {
|
||||||
if (!container.contains(e.target as Node)) {
|
if (!container.contains(e.target as Node)) {
|
||||||
container.classList.remove('open');
|
container.classList.remove("open");
|
||||||
toggle?.setAttribute('aria-expanded', 'false');
|
toggle?.setAttribute("aria-expanded", "false");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close dropdown when clicking a menu item
|
// Close dropdown when clicking a menu item
|
||||||
container.querySelectorAll('.install-dropdown-menu a').forEach(link => {
|
container.querySelectorAll(".install-dropdown-menu a").forEach((link) => {
|
||||||
link.addEventListener('click', () => {
|
link.addEventListener("click", () => {
|
||||||
container.classList.remove('open');
|
container.classList.remove("open");
|
||||||
toggle?.setAttribute('aria-expanded', 'false');
|
toggle?.setAttribute("aria-expanded", "false");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -263,18 +291,29 @@ export function setupInstallDropdown(containerId: string): void {
|
|||||||
* @param updateUrl - Whether to update the URL hash (default: true)
|
* @param updateUrl - Whether to update the URL hash (default: true)
|
||||||
* @param trigger - The element that triggered the modal (for focus return)
|
* @param trigger - The element that triggered the modal (for focus return)
|
||||||
*/
|
*/
|
||||||
export async function openFileModal(filePath: string, type: string, updateUrl = true, trigger?: HTMLElement): Promise<void> {
|
export async function openFileModal(
|
||||||
const modal = document.getElementById('file-modal');
|
filePath: string,
|
||||||
const title = document.getElementById('modal-title');
|
type: string,
|
||||||
const modalContent = document.getElementById('modal-content');
|
updateUrl = true,
|
||||||
const contentEl = modalContent?.querySelector('code');
|
trigger?: HTMLElement
|
||||||
const installDropdown = document.getElementById('install-dropdown');
|
): Promise<void> {
|
||||||
const installBtnMain = document.getElementById('install-btn-main') as HTMLAnchorElement | null;
|
const modal = document.getElementById("file-modal");
|
||||||
const installVscode = document.getElementById('install-vscode') as HTMLAnchorElement | null;
|
const title = document.getElementById("modal-title");
|
||||||
const installInsiders = document.getElementById('install-insiders') as HTMLAnchorElement | null;
|
const modalContent = document.getElementById("modal-content");
|
||||||
const copyBtn = document.getElementById('copy-btn');
|
const contentEl = modalContent?.querySelector("code");
|
||||||
const downloadBtn = document.getElementById('download-btn');
|
const installDropdown = document.getElementById("install-dropdown");
|
||||||
const closeBtn = document.getElementById('close-modal');
|
const installBtnMain = document.getElementById(
|
||||||
|
"install-btn-main"
|
||||||
|
) as HTMLAnchorElement | null;
|
||||||
|
const installVscode = document.getElementById(
|
||||||
|
"install-vscode"
|
||||||
|
) as HTMLAnchorElement | null;
|
||||||
|
const installInsiders = document.getElementById(
|
||||||
|
"install-insiders"
|
||||||
|
) as HTMLAnchorElement | null;
|
||||||
|
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 || !modalContent) return;
|
||||||
|
|
||||||
@@ -282,7 +321,7 @@ export async function openFileModal(filePath: string, type: string, updateUrl =
|
|||||||
currentFileType = type;
|
currentFileType = type;
|
||||||
|
|
||||||
// Track trigger element for focus return
|
// Track trigger element for focus return
|
||||||
triggerElement = trigger || document.activeElement as HTMLElement;
|
triggerElement = trigger || (document.activeElement as HTMLElement);
|
||||||
|
|
||||||
// Update URL for deep linking
|
// Update URL for deep linking
|
||||||
if (updateUrl) {
|
if (updateUrl) {
|
||||||
@@ -290,8 +329,8 @@ export async function openFileModal(filePath: string, type: string, updateUrl =
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show modal with loading state
|
// Show modal with loading state
|
||||||
title.textContent = filePath.split('/').pop() || filePath;
|
title.textContent = filePath.split("/").pop() || filePath;
|
||||||
modal.classList.remove('hidden');
|
modal.classList.remove("hidden");
|
||||||
|
|
||||||
// Set focus to close button for accessibility
|
// Set focus to close button for accessibility
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -299,38 +338,45 @@ export async function openFileModal(filePath: string, type: string, updateUrl =
|
|||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
// Handle collections differently - show as item list
|
// Handle collections differently - show as item list
|
||||||
if (type === 'collection') {
|
if (type === "collection") {
|
||||||
await openCollectionModal(filePath, title, modalContent, installDropdown, copyBtn, downloadBtn);
|
await openCollectionModal(
|
||||||
|
filePath,
|
||||||
|
title,
|
||||||
|
modalContent,
|
||||||
|
installDropdown,
|
||||||
|
copyBtn,
|
||||||
|
downloadBtn
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regular file modal
|
// Regular file modal
|
||||||
if (contentEl) {
|
if (contentEl) {
|
||||||
contentEl.textContent = 'Loading...';
|
contentEl.textContent = "Loading...";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show copy/download buttons for regular files
|
// Show copy/download buttons for regular files
|
||||||
if (copyBtn) copyBtn.style.display = 'inline-flex';
|
if (copyBtn) copyBtn.style.display = "inline-flex";
|
||||||
if (downloadBtn) downloadBtn.style.display = 'inline-flex';
|
if (downloadBtn) downloadBtn.style.display = "inline-flex";
|
||||||
|
|
||||||
// Restore pre/code structure if it was replaced by collection view
|
// Restore pre/code structure if it was replaced by collection view
|
||||||
if (!modalContent.querySelector('pre')) {
|
if (!modalContent.querySelector("pre")) {
|
||||||
modalContent.innerHTML = '<pre id="modal-content"><code></code></pre>';
|
modalContent.innerHTML = '<pre id="modal-content"><code></code></pre>';
|
||||||
}
|
}
|
||||||
const codeEl = modalContent.querySelector('code');
|
const codeEl = modalContent.querySelector("code");
|
||||||
|
|
||||||
// Setup install dropdown
|
// Setup install dropdown
|
||||||
const vscodeUrl = getVSCodeInstallUrl(type, filePath, false);
|
const vscodeUrl = getVSCodeInstallUrl(type, filePath, false);
|
||||||
const insidersUrl = getVSCodeInstallUrl(type, filePath, true);
|
const insidersUrl = getVSCodeInstallUrl(type, filePath, true);
|
||||||
|
|
||||||
if (vscodeUrl && installDropdown) {
|
if (vscodeUrl && installDropdown) {
|
||||||
installDropdown.style.display = 'inline-flex';
|
installDropdown.style.display = "inline-flex";
|
||||||
installDropdown.classList.remove('open');
|
installDropdown.classList.remove("open");
|
||||||
if (installBtnMain) installBtnMain.href = vscodeUrl;
|
if (installBtnMain) installBtnMain.href = vscodeUrl;
|
||||||
if (installVscode) installVscode.href = vscodeUrl;
|
if (installVscode) installVscode.href = vscodeUrl;
|
||||||
if (installInsiders) installInsiders.href = insidersUrl || '#';
|
if (installInsiders) installInsiders.href = insidersUrl || "#";
|
||||||
} else if (installDropdown) {
|
} else if (installDropdown) {
|
||||||
installDropdown.style.display = 'none';
|
installDropdown.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch and display content
|
// Fetch and display content
|
||||||
@@ -340,7 +386,8 @@ export async function openFileModal(filePath: string, type: string, updateUrl =
|
|||||||
if (fileContent && codeEl) {
|
if (fileContent && codeEl) {
|
||||||
codeEl.textContent = fileContent;
|
codeEl.textContent = fileContent;
|
||||||
} else if (codeEl) {
|
} else if (codeEl) {
|
||||||
codeEl.textContent = 'Failed to load file content. Click the button below to view on GitHub.';
|
codeEl.textContent =
|
||||||
|
"Failed to load file content. Click the button below to view on GitHub.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,27 +403,30 @@ async function openCollectionModal(
|
|||||||
downloadBtn: HTMLElement | null
|
downloadBtn: HTMLElement | null
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Hide install dropdown and copy/download for collections
|
// Hide install dropdown and copy/download for collections
|
||||||
if (installDropdown) installDropdown.style.display = 'none';
|
if (installDropdown) installDropdown.style.display = "none";
|
||||||
if (copyBtn) copyBtn.style.display = 'none';
|
if (copyBtn) copyBtn.style.display = "none";
|
||||||
if (downloadBtn) downloadBtn.style.display = 'none';
|
if (downloadBtn) downloadBtn.style.display = "none";
|
||||||
|
|
||||||
// Show loading
|
// Show loading
|
||||||
modalContent.innerHTML = '<div class="collection-loading">Loading collection...</div>';
|
modalContent.innerHTML =
|
||||||
|
'<div class="collection-loading">Loading collection...</div>';
|
||||||
|
|
||||||
// Load collections data if not cached
|
// Load collections data if not cached
|
||||||
if (!collectionsCache) {
|
if (!collectionsCache) {
|
||||||
collectionsCache = await fetchData<CollectionsData>('collections.json');
|
collectionsCache = await fetchData<CollectionsData>("collections.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!collectionsCache) {
|
if (!collectionsCache) {
|
||||||
modalContent.innerHTML = '<div class="collection-error">Failed to load collection data.</div>';
|
modalContent.innerHTML =
|
||||||
|
'<div class="collection-error">Failed to load collection data.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the collection
|
// Find the collection
|
||||||
const collection = collectionsCache.items.find(c => c.path === filePath);
|
const collection = collectionsCache.items.find((c) => c.path === filePath);
|
||||||
if (!collection) {
|
if (!collection) {
|
||||||
modalContent.innerHTML = '<div class="collection-error">Collection not found.</div>';
|
modalContent.innerHTML =
|
||||||
|
'<div class="collection-error">Collection not found.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,33 +436,57 @@ async function openCollectionModal(
|
|||||||
// Render collection view
|
// Render collection view
|
||||||
modalContent.innerHTML = `
|
modalContent.innerHTML = `
|
||||||
<div class="collection-view">
|
<div class="collection-view">
|
||||||
<div class="collection-description">${escapeHtml(collection.description || '')}</div>
|
<div class="collection-description">${escapeHtml(
|
||||||
${collection.tags && collection.tags.length > 0 ? `
|
collection.description || ""
|
||||||
|
)}</div>
|
||||||
|
${
|
||||||
|
collection.tags && collection.tags.length > 0
|
||||||
|
? `
|
||||||
<div class="collection-tags">
|
<div class="collection-tags">
|
||||||
${collection.tags.map(t => `<span class="resource-tag">${escapeHtml(t)}</span>`).join('')}
|
${collection.tags
|
||||||
|
.map((t) => `<span class="resource-tag">${escapeHtml(t)}</span>`)
|
||||||
|
.join("")}
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
<div class="collection-items-header">
|
<div class="collection-items-header">
|
||||||
<strong>${collection.items.length} items in this collection</strong>
|
<strong>${collection.items.length} items in this collection</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="collection-items-list">
|
<div class="collection-items-list">
|
||||||
${collection.items.map(item => `
|
${collection.items
|
||||||
<div class="collection-item" data-path="${escapeHtml(item.path)}" data-type="${escapeHtml(item.kind)}">
|
.map(
|
||||||
<span class="collection-item-icon">${getResourceIcon(item.kind)}</span>
|
(item) => `
|
||||||
|
<div class="collection-item" data-path="${escapeHtml(
|
||||||
|
item.path
|
||||||
|
)}" data-type="${escapeHtml(item.kind)}">
|
||||||
|
<span class="collection-item-icon">${getResourceIcon(
|
||||||
|
item.kind
|
||||||
|
)}</span>
|
||||||
<div class="collection-item-info">
|
<div class="collection-item-info">
|
||||||
<div class="collection-item-name">${escapeHtml(item.path.split('/').pop() || item.path)}</div>
|
<div class="collection-item-name">${escapeHtml(
|
||||||
${item.usage ? `<div class="collection-item-usage">${escapeHtml(item.usage)}</div>` : ''}
|
item.path.split("/").pop() || item.path
|
||||||
|
)}</div>
|
||||||
|
${
|
||||||
|
item.usage
|
||||||
|
? `<div class="collection-item-usage">${escapeHtml(
|
||||||
|
item.usage
|
||||||
|
)}</div>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<span class="collection-item-type">${escapeHtml(item.kind)}</span>
|
<span class="collection-item-type">${escapeHtml(item.kind)}</span>
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
`
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Add click handlers to collection items
|
// Add click handlers to collection items
|
||||||
modalContent.querySelectorAll('.collection-item').forEach(el => {
|
modalContent.querySelectorAll(".collection-item").forEach((el) => {
|
||||||
el.addEventListener('click', () => {
|
el.addEventListener("click", () => {
|
||||||
const path = (el as HTMLElement).dataset.path;
|
const path = (el as HTMLElement).dataset.path;
|
||||||
const itemType = (el as HTMLElement).dataset.type;
|
const itemType = (el as HTMLElement).dataset.type;
|
||||||
if (path && itemType) {
|
if (path && itemType) {
|
||||||
@@ -427,14 +501,14 @@ async function openCollectionModal(
|
|||||||
* @param updateUrl - Whether to update the URL hash (default: true)
|
* @param updateUrl - Whether to update the URL hash (default: true)
|
||||||
*/
|
*/
|
||||||
export function closeModal(updateUrl = true): void {
|
export function closeModal(updateUrl = true): void {
|
||||||
const modal = document.getElementById('file-modal');
|
const modal = document.getElementById("file-modal");
|
||||||
const installDropdown = document.getElementById('install-dropdown');
|
const installDropdown = document.getElementById("install-dropdown");
|
||||||
|
|
||||||
if (modal) {
|
if (modal) {
|
||||||
modal.classList.add('hidden');
|
modal.classList.add("hidden");
|
||||||
}
|
}
|
||||||
if (installDropdown) {
|
if (installDropdown) {
|
||||||
installDropdown.classList.remove('open');
|
installDropdown.classList.remove("open");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update URL for deep linking
|
// Update URL for deep linking
|
||||||
@@ -443,7 +517,11 @@ export function closeModal(updateUrl = true): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return focus to trigger element
|
// Return focus to trigger element
|
||||||
if (triggerElement && typeof triggerElement.focus === 'function') {
|
if (
|
||||||
|
triggerElement &&
|
||||||
|
triggerElement.isConnected &&
|
||||||
|
typeof triggerElement.focus === "function"
|
||||||
|
) {
|
||||||
triggerElement.focus();
|
triggerElement.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* Samples/Cookbook page functionality
|
* Samples/Cookbook page functionality
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FuzzySearch, type SearchableItem } from '../search';
|
import { FuzzySearch, type SearchableItem } from "../search";
|
||||||
import { fetchData, escapeHtml } from '../utils';
|
import { fetchData, escapeHtml } from "../utils";
|
||||||
import { createChoices, getChoicesValues, type Choices } from '../choices';
|
import { createChoices, getChoicesValues, type Choices } from "../choices";
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
interface Language {
|
interface Language {
|
||||||
@@ -59,36 +59,44 @@ let tagChoices: Choices | null = null;
|
|||||||
* Initialize the samples page
|
* Initialize the samples page
|
||||||
*/
|
*/
|
||||||
export async function initSamplesPage(): Promise<void> {
|
export async function initSamplesPage(): Promise<void> {
|
||||||
// Load samples data
|
try {
|
||||||
samplesData = await fetchData<SamplesData>('samples.json');
|
// Load samples data
|
||||||
|
samplesData = await fetchData<SamplesData>("samples.json");
|
||||||
|
|
||||||
if (!samplesData || samplesData.cookbooks.length === 0) {
|
if (!samplesData || samplesData.cookbooks.length === 0) {
|
||||||
|
showEmptyState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize search with all recipes
|
||||||
|
const allRecipes = samplesData.cookbooks.flatMap((cookbook) =>
|
||||||
|
cookbook.recipes.map(
|
||||||
|
(recipe) =>
|
||||||
|
({
|
||||||
|
...recipe,
|
||||||
|
title: recipe.name,
|
||||||
|
cookbookId: cookbook.id,
|
||||||
|
} as SearchableItem & { cookbookId: string })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
search = new FuzzySearch(allRecipes);
|
||||||
|
|
||||||
|
// Setup UI
|
||||||
|
setupFilters();
|
||||||
|
setupSearch();
|
||||||
|
renderCookbooks();
|
||||||
|
updateResultsCount();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to initialize samples page:", error);
|
||||||
showEmptyState();
|
showEmptyState();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize search with all recipes
|
|
||||||
const allRecipes = samplesData.cookbooks.flatMap(cookbook =>
|
|
||||||
cookbook.recipes.map(recipe => ({
|
|
||||||
...recipe,
|
|
||||||
title: recipe.name,
|
|
||||||
cookbookId: cookbook.id
|
|
||||||
} as SearchableItem & { cookbookId: string }))
|
|
||||||
);
|
|
||||||
search = new FuzzySearch(allRecipes);
|
|
||||||
|
|
||||||
// Setup UI
|
|
||||||
setupFilters();
|
|
||||||
setupSearch();
|
|
||||||
renderCookbooks();
|
|
||||||
updateResultsCount();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show empty state when no cookbooks are available
|
* Show empty state when no cookbooks are available
|
||||||
*/
|
*/
|
||||||
function showEmptyState(): void {
|
function showEmptyState(): void {
|
||||||
const container = document.getElementById('samples-list');
|
const container = document.getElementById("samples-list");
|
||||||
if (container) {
|
if (container) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
@@ -99,8 +107,8 @@ function showEmptyState(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hide filters
|
// Hide filters
|
||||||
const filtersBar = document.getElementById('filters-bar');
|
const filtersBar = document.getElementById("filters-bar");
|
||||||
if (filtersBar) filtersBar.style.display = 'none';
|
if (filtersBar) filtersBar.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -110,12 +118,14 @@ function setupFilters(): void {
|
|||||||
if (!samplesData) return;
|
if (!samplesData) return;
|
||||||
|
|
||||||
// Language filter
|
// Language filter
|
||||||
const languageSelect = document.getElementById('filter-language') as HTMLSelectElement;
|
const languageSelect = document.getElementById(
|
||||||
|
"filter-language"
|
||||||
|
) as HTMLSelectElement;
|
||||||
if (languageSelect) {
|
if (languageSelect) {
|
||||||
// Get unique languages across all cookbooks
|
// Get unique languages across all cookbooks
|
||||||
const languages = new Map<string, Language>();
|
const languages = new Map<string, Language>();
|
||||||
samplesData.cookbooks.forEach(cookbook => {
|
samplesData.cookbooks.forEach((cookbook) => {
|
||||||
cookbook.languages.forEach(lang => {
|
cookbook.languages.forEach((lang) => {
|
||||||
if (!languages.has(lang.id)) {
|
if (!languages.has(lang.id)) {
|
||||||
languages.set(lang.id, lang);
|
languages.set(lang.id, lang);
|
||||||
}
|
}
|
||||||
@@ -124,13 +134,13 @@ function setupFilters(): void {
|
|||||||
|
|
||||||
languageSelect.innerHTML = '<option value="">All Languages</option>';
|
languageSelect.innerHTML = '<option value="">All Languages</option>';
|
||||||
languages.forEach((lang, id) => {
|
languages.forEach((lang, id) => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement("option");
|
||||||
option.value = id;
|
option.value = id;
|
||||||
option.textContent = `${lang.icon} ${lang.name}`;
|
option.textContent = `${lang.icon} ${lang.name}`;
|
||||||
languageSelect.appendChild(option);
|
languageSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
languageSelect.addEventListener('change', () => {
|
languageSelect.addEventListener("change", () => {
|
||||||
selectedLanguage = languageSelect.value || null;
|
selectedLanguage = languageSelect.value || null;
|
||||||
renderCookbooks();
|
renderCookbooks();
|
||||||
updateResultsCount();
|
updateResultsCount();
|
||||||
@@ -138,18 +148,18 @@ function setupFilters(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tag filter (multi-select with Choices.js)
|
// Tag filter (multi-select with Choices.js)
|
||||||
const tagSelect = document.getElementById('filter-tag') as HTMLSelectElement;
|
const tagSelect = document.getElementById("filter-tag") as HTMLSelectElement;
|
||||||
if (tagSelect && samplesData.filters.tags.length > 0) {
|
if (tagSelect && samplesData.filters.tags.length > 0) {
|
||||||
// Initialize Choices.js
|
// Initialize Choices.js
|
||||||
tagChoices = createChoices('#filter-tag', { placeholderValue: 'All Tags' });
|
tagChoices = createChoices("#filter-tag", { placeholderValue: "All Tags" });
|
||||||
tagChoices.setChoices(
|
tagChoices.setChoices(
|
||||||
samplesData.filters.tags.map(tag => ({ value: tag, label: tag })),
|
samplesData.filters.tags.map((tag) => ({ value: tag, label: tag })),
|
||||||
'value',
|
"value",
|
||||||
'label',
|
"label",
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
tagSelect.addEventListener('change', () => {
|
tagSelect.addEventListener("change", () => {
|
||||||
selectedTags = getChoicesValues(tagChoices!);
|
selectedTags = getChoicesValues(tagChoices!);
|
||||||
renderCookbooks();
|
renderCookbooks();
|
||||||
updateResultsCount();
|
updateResultsCount();
|
||||||
@@ -157,19 +167,21 @@ function setupFilters(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear filters button
|
// Clear filters button
|
||||||
const clearBtn = document.getElementById('clear-filters');
|
const clearBtn = document.getElementById("clear-filters");
|
||||||
clearBtn?.addEventListener('click', clearFilters);
|
clearBtn?.addEventListener("click", clearFilters);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup search functionality
|
* Setup search functionality
|
||||||
*/
|
*/
|
||||||
function setupSearch(): void {
|
function setupSearch(): void {
|
||||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
const searchInput = document.getElementById(
|
||||||
|
"search-input"
|
||||||
|
) as HTMLInputElement;
|
||||||
if (!searchInput) return;
|
if (!searchInput) return;
|
||||||
|
|
||||||
let debounceTimer: number;
|
let debounceTimer: number;
|
||||||
searchInput.addEventListener('input', () => {
|
searchInput.addEventListener("input", () => {
|
||||||
clearTimeout(debounceTimer);
|
clearTimeout(debounceTimer);
|
||||||
debounceTimer = window.setTimeout(() => {
|
debounceTimer = window.setTimeout(() => {
|
||||||
renderCookbooks();
|
renderCookbooks();
|
||||||
@@ -185,16 +197,20 @@ function clearFilters(): void {
|
|||||||
selectedLanguage = null;
|
selectedLanguage = null;
|
||||||
selectedTags = [];
|
selectedTags = [];
|
||||||
|
|
||||||
const languageSelect = document.getElementById('filter-language') as HTMLSelectElement;
|
const languageSelect = document.getElementById(
|
||||||
if (languageSelect) languageSelect.value = '';
|
"filter-language"
|
||||||
|
) as HTMLSelectElement;
|
||||||
|
if (languageSelect) languageSelect.value = "";
|
||||||
|
|
||||||
// Clear Choices.js selection
|
// Clear Choices.js selection
|
||||||
if (tagChoices) {
|
if (tagChoices) {
|
||||||
tagChoices.removeActiveItems();
|
tagChoices.removeActiveItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
const searchInput = document.getElementById(
|
||||||
if (searchInput) searchInput.value = '';
|
"search-input"
|
||||||
|
) as HTMLInputElement;
|
||||||
|
if (searchInput) searchInput.value = "";
|
||||||
|
|
||||||
renderCookbooks();
|
renderCookbooks();
|
||||||
updateResultsCount();
|
updateResultsCount();
|
||||||
@@ -203,44 +219,53 @@ function clearFilters(): void {
|
|||||||
/**
|
/**
|
||||||
* Get filtered recipes
|
* Get filtered recipes
|
||||||
*/
|
*/
|
||||||
function getFilteredRecipes(): { cookbook: Cookbook; recipe: Recipe; highlighted?: string }[] {
|
function getFilteredRecipes(): {
|
||||||
|
cookbook: Cookbook;
|
||||||
|
recipe: Recipe;
|
||||||
|
highlighted?: string;
|
||||||
|
}[] {
|
||||||
if (!samplesData || !search) return [];
|
if (!samplesData || !search) return [];
|
||||||
|
|
||||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
const searchInput = document.getElementById(
|
||||||
const query = searchInput?.value.trim() || '';
|
"search-input"
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const query = searchInput?.value.trim() || "";
|
||||||
|
|
||||||
let results: { cookbook: Cookbook; recipe: Recipe; highlighted?: string }[] = [];
|
let results: { cookbook: Cookbook; recipe: Recipe; highlighted?: string }[] =
|
||||||
|
[];
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
// Use fuzzy search - returns SearchableItem[] directly
|
// Use fuzzy search - returns SearchableItem[] directly
|
||||||
const searchResults = search.search(query);
|
const searchResults = search.search(query);
|
||||||
results = searchResults.map(item => {
|
results = searchResults.map((item) => {
|
||||||
const recipe = item as SearchableItem & { cookbookId: string };
|
const recipe = item as SearchableItem & { cookbookId: string };
|
||||||
const cookbook = samplesData!.cookbooks.find(c => c.id === recipe.cookbookId)!;
|
const cookbook = samplesData!.cookbooks.find(
|
||||||
|
(c) => c.id === recipe.cookbookId
|
||||||
|
)!;
|
||||||
return {
|
return {
|
||||||
cookbook,
|
cookbook,
|
||||||
recipe: recipe as unknown as Recipe,
|
recipe: recipe as unknown as Recipe,
|
||||||
highlighted: search!.highlight(recipe.title, query)
|
highlighted: search!.highlight(recipe.title, query),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// No search query - return all recipes
|
// No search query - return all recipes
|
||||||
results = samplesData.cookbooks.flatMap(cookbook =>
|
results = samplesData.cookbooks.flatMap((cookbook) =>
|
||||||
cookbook.recipes.map(recipe => ({ cookbook, recipe }))
|
cookbook.recipes.map((recipe) => ({ cookbook, recipe }))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply language filter
|
// Apply language filter
|
||||||
if (selectedLanguage) {
|
if (selectedLanguage) {
|
||||||
results = results.filter(({ recipe }) =>
|
results = results.filter(
|
||||||
recipe.variants[selectedLanguage!]
|
({ recipe }) => recipe.variants[selectedLanguage!]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply tag filter
|
// Apply tag filter
|
||||||
if (selectedTags.length > 0) {
|
if (selectedTags.length > 0) {
|
||||||
results = results.filter(({ recipe }) =>
|
results = results.filter(({ recipe }) =>
|
||||||
selectedTags.some(tag => recipe.tags.includes(tag))
|
selectedTags.some((tag) => recipe.tags.includes(tag))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,7 +276,7 @@ function getFilteredRecipes(): { cookbook: Cookbook; recipe: Recipe; highlighted
|
|||||||
* Render cookbooks and recipes
|
* Render cookbooks and recipes
|
||||||
*/
|
*/
|
||||||
function renderCookbooks(): void {
|
function renderCookbooks(): void {
|
||||||
const container = document.getElementById('samples-list');
|
const container = document.getElementById("samples-list");
|
||||||
if (!container || !samplesData) return;
|
if (!container || !samplesData) return;
|
||||||
|
|
||||||
const filteredResults = getFilteredRecipes();
|
const filteredResults = getFilteredRecipes();
|
||||||
@@ -267,7 +292,10 @@ function renderCookbooks(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Group by cookbook
|
// Group by cookbook
|
||||||
const byCookbook = new Map<string, { cookbook: Cookbook; recipes: { recipe: Recipe; highlighted?: string }[] }>();
|
const byCookbook = new Map<
|
||||||
|
string,
|
||||||
|
{ cookbook: Cookbook; recipes: { recipe: Recipe; highlighted?: string }[] }
|
||||||
|
>();
|
||||||
filteredResults.forEach(({ cookbook, recipe, highlighted }) => {
|
filteredResults.forEach(({ cookbook, recipe, highlighted }) => {
|
||||||
if (!byCookbook.has(cookbook.id)) {
|
if (!byCookbook.has(cookbook.id)) {
|
||||||
byCookbook.set(cookbook.id, { cookbook, recipes: [] });
|
byCookbook.set(cookbook.id, { cookbook, recipes: [] });
|
||||||
@@ -275,7 +303,7 @@ function renderCookbooks(): void {
|
|||||||
byCookbook.get(cookbook.id)!.recipes.push({ recipe, highlighted });
|
byCookbook.get(cookbook.id)!.recipes.push({ recipe, highlighted });
|
||||||
});
|
});
|
||||||
|
|
||||||
let html = '';
|
let html = "";
|
||||||
byCookbook.forEach(({ cookbook, recipes }) => {
|
byCookbook.forEach(({ cookbook, recipes }) => {
|
||||||
html += renderCookbookSection(cookbook, recipes);
|
html += renderCookbookSection(cookbook, recipes);
|
||||||
});
|
});
|
||||||
@@ -289,18 +317,27 @@ function renderCookbooks(): void {
|
|||||||
/**
|
/**
|
||||||
* Render a cookbook section
|
* Render a cookbook section
|
||||||
*/
|
*/
|
||||||
function renderCookbookSection(cookbook: Cookbook, recipes: { recipe: Recipe; highlighted?: string }[]): string {
|
function renderCookbookSection(
|
||||||
const languageTabs = cookbook.languages.map(lang => `
|
cookbook: Cookbook,
|
||||||
<button class="lang-tab${selectedLanguage === lang.id ? ' active' : ''}"
|
recipes: { recipe: Recipe; highlighted?: string }[]
|
||||||
|
): string {
|
||||||
|
const languageTabs = cookbook.languages
|
||||||
|
.map(
|
||||||
|
(lang) => `
|
||||||
|
<button class="lang-tab${selectedLanguage === lang.id ? " active" : ""}"
|
||||||
data-lang="${lang.id}"
|
data-lang="${lang.id}"
|
||||||
title="${lang.name}">
|
title="${lang.name}">
|
||||||
${lang.icon}
|
${lang.icon}
|
||||||
</button>
|
</button>
|
||||||
`).join('');
|
`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
const recipeCards = recipes.map(({ recipe, highlighted }) =>
|
const recipeCards = recipes
|
||||||
renderRecipeCard(cookbook, recipe, highlighted)
|
.map(({ recipe, highlighted }) =>
|
||||||
).join('');
|
renderRecipeCard(cookbook, recipe, highlighted)
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="cookbook-section" data-cookbook="${cookbook.id}">
|
<div class="cookbook-section" data-cookbook="${cookbook.id}">
|
||||||
@@ -323,25 +360,36 @@ function renderCookbookSection(cookbook: Cookbook, recipes: { recipe: Recipe; hi
|
|||||||
/**
|
/**
|
||||||
* Render a recipe card
|
* Render a recipe card
|
||||||
*/
|
*/
|
||||||
function renderRecipeCard(cookbook: Cookbook, recipe: Recipe, highlightedName?: string): string {
|
function renderRecipeCard(
|
||||||
|
cookbook: Cookbook,
|
||||||
|
recipe: Recipe,
|
||||||
|
highlightedName?: string
|
||||||
|
): string {
|
||||||
const recipeKey = `${cookbook.id}-${recipe.id}`;
|
const recipeKey = `${cookbook.id}-${recipe.id}`;
|
||||||
const isExpanded = expandedRecipes.has(recipeKey);
|
const isExpanded = expandedRecipes.has(recipeKey);
|
||||||
|
|
||||||
// Determine which language to show
|
// Determine which language to show
|
||||||
const displayLang = selectedLanguage || cookbook.languages[0]?.id || 'nodejs';
|
const displayLang = selectedLanguage || cookbook.languages[0]?.id || "nodejs";
|
||||||
const variant = recipe.variants[displayLang];
|
const variant = recipe.variants[displayLang];
|
||||||
|
|
||||||
const tags = recipe.tags.map(tag =>
|
const tags = recipe.tags
|
||||||
`<span class="recipe-tag">${escapeHtml(tag)}</span>`
|
.map((tag) => `<span class="recipe-tag">${escapeHtml(tag)}</span>`)
|
||||||
).join('');
|
.join("");
|
||||||
|
|
||||||
const langIndicators = cookbook.languages
|
const langIndicators = cookbook.languages
|
||||||
.filter(lang => recipe.variants[lang.id])
|
.filter((lang) => recipe.variants[lang.id])
|
||||||
.map(lang => `<span class="lang-indicator" title="${lang.name}">${lang.icon}</span>`)
|
.map(
|
||||||
.join('');
|
(lang) =>
|
||||||
|
`<span class="lang-indicator" title="${lang.name}">${lang.icon}</span>`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="recipe-card${isExpanded ? ' expanded' : ''}" data-recipe="${recipeKey}" data-cookbook="${cookbook.id}" data-recipe-id="${recipe.id}">
|
<div class="recipe-card${
|
||||||
|
isExpanded ? " expanded" : ""
|
||||||
|
}" data-recipe="${recipeKey}" data-cookbook="${
|
||||||
|
cookbook.id
|
||||||
|
}" data-recipe-id="${recipe.id}">
|
||||||
<div class="recipe-header">
|
<div class="recipe-header">
|
||||||
<h3>${highlightedName || escapeHtml(recipe.name)}</h3>
|
<h3>${highlightedName || escapeHtml(recipe.name)}</h3>
|
||||||
<div class="recipe-langs">${langIndicators}</div>
|
<div class="recipe-langs">${langIndicators}</div>
|
||||||
@@ -349,29 +397,41 @@ function renderRecipeCard(cookbook: Cookbook, recipe: Recipe, highlightedName?:
|
|||||||
<p class="recipe-description">${escapeHtml(recipe.description)}</p>
|
<p class="recipe-description">${escapeHtml(recipe.description)}</p>
|
||||||
<div class="recipe-tags">${tags}</div>
|
<div class="recipe-tags">${tags}</div>
|
||||||
<div class="recipe-actions">
|
<div class="recipe-actions">
|
||||||
${variant ? `
|
${
|
||||||
<button class="btn btn-secondary btn-small view-recipe-btn" data-doc="${variant.doc}">
|
variant
|
||||||
|
? `
|
||||||
|
<button class="btn btn-secondary btn-small view-recipe-btn" data-doc="${
|
||||||
|
variant.doc
|
||||||
|
}">
|
||||||
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">
|
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">
|
||||||
<path d="M1 2.75A.75.75 0 0 1 1.75 2h12.5a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 2.75zm0 5A.75.75 0 0 1 1.75 7h12.5a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 7.75zM1.75 12h12.5a.75.75 0 0 1 0 1.5H1.75a.75.75 0 0 1 0-1.5z"/>
|
<path d="M1 2.75A.75.75 0 0 1 1.75 2h12.5a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 2.75zm0 5A.75.75 0 0 1 1.75 7h12.5a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 7.75zM1.75 12h12.5a.75.75 0 0 1 0 1.5H1.75a.75.75 0 0 1 0-1.5z"/>
|
||||||
</svg>
|
</svg>
|
||||||
View Recipe
|
View Recipe
|
||||||
</button>
|
</button>
|
||||||
${variant.example ? `
|
${
|
||||||
|
variant.example
|
||||||
|
? `
|
||||||
<button class="btn btn-secondary btn-small view-example-btn" data-example="${variant.example}">
|
<button class="btn btn-secondary btn-small view-example-btn" data-example="${variant.example}">
|
||||||
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">
|
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">
|
||||||
<path d="M4.72 3.22a.75.75 0 0 1 1.06 0l3.5 3.5a.75.75 0 0 1 0 1.06l-3.5 3.5a.75.75 0 0 1-1.06-1.06L7.69 7.5 4.72 4.28a.75.75 0 0 1 0-1.06zm6.25 1.06L10.22 5l.75.75-2.25 2.25 2.25 2.25-.75.75-.75-.72L11.97 7.5z"/>
|
<path d="M4.72 3.22a.75.75 0 0 1 1.06 0l3.5 3.5a.75.75 0 0 1 0 1.06l-3.5 3.5a.75.75 0 0 1-1.06-1.06L7.69 7.5 4.72 4.28a.75.75 0 0 1 0-1.06zm6.25 1.06L10.22 5l.75.75-2.25 2.25 2.25 2.25-.75.75-.75-.72L11.97 7.5z"/>
|
||||||
</svg>
|
</svg>
|
||||||
View Example
|
View Example
|
||||||
</button>
|
</button>
|
||||||
` : ''}
|
`
|
||||||
<a href="https://github.com/github/awesome-copilot/blob/main/${variant.doc}"
|
: ""
|
||||||
|
}
|
||||||
|
<a href="https://github.com/github/awesome-copilot/blob/main/${
|
||||||
|
variant.doc
|
||||||
|
}"
|
||||||
class="btn btn-secondary btn-small" target="_blank" rel="noopener">
|
class="btn btn-secondary btn-small" target="_blank" rel="noopener">
|
||||||
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">
|
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">
|
||||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
||||||
</svg>
|
</svg>
|
||||||
GitHub
|
GitHub
|
||||||
</a>
|
</a>
|
||||||
` : '<span class="no-variant">Not available for selected language</span>'}
|
`
|
||||||
|
: '<span class="no-variant">Not available for selected language</span>'
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -382,35 +442,37 @@ function renderRecipeCard(cookbook: Cookbook, recipe: Recipe, highlightedName?:
|
|||||||
*/
|
*/
|
||||||
function setupRecipeListeners(): void {
|
function setupRecipeListeners(): void {
|
||||||
// View recipe buttons
|
// View recipe buttons
|
||||||
document.querySelectorAll('.view-recipe-btn').forEach(btn => {
|
document.querySelectorAll(".view-recipe-btn").forEach((btn) => {
|
||||||
btn.addEventListener('click', async (e) => {
|
btn.addEventListener("click", async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const docPath = (btn as HTMLElement).dataset.doc;
|
const docPath = (btn as HTMLElement).dataset.doc;
|
||||||
if (docPath) {
|
if (docPath) {
|
||||||
await showRecipeContent(docPath, 'recipe');
|
await showRecipeContent(docPath, "recipe");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// View example buttons
|
// View example buttons
|
||||||
document.querySelectorAll('.view-example-btn').forEach(btn => {
|
document.querySelectorAll(".view-example-btn").forEach((btn) => {
|
||||||
btn.addEventListener('click', async (e) => {
|
btn.addEventListener("click", async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const examplePath = (btn as HTMLElement).dataset.example;
|
const examplePath = (btn as HTMLElement).dataset.example;
|
||||||
if (examplePath) {
|
if (examplePath) {
|
||||||
await showRecipeContent(examplePath, 'example');
|
await showRecipeContent(examplePath, "example");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Language tab clicks
|
// Language tab clicks
|
||||||
document.querySelectorAll('.lang-tab').forEach(tab => {
|
document.querySelectorAll(".lang-tab").forEach((tab) => {
|
||||||
tab.addEventListener('click', (e) => {
|
tab.addEventListener("click", (e) => {
|
||||||
const langId = (tab as HTMLElement).dataset.lang;
|
const langId = (tab as HTMLElement).dataset.lang;
|
||||||
if (langId) {
|
if (langId) {
|
||||||
selectedLanguage = langId;
|
selectedLanguage = langId;
|
||||||
// Update language filter select
|
// Update language filter select
|
||||||
const languageSelect = document.getElementById('filter-language') as HTMLSelectElement;
|
const languageSelect = document.getElementById(
|
||||||
|
"filter-language"
|
||||||
|
) as HTMLSelectElement;
|
||||||
if (languageSelect) languageSelect.value = langId;
|
if (languageSelect) languageSelect.value = langId;
|
||||||
renderCookbooks();
|
renderCookbooks();
|
||||||
updateResultsCount();
|
updateResultsCount();
|
||||||
@@ -422,9 +484,12 @@ function setupRecipeListeners(): void {
|
|||||||
/**
|
/**
|
||||||
* Show recipe/example content in modal
|
* Show recipe/example content in modal
|
||||||
*/
|
*/
|
||||||
async function showRecipeContent(filePath: string, type: 'recipe' | 'example'): Promise<void> {
|
async function showRecipeContent(
|
||||||
|
filePath: string,
|
||||||
|
type: "recipe" | "example"
|
||||||
|
): Promise<void> {
|
||||||
// Use existing modal infrastructure
|
// Use existing modal infrastructure
|
||||||
const { openFileModal } = await import('../modal');
|
const { openFileModal } = await import("../modal");
|
||||||
await openFileModal(filePath, type);
|
await openFileModal(filePath, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,23 +497,25 @@ async function showRecipeContent(filePath: string, type: 'recipe' | 'example'):
|
|||||||
* Update results count display
|
* Update results count display
|
||||||
*/
|
*/
|
||||||
function updateResultsCount(): void {
|
function updateResultsCount(): void {
|
||||||
const resultsCount = document.getElementById('results-count');
|
const resultsCount = document.getElementById("results-count");
|
||||||
if (!resultsCount || !samplesData) return;
|
if (!resultsCount || !samplesData) return;
|
||||||
|
|
||||||
const filtered = getFilteredRecipes();
|
const filtered = getFilteredRecipes();
|
||||||
const total = samplesData.totalRecipes;
|
const total = samplesData.totalRecipes;
|
||||||
|
|
||||||
if (filtered.length === total) {
|
if (filtered.length === total) {
|
||||||
resultsCount.textContent = `${total} recipe${total !== 1 ? 's' : ''}`;
|
resultsCount.textContent = `${total} recipe${total !== 1 ? "s" : ""}`;
|
||||||
} else {
|
} else {
|
||||||
resultsCount.textContent = `${filtered.length} of ${total} recipe${total !== 1 ? 's' : ''}`;
|
resultsCount.textContent = `${filtered.length} of ${total} recipe${
|
||||||
|
total !== 1 ? "s" : ""
|
||||||
|
}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-initialize when DOM is ready
|
// Auto-initialize when DOM is ready
|
||||||
if (typeof document !== 'undefined') {
|
if (typeof document !== "undefined") {
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener('DOMContentLoaded', () => initSamplesPage());
|
document.addEventListener("DOMContentLoaded", () => initSamplesPage());
|
||||||
} else {
|
} else {
|
||||||
initSamplesPage();
|
initSamplesPage();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* Skills page functionality
|
* Skills page functionality
|
||||||
*/
|
*/
|
||||||
import { createChoices, getChoicesValues, type Choices } from '../choices';
|
import { createChoices, getChoicesValues, type Choices } from "../choices";
|
||||||
import { FuzzySearch, SearchItem } from '../search';
|
import { FuzzySearch, SearchItem } from "../search";
|
||||||
import { fetchData, debounce, escapeHtml, getGitHubUrl, getRawGitHubUrl } from '../utils';
|
import {
|
||||||
import { setupModal, openFileModal } from '../modal';
|
fetchData,
|
||||||
import JSZip from '../jszip';
|
debounce,
|
||||||
|
escapeHtml,
|
||||||
|
getGitHubUrl,
|
||||||
|
getRawGitHubUrl,
|
||||||
|
showToast,
|
||||||
|
} from "../utils";
|
||||||
|
import { setupModal, openFileModal } from "../modal";
|
||||||
|
import JSZip from "../jszip";
|
||||||
|
|
||||||
interface SkillFile {
|
interface SkillFile {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -29,86 +36,120 @@ interface SkillsData {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceType = 'skill';
|
const resourceType = "skill";
|
||||||
let allItems: Skill[] = [];
|
let allItems: Skill[] = [];
|
||||||
let search = new FuzzySearch<Skill>();
|
let search = new FuzzySearch<Skill>();
|
||||||
let categorySelect: Choices;
|
let categorySelect: Choices;
|
||||||
let currentFilters = {
|
let currentFilters = {
|
||||||
categories: [] as string[],
|
categories: [] as string[],
|
||||||
hasAssets: false
|
hasAssets: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
function applyFiltersAndRender(): void {
|
function applyFiltersAndRender(): void {
|
||||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
const searchInput = document.getElementById(
|
||||||
const countEl = document.getElementById('results-count');
|
"search-input"
|
||||||
const query = searchInput?.value || '';
|
) as HTMLInputElement;
|
||||||
|
const countEl = document.getElementById("results-count");
|
||||||
|
const query = searchInput?.value || "";
|
||||||
|
|
||||||
let results = query ? search.search(query) : [...allItems];
|
let results = query ? search.search(query) : [...allItems];
|
||||||
|
|
||||||
if (currentFilters.categories.length > 0) {
|
if (currentFilters.categories.length > 0) {
|
||||||
results = results.filter(item => currentFilters.categories.includes(item.category));
|
results = results.filter((item) =>
|
||||||
|
currentFilters.categories.includes(item.category)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (currentFilters.hasAssets) {
|
if (currentFilters.hasAssets) {
|
||||||
results = results.filter(item => item.hasAssets);
|
results = results.filter((item) => item.hasAssets);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderItems(results, query);
|
renderItems(results, query);
|
||||||
const activeFilters: string[] = [];
|
const activeFilters: string[] = [];
|
||||||
if (currentFilters.categories.length > 0) activeFilters.push(`${currentFilters.categories.length} categor${currentFilters.categories.length > 1 ? 'ies' : 'y'}`);
|
if (currentFilters.categories.length > 0)
|
||||||
if (currentFilters.hasAssets) activeFilters.push('has assets');
|
activeFilters.push(
|
||||||
|
`${currentFilters.categories.length} categor${
|
||||||
|
currentFilters.categories.length > 1 ? "ies" : "y"
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
if (currentFilters.hasAssets) activeFilters.push("has assets");
|
||||||
let countText = `${results.length} of ${allItems.length} skills`;
|
let countText = `${results.length} of ${allItems.length} skills`;
|
||||||
if (activeFilters.length > 0) {
|
if (activeFilters.length > 0) {
|
||||||
countText += ` (filtered by ${activeFilters.join(', ')})`;
|
countText += ` (filtered by ${activeFilters.join(", ")})`;
|
||||||
}
|
}
|
||||||
if (countEl) countEl.textContent = countText;
|
if (countEl) countEl.textContent = countText;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderItems(items: Skill[], query = ''): void {
|
function renderItems(items: Skill[], query = ""): void {
|
||||||
const list = document.getElementById('resource-list');
|
const list = document.getElementById("resource-list");
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
list.innerHTML = '<div class="empty-state"><h3>No skills found</h3><p>Try a different search term or adjust filters</p></div>';
|
list.innerHTML =
|
||||||
|
'<div class="empty-state"><h3>No skills found</h3><p>Try a different search term or adjust filters</p></div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
list.innerHTML = items.map(item => `
|
list.innerHTML = items
|
||||||
<div class="resource-item" data-path="${escapeHtml(item.skillFile)}" data-skill-id="${escapeHtml(item.id)}">
|
.map(
|
||||||
|
(item) => `
|
||||||
|
<div class="resource-item" data-path="${escapeHtml(
|
||||||
|
item.skillFile
|
||||||
|
)}" data-skill-id="${escapeHtml(item.id)}">
|
||||||
<div class="resource-info">
|
<div class="resource-info">
|
||||||
<div class="resource-title">${query ? search.highlight(item.title, query) : escapeHtml(item.title)}</div>
|
<div class="resource-title">${
|
||||||
<div class="resource-description">${escapeHtml(item.description || 'No description')}</div>
|
query ? search.highlight(item.title, query) : escapeHtml(item.title)
|
||||||
|
}</div>
|
||||||
|
<div class="resource-description">${escapeHtml(
|
||||||
|
item.description || "No description"
|
||||||
|
)}</div>
|
||||||
<div class="resource-meta">
|
<div class="resource-meta">
|
||||||
<span class="resource-tag tag-category">${escapeHtml(item.category)}</span>
|
<span class="resource-tag tag-category">${escapeHtml(
|
||||||
${item.hasAssets ? `<span class="resource-tag tag-assets">${item.assetCount} asset${item.assetCount === 1 ? '' : 's'}</span>` : ''}
|
item.category
|
||||||
<span class="resource-tag">${item.files.length} file${item.files.length === 1 ? '' : 's'}</span>
|
)}</span>
|
||||||
|
${
|
||||||
|
item.hasAssets
|
||||||
|
? `<span class="resource-tag tag-assets">${
|
||||||
|
item.assetCount
|
||||||
|
} asset${item.assetCount === 1 ? "" : "s"}</span>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
<span class="resource-tag">${item.files.length} file${
|
||||||
|
item.files.length === 1 ? "" : "s"
|
||||||
|
}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="resource-actions">
|
<div class="resource-actions">
|
||||||
<button class="btn btn-primary download-skill-btn" data-skill-id="${escapeHtml(item.id)}" title="Download as ZIP">
|
<button class="btn btn-primary download-skill-btn" data-skill-id="${escapeHtml(
|
||||||
|
item.id
|
||||||
|
)}" title="Download as ZIP">
|
||||||
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
||||||
<path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"/>
|
<path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"/>
|
||||||
<path d="M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06l1.97 1.969Z"/>
|
<path d="M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06l1.97 1.969Z"/>
|
||||||
</svg>
|
</svg>
|
||||||
Download
|
Download
|
||||||
</button>
|
</button>
|
||||||
<a href="${getGitHubUrl(item.path)}" class="btn btn-secondary" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a>
|
<a href="${getGitHubUrl(
|
||||||
|
item.path
|
||||||
|
)}" class="btn btn-secondary" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
// Add click handlers for opening modal
|
// Add click handlers for opening modal
|
||||||
list.querySelectorAll('.resource-item').forEach(el => {
|
list.querySelectorAll(".resource-item").forEach((el) => {
|
||||||
el.addEventListener('click', (e) => {
|
el.addEventListener("click", (e) => {
|
||||||
// Don't trigger modal if clicking download button or github link
|
// Don't trigger modal if clicking download button or github link
|
||||||
if ((e.target as HTMLElement).closest('.resource-actions')) return;
|
if ((e.target as HTMLElement).closest(".resource-actions")) return;
|
||||||
const path = (el as HTMLElement).dataset.path;
|
const path = (el as HTMLElement).dataset.path;
|
||||||
if (path) openFileModal(path, resourceType);
|
if (path) openFileModal(path, resourceType);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add download handlers
|
// Add download handlers
|
||||||
list.querySelectorAll('.download-skill-btn').forEach(btn => {
|
list.querySelectorAll(".download-skill-btn").forEach((btn) => {
|
||||||
btn.addEventListener('click', (e) => {
|
btn.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const skillId = (btn as HTMLElement).dataset.skillId;
|
const skillId = (btn as HTMLElement).dataset.skillId;
|
||||||
if (skillId) downloadSkill(skillId, btn as HTMLButtonElement);
|
if (skillId) downloadSkill(skillId, btn as HTMLButtonElement);
|
||||||
@@ -116,16 +157,20 @@ function renderItems(items: Skill[], query = ''): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadSkill(skillId: string, btn: HTMLButtonElement): Promise<void> {
|
async function downloadSkill(
|
||||||
const skill = allItems.find(item => item.id === skillId);
|
skillId: string,
|
||||||
|
btn: HTMLButtonElement
|
||||||
|
): Promise<void> {
|
||||||
|
const skill = allItems.find((item) => item.id === skillId);
|
||||||
if (!skill || !skill.files || skill.files.length === 0) {
|
if (!skill || !skill.files || skill.files.length === 0) {
|
||||||
alert('No files found for this skill');
|
showToast("No files found for this skill.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const originalContent = btn.innerHTML;
|
const originalContent = btn.innerHTML;
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = '<svg class="spinner" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0a8 8 0 1 0 8 8h-1.5A6.5 6.5 0 1 1 8 1.5V0z"/></svg> Preparing...';
|
btn.innerHTML =
|
||||||
|
'<svg class="spinner" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0a8 8 0 1 0 8 8h-1.5A6.5 6.5 0 1 1 8 1.5V0z"/></svg> Preparing...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
@@ -152,11 +197,11 @@ async function downloadSkill(skillId: string, btn: HTMLButtonElement): Promise<v
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addedFiles === 0) throw new Error('Failed to fetch any files');
|
if (addedFiles === 0) throw new Error("Failed to fetch any files");
|
||||||
|
|
||||||
const blob = await zip.generateAsync({ type: 'blob' });
|
const blob = await zip.generateAsync({ type: "blob" });
|
||||||
const downloadUrl = URL.createObjectURL(blob);
|
const downloadUrl = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement("a");
|
||||||
link.href = downloadUrl;
|
link.href = downloadUrl;
|
||||||
link.download = `${skill.id}.zip`;
|
link.download = `${skill.id}.zip`;
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
@@ -164,49 +209,75 @@ async function downloadSkill(skillId: string, btn: HTMLButtonElement): Promise<v
|
|||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(downloadUrl);
|
URL.revokeObjectURL(downloadUrl);
|
||||||
|
|
||||||
btn.innerHTML = '<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0z"/></svg> Downloaded!';
|
btn.innerHTML =
|
||||||
setTimeout(() => { btn.disabled = false; btn.innerHTML = originalContent; }, 2000);
|
'<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0z"/></svg> Downloaded!';
|
||||||
} catch {
|
setTimeout(() => {
|
||||||
btn.innerHTML = '<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.75.75 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.75.75 0 0 1-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 0 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06z"/></svg> Failed';
|
btn.disabled = false;
|
||||||
setTimeout(() => { btn.disabled = false; btn.innerHTML = originalContent; }, 2000);
|
btn.innerHTML = originalContent;
|
||||||
|
}, 2000);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Download failed.";
|
||||||
|
showToast(message, "error");
|
||||||
|
btn.innerHTML =
|
||||||
|
'<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.75.75 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.75.75 0 0 1-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 0 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06z"/></svg> Failed';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = originalContent;
|
||||||
|
}, 2000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initSkillsPage(): Promise<void> {
|
export async function initSkillsPage(): Promise<void> {
|
||||||
const list = document.getElementById('resource-list');
|
const list = document.getElementById("resource-list");
|
||||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
const searchInput = document.getElementById(
|
||||||
const hasAssetsCheckbox = document.getElementById('filter-has-assets') as HTMLInputElement;
|
"search-input"
|
||||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
) as HTMLInputElement;
|
||||||
|
const hasAssetsCheckbox = document.getElementById(
|
||||||
|
"filter-has-assets"
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const clearFiltersBtn = document.getElementById("clear-filters");
|
||||||
|
|
||||||
const data = await fetchData<SkillsData>('skills.json');
|
const data = await fetchData<SkillsData>("skills.json");
|
||||||
if (!data || !data.items) {
|
if (!data || !data.items) {
|
||||||
if (list) list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
|
if (list)
|
||||||
|
list.innerHTML =
|
||||||
|
'<div class="empty-state"><h3>Failed to load data</h3></div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
allItems = data.items;
|
allItems = data.items;
|
||||||
search.setItems(allItems);
|
search.setItems(allItems);
|
||||||
|
|
||||||
categorySelect = createChoices('#filter-category', { placeholderValue: 'All Categories' });
|
categorySelect = createChoices("#filter-category", {
|
||||||
categorySelect.setChoices(data.filters.categories.map(c => ({ value: c, label: c })), 'value', 'label', true);
|
placeholderValue: "All Categories",
|
||||||
document.getElementById('filter-category')?.addEventListener('change', () => {
|
});
|
||||||
|
categorySelect.setChoices(
|
||||||
|
data.filters.categories.map((c) => ({ value: c, label: c })),
|
||||||
|
"value",
|
||||||
|
"label",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
document.getElementById("filter-category")?.addEventListener("change", () => {
|
||||||
currentFilters.categories = getChoicesValues(categorySelect);
|
currentFilters.categories = getChoicesValues(categorySelect);
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
});
|
});
|
||||||
|
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
searchInput?.addEventListener('input', debounce(() => applyFiltersAndRender(), 200));
|
searchInput?.addEventListener(
|
||||||
|
"input",
|
||||||
|
debounce(() => applyFiltersAndRender(), 200)
|
||||||
|
);
|
||||||
|
|
||||||
hasAssetsCheckbox?.addEventListener('change', () => {
|
hasAssetsCheckbox?.addEventListener("change", () => {
|
||||||
currentFilters.hasAssets = hasAssetsCheckbox.checked;
|
currentFilters.hasAssets = hasAssetsCheckbox.checked;
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
});
|
});
|
||||||
|
|
||||||
clearFiltersBtn?.addEventListener('click', () => {
|
clearFiltersBtn?.addEventListener("click", () => {
|
||||||
currentFilters = { categories: [], hasAssets: false };
|
currentFilters = { categories: [], hasAssets: false };
|
||||||
categorySelect.removeActiveItems();
|
categorySelect.removeActiveItems();
|
||||||
if (hasAssetsCheckbox) hasAssetsCheckbox.checked = false;
|
if (hasAssetsCheckbox) hasAssetsCheckbox.checked = false;
|
||||||
if (searchInput) searchInput.value = '';
|
if (searchInput) searchInput.value = "";
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -214,4 +285,4 @@ export async function initSkillsPage(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Auto-initialize when DOM is ready
|
// Auto-initialize when DOM is ready
|
||||||
document.addEventListener('DOMContentLoaded', initSkillsPage);
|
document.addEventListener("DOMContentLoaded", initSkillsPage);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Tools page functionality
|
* Tools page functionality
|
||||||
*/
|
*/
|
||||||
import { FuzzySearch, type SearchableItem } from '../search';
|
import { FuzzySearch, type SearchableItem } from "../search";
|
||||||
import { fetchData, debounce, escapeHtml } from '../utils';
|
import { fetchData, debounce, escapeHtml } from "../utils";
|
||||||
|
|
||||||
export interface Tool extends SearchableItem {
|
export interface Tool extends SearchableItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -16,8 +16,8 @@ export interface Tool extends SearchableItem {
|
|||||||
links: {
|
links: {
|
||||||
blog?: string;
|
blog?: string;
|
||||||
vscode?: string;
|
vscode?: string;
|
||||||
'vscode-insiders'?: string;
|
"vscode-insiders"?: string;
|
||||||
'visual-studio'?: string;
|
"visual-studio"?: string;
|
||||||
github?: string;
|
github?: string;
|
||||||
documentation?: string;
|
documentation?: string;
|
||||||
marketplace?: string;
|
marketplace?: string;
|
||||||
@@ -43,19 +43,25 @@ let allItems: Tool[] = [];
|
|||||||
let search: FuzzySearch<Tool>;
|
let search: FuzzySearch<Tool>;
|
||||||
let currentFilters = {
|
let currentFilters = {
|
||||||
categories: [] as string[],
|
categories: [] as string[],
|
||||||
query: '',
|
query: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function formatMultilineText(text: string): string {
|
||||||
|
return escapeHtml(text).replace(/\r?\n/g, "<br>");
|
||||||
|
}
|
||||||
|
|
||||||
function applyFiltersAndRender(): void {
|
function applyFiltersAndRender(): void {
|
||||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
const searchInput = document.getElementById(
|
||||||
const countEl = document.getElementById('results-count');
|
"search-input"
|
||||||
const query = searchInput?.value || '';
|
) as HTMLInputElement;
|
||||||
|
const countEl = document.getElementById("results-count");
|
||||||
|
const query = searchInput?.value || "";
|
||||||
currentFilters.query = query;
|
currentFilters.query = query;
|
||||||
|
|
||||||
let results = query ? search.search(query) : [...allItems];
|
let results = query ? search.search(query) : [...allItems];
|
||||||
|
|
||||||
if (currentFilters.categories.length > 0) {
|
if (currentFilters.categories.length > 0) {
|
||||||
results = results.filter(item =>
|
results = results.filter((item) =>
|
||||||
currentFilters.categories.includes(item.category)
|
currentFilters.categories.includes(item.category)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -69,8 +75,8 @@ function applyFiltersAndRender(): void {
|
|||||||
if (countEl) countEl.textContent = countText;
|
if (countEl) countEl.textContent = countText;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTools(tools: Tool[], query = ''): void {
|
function renderTools(tools: Tool[], query = ""): void {
|
||||||
const container = document.getElementById('tools-list');
|
const container = document.getElementById("tools-list");
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
if (tools.length === 0) {
|
if (tools.length === 0) {
|
||||||
@@ -83,40 +89,54 @@ function renderTools(tools: Tool[], query = ''): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = tools.map(tool => {
|
container.innerHTML = tools
|
||||||
const badges: string[] = [];
|
.map((tool) => {
|
||||||
if (tool.featured) {
|
const badges: string[] = [];
|
||||||
badges.push('<span class="tool-badge featured">Featured</span>');
|
if (tool.featured) {
|
||||||
}
|
badges.push('<span class="tool-badge featured">Featured</span>');
|
||||||
badges.push(`<span class="tool-badge category">${escapeHtml(tool.category)}</span>`);
|
}
|
||||||
|
badges.push(
|
||||||
|
`<span class="tool-badge category">${escapeHtml(tool.category)}</span>`
|
||||||
|
);
|
||||||
|
|
||||||
const features = tool.features && tool.features.length > 0
|
const features =
|
||||||
? `<div class="tool-section">
|
tool.features && tool.features.length > 0
|
||||||
|
? `<div class="tool-section">
|
||||||
<h3>Features</h3>
|
<h3>Features</h3>
|
||||||
<ul>${tool.features.map(f => `<li>${escapeHtml(f)}</li>`).join('')}</ul>
|
<ul>${tool.features
|
||||||
|
.map((f) => `<li>${escapeHtml(f)}</li>`)
|
||||||
|
.join("")}</ul>
|
||||||
</div>`
|
</div>`
|
||||||
: '';
|
: "";
|
||||||
|
|
||||||
const requirements = tool.requirements && tool.requirements.length > 0
|
const requirements =
|
||||||
? `<div class="tool-section">
|
tool.requirements && tool.requirements.length > 0
|
||||||
|
? `<div class="tool-section">
|
||||||
<h3>Requirements</h3>
|
<h3>Requirements</h3>
|
||||||
<ul>${tool.requirements.map(r => `<li>${escapeHtml(r)}</li>`).join('')}</ul>
|
<ul>${tool.requirements
|
||||||
|
.map((r) => `<li>${escapeHtml(r)}</li>`)
|
||||||
|
.join("")}</ul>
|
||||||
</div>`
|
</div>`
|
||||||
: '';
|
: "";
|
||||||
|
|
||||||
const tags = tool.tags && tool.tags.length > 0
|
const tags =
|
||||||
? `<div class="tool-tags">
|
tool.tags && tool.tags.length > 0
|
||||||
${tool.tags.map(t => `<span class="tool-tag">${escapeHtml(t)}</span>`).join('')}
|
? `<div class="tool-tags">
|
||||||
|
${tool.tags
|
||||||
|
.map((t) => `<span class="tool-tag">${escapeHtml(t)}</span>`)
|
||||||
|
.join("")}
|
||||||
</div>`
|
</div>`
|
||||||
: '';
|
: "";
|
||||||
|
|
||||||
const config = tool.configuration
|
const config = tool.configuration
|
||||||
? `<div class="tool-config">
|
? `<div class="tool-config">
|
||||||
<h3>Configuration</h3>
|
<h3>Configuration</h3>
|
||||||
<div class="tool-config-wrapper">
|
<div class="tool-config-wrapper">
|
||||||
<pre><code>${escapeHtml(tool.configuration.content)}</code></pre>
|
<pre><code>${escapeHtml(tool.configuration.content)}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<button class="copy-config-btn" data-config="${encodeURIComponent(tool.configuration.content)}">
|
<button class="copy-config-btn" data-config="${encodeURIComponent(
|
||||||
|
tool.configuration.content
|
||||||
|
)}">
|
||||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/>
|
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/>
|
||||||
<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/>
|
<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/>
|
||||||
@@ -124,52 +144,74 @@ function renderTools(tools: Tool[], query = ''): void {
|
|||||||
Copy Configuration
|
Copy Configuration
|
||||||
</button>
|
</button>
|
||||||
</div>`
|
</div>`
|
||||||
: '';
|
: "";
|
||||||
|
|
||||||
const actions: string[] = [];
|
const actions: string[] = [];
|
||||||
if (tool.links.blog) {
|
if (tool.links.blog) {
|
||||||
actions.push(`<a href="${tool.links.blog}" class="btn btn-secondary" target="_blank" rel="noopener">📖 Blog</a>`);
|
actions.push(
|
||||||
}
|
`<a href="${tool.links.blog}" class="btn btn-secondary" target="_blank" rel="noopener">📖 Blog</a>`
|
||||||
if (tool.links.marketplace) {
|
);
|
||||||
actions.push(`<a href="${tool.links.marketplace}" class="btn btn-secondary" target="_blank" rel="noopener">🏪 Marketplace</a>`);
|
}
|
||||||
}
|
if (tool.links.marketplace) {
|
||||||
if (tool.links.npm) {
|
actions.push(
|
||||||
actions.push(`<a href="${tool.links.npm}" class="btn btn-secondary" target="_blank" rel="noopener">📦 npm</a>`);
|
`<a href="${tool.links.marketplace}" class="btn btn-secondary" target="_blank" rel="noopener">🏪 Marketplace</a>`
|
||||||
}
|
);
|
||||||
if (tool.links.pypi) {
|
}
|
||||||
actions.push(`<a href="${tool.links.pypi}" class="btn btn-secondary" target="_blank" rel="noopener">🐍 PyPI</a>`);
|
if (tool.links.npm) {
|
||||||
}
|
actions.push(
|
||||||
if (tool.links.documentation) {
|
`<a href="${tool.links.npm}" class="btn btn-secondary" target="_blank" rel="noopener">📦 npm</a>`
|
||||||
actions.push(`<a href="${tool.links.documentation}" class="btn btn-secondary" target="_blank" rel="noopener">📚 Docs</a>`);
|
);
|
||||||
}
|
}
|
||||||
if (tool.links.github) {
|
if (tool.links.pypi) {
|
||||||
actions.push(`<a href="${tool.links.github}" class="btn btn-secondary" target="_blank" rel="noopener">GitHub</a>`);
|
actions.push(
|
||||||
}
|
`<a href="${tool.links.pypi}" class="btn btn-secondary" target="_blank" rel="noopener">🐍 PyPI</a>`
|
||||||
if (tool.links.vscode) {
|
);
|
||||||
actions.push(`<a href="${tool.links.vscode}" class="btn btn-primary" target="_blank" rel="noopener">Install in VS Code</a>`);
|
}
|
||||||
}
|
if (tool.links.documentation) {
|
||||||
if (tool.links['vscode-insiders']) {
|
actions.push(
|
||||||
actions.push(`<a href="${tool.links['vscode-insiders']}" class="btn btn-outline" target="_blank" rel="noopener">VS Code Insiders</a>`);
|
`<a href="${tool.links.documentation}" class="btn btn-secondary" target="_blank" rel="noopener">📚 Docs</a>`
|
||||||
}
|
);
|
||||||
if (tool.links['visual-studio']) {
|
}
|
||||||
actions.push(`<a href="${tool.links['visual-studio']}" class="btn btn-outline" target="_blank" rel="noopener">Visual Studio</a>`);
|
if (tool.links.github) {
|
||||||
}
|
actions.push(
|
||||||
|
`<a href="${tool.links.github}" class="btn btn-secondary" target="_blank" rel="noopener">GitHub</a>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (tool.links.vscode) {
|
||||||
|
actions.push(
|
||||||
|
`<a href="${tool.links.vscode}" class="btn btn-primary" target="_blank" rel="noopener">Install in VS Code</a>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (tool.links["vscode-insiders"]) {
|
||||||
|
actions.push(
|
||||||
|
`<a href="${tool.links["vscode-insiders"]}" class="btn btn-outline" target="_blank" rel="noopener">VS Code Insiders</a>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (tool.links["visual-studio"]) {
|
||||||
|
actions.push(
|
||||||
|
`<a href="${tool.links["visual-studio"]}" class="btn btn-outline" target="_blank" rel="noopener">Visual Studio</a>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const actionsHtml = actions.length > 0
|
const actionsHtml =
|
||||||
? `<div class="tool-actions">${actions.join('')}</div>`
|
actions.length > 0
|
||||||
: '';
|
? `<div class="tool-actions">${actions.join("")}</div>`
|
||||||
|
: "";
|
||||||
|
|
||||||
const titleHtml = query ? search.highlight(tool.name, query) : escapeHtml(tool.name);
|
const titleHtml = query
|
||||||
|
? search.highlight(tool.name, query)
|
||||||
|
: escapeHtml(tool.name);
|
||||||
|
const descriptionHtml = formatMultilineText(tool.description);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="tool-card">
|
<div class="tool-card">
|
||||||
<div class="tool-header">
|
<div class="tool-header">
|
||||||
<h2>${titleHtml}</h2>
|
<h2>${titleHtml}</h2>
|
||||||
<div class="tool-badges">
|
<div class="tool-badges">
|
||||||
${badges.join('')}
|
${badges.join("")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="tool-description">${escapeHtml(tool.description)}</p>
|
<p class="tool-description">${descriptionHtml}</p>
|
||||||
${features}
|
${features}
|
||||||
${requirements}
|
${requirements}
|
||||||
${config}
|
${config}
|
||||||
@@ -177,20 +219,21 @@ function renderTools(tools: Tool[], query = ''): void {
|
|||||||
${actionsHtml}
|
${actionsHtml}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
setupCopyConfigHandlers();
|
setupCopyConfigHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupCopyConfigHandlers(): void {
|
function setupCopyConfigHandlers(): void {
|
||||||
document.querySelectorAll('.copy-config-btn').forEach(btn => {
|
document.querySelectorAll(".copy-config-btn").forEach((btn) => {
|
||||||
btn.addEventListener('click', async (e) => {
|
btn.addEventListener("click", async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const button = e.currentTarget as HTMLButtonElement;
|
const button = e.currentTarget as HTMLButtonElement;
|
||||||
const config = decodeURIComponent(button.dataset.config || '');
|
const config = decodeURIComponent(button.dataset.config || "");
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(config);
|
await navigator.clipboard.writeText(config);
|
||||||
button.classList.add('copied');
|
button.classList.add("copied");
|
||||||
const originalHtml = button.innerHTML;
|
const originalHtml = button.innerHTML;
|
||||||
button.innerHTML = `
|
button.innerHTML = `
|
||||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
||||||
@@ -199,35 +242,40 @@ function setupCopyConfigHandlers(): void {
|
|||||||
Copied!
|
Copied!
|
||||||
`;
|
`;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
button.classList.remove('copied');
|
button.classList.remove("copied");
|
||||||
button.innerHTML = originalHtml;
|
button.innerHTML = originalHtml;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to copy:', err);
|
console.error("Failed to copy:", err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initToolsPage(): Promise<void> {
|
export async function initToolsPage(): Promise<void> {
|
||||||
const container = document.getElementById('tools-list');
|
const container = document.getElementById("tools-list");
|
||||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
const searchInput = document.getElementById(
|
||||||
const categoryFilter = document.getElementById('filter-category') as HTMLSelectElement;
|
"search-input"
|
||||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
) as HTMLInputElement;
|
||||||
const countEl = document.getElementById('results-count');
|
const categoryFilter = document.getElementById(
|
||||||
|
"filter-category"
|
||||||
|
) as HTMLSelectElement;
|
||||||
|
const clearFiltersBtn = document.getElementById("clear-filters");
|
||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
container.innerHTML = '<div class="loading">Loading tools...</div>';
|
container.innerHTML = '<div class="loading">Loading tools...</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await fetchData<ToolsData>('tools.json');
|
const data = await fetchData<ToolsData>("tools.json");
|
||||||
if (!data || !data.items) {
|
if (!data || !data.items) {
|
||||||
if (container) container.innerHTML = '<div class="empty-state"><h3>Failed to load tools</h3></div>';
|
if (container)
|
||||||
|
container.innerHTML =
|
||||||
|
'<div class="empty-state"><h3>Failed to load tools</h3></div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map items to include title for FuzzySearch
|
// Map items to include title for FuzzySearch
|
||||||
allItems = data.items.map(item => ({
|
allItems = data.items.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
title: item.name, // FuzzySearch uses title
|
title: item.name, // FuzzySearch uses title
|
||||||
}));
|
}));
|
||||||
@@ -237,23 +285,33 @@ export async function initToolsPage(): Promise<void> {
|
|||||||
|
|
||||||
// Populate category filter
|
// Populate category filter
|
||||||
if (categoryFilter && data.filters.categories) {
|
if (categoryFilter && data.filters.categories) {
|
||||||
categoryFilter.innerHTML = '<option value="">All Categories</option>' +
|
categoryFilter.innerHTML =
|
||||||
data.filters.categories.map(c => `<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>`).join('');
|
'<option value="">All Categories</option>' +
|
||||||
|
data.filters.categories
|
||||||
|
.map(
|
||||||
|
(c) => `<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
categoryFilter.addEventListener('change', () => {
|
categoryFilter.addEventListener("change", () => {
|
||||||
currentFilters.categories = categoryFilter.value ? [categoryFilter.value] : [];
|
currentFilters.categories = categoryFilter.value
|
||||||
|
? [categoryFilter.value]
|
||||||
|
: [];
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search input handler
|
// Search input handler
|
||||||
searchInput?.addEventListener('input', debounce(() => applyFiltersAndRender(), 200));
|
searchInput?.addEventListener(
|
||||||
|
"input",
|
||||||
|
debounce(() => applyFiltersAndRender(), 200)
|
||||||
|
);
|
||||||
|
|
||||||
// Clear filters
|
// Clear filters
|
||||||
clearFiltersBtn?.addEventListener('click', () => {
|
clearFiltersBtn?.addEventListener("click", () => {
|
||||||
currentFilters = { categories: [], query: '' };
|
currentFilters = { categories: [], query: "" };
|
||||||
if (categoryFilter) categoryFilter.value = '';
|
if (categoryFilter) categoryFilter.value = "";
|
||||||
if (searchInput) searchInput.value = '';
|
if (searchInput) searchInput.value = "";
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -261,4 +319,4 @@ export async function initToolsPage(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Auto-initialize when DOM is ready
|
// Auto-initialize when DOM is ready
|
||||||
document.addEventListener('DOMContentLoaded', initToolsPage);
|
document.addEventListener("DOMContentLoaded", initToolsPage);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Simple substring matching on title and description with scoring
|
* Simple substring matching on title and description with scoring
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { escapeHtml, fetchData } from './utils';
|
import { escapeHtml, fetchData } from "./utils";
|
||||||
|
|
||||||
export interface SearchItem {
|
export interface SearchItem {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -45,7 +45,7 @@ export class FuzzySearch<T extends SearchableItem = SearchItem> {
|
|||||||
*/
|
*/
|
||||||
search(query: string, options: SearchOptions = {}): T[] {
|
search(query: string, options: SearchOptions = {}): T[] {
|
||||||
const {
|
const {
|
||||||
fields = ['title', 'description', 'searchText'],
|
fields = ["title", "description", "searchText"],
|
||||||
limit = 50,
|
limit = 50,
|
||||||
minScore = 0,
|
minScore = 0,
|
||||||
} = options;
|
} = options;
|
||||||
@@ -68,13 +68,17 @@ export class FuzzySearch<T extends SearchableItem = SearchItem> {
|
|||||||
// Sort by score descending
|
// Sort by score descending
|
||||||
results.sort((a, b) => b.score - a.score);
|
results.sort((a, b) => b.score - a.score);
|
||||||
|
|
||||||
return results.slice(0, limit).map(r => r.item);
|
return results.slice(0, limit).map((r) => r.item);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate match score for an item
|
* Calculate match score for an item
|
||||||
*/
|
*/
|
||||||
private calculateScore(item: T, queryWords: string[], fields: string[]): number {
|
private calculateScore(
|
||||||
|
item: T,
|
||||||
|
queryWords: string[],
|
||||||
|
fields: string[]
|
||||||
|
): number {
|
||||||
let totalScore = 0;
|
let totalScore = 0;
|
||||||
|
|
||||||
for (const word of queryWords) {
|
for (const word of queryWords) {
|
||||||
@@ -87,23 +91,23 @@ export class FuzzySearch<T extends SearchableItem = SearchItem> {
|
|||||||
const normalizedValue = String(value).toLowerCase();
|
const normalizedValue = String(value).toLowerCase();
|
||||||
|
|
||||||
// Exact match in title gets highest score
|
// Exact match in title gets highest score
|
||||||
if (field === 'title' && normalizedValue === word) {
|
if (field === "title" && normalizedValue === word) {
|
||||||
wordScore = Math.max(wordScore, 100);
|
wordScore = Math.max(wordScore, 100);
|
||||||
}
|
}
|
||||||
// Title starts with word
|
// Title starts with word
|
||||||
else if (field === 'title' && normalizedValue.startsWith(word)) {
|
else if (field === "title" && normalizedValue.startsWith(word)) {
|
||||||
wordScore = Math.max(wordScore, 80);
|
wordScore = Math.max(wordScore, 80);
|
||||||
}
|
}
|
||||||
// Title contains word
|
// Title contains word
|
||||||
else if (field === 'title' && normalizedValue.includes(word)) {
|
else if (field === "title" && normalizedValue.includes(word)) {
|
||||||
wordScore = Math.max(wordScore, 60);
|
wordScore = Math.max(wordScore, 60);
|
||||||
}
|
}
|
||||||
// Description contains word
|
// Description contains word
|
||||||
else if (field === 'description' && normalizedValue.includes(word)) {
|
else if (field === "description" && normalizedValue.includes(word)) {
|
||||||
wordScore = Math.max(wordScore, 30);
|
wordScore = Math.max(wordScore, 30);
|
||||||
}
|
}
|
||||||
// searchText (includes tags, tools, etc) contains word
|
// searchText (includes tags, tools, etc) contains word
|
||||||
else if (field === 'searchText' && normalizedValue.includes(word)) {
|
else if (field === "searchText" && normalizedValue.includes(word)) {
|
||||||
wordScore = Math.max(wordScore, 20);
|
wordScore = Math.max(wordScore, 20);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,8 +116,8 @@ export class FuzzySearch<T extends SearchableItem = SearchItem> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bonus for matching all words
|
// Bonus for matching all words
|
||||||
const matchesAllWords = queryWords.every(word =>
|
const matchesAllWords = queryWords.every((word) =>
|
||||||
fields.some(field => {
|
fields.some((field) => {
|
||||||
const value = (item as Record<string, unknown>)[field];
|
const value = (item as Record<string, unknown>)[field];
|
||||||
return value && String(value).toLowerCase().includes(word);
|
return value && String(value).toLowerCase().includes(word);
|
||||||
})
|
})
|
||||||
@@ -130,7 +134,7 @@ export class FuzzySearch<T extends SearchableItem = SearchItem> {
|
|||||||
* Highlight matching text in a string
|
* Highlight matching text in a string
|
||||||
*/
|
*/
|
||||||
highlight(text: string, query: string): string {
|
highlight(text: string, query: string): string {
|
||||||
if (!query || !text) return escapeHtml(text || '');
|
if (!query || !text) return escapeHtml(text || "");
|
||||||
|
|
||||||
const normalizedQuery = query.toLowerCase().trim();
|
const normalizedQuery = query.toLowerCase().trim();
|
||||||
const words = normalizedQuery.split(/\s+/);
|
const words = normalizedQuery.split(/\s+/);
|
||||||
@@ -138,8 +142,27 @@ export class FuzzySearch<T extends SearchableItem = SearchItem> {
|
|||||||
|
|
||||||
for (const word of words) {
|
for (const word of words) {
|
||||||
if (word.length < 2) continue;
|
if (word.length < 2) continue;
|
||||||
const regex = new RegExp(`(${word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
const regex = new RegExp(
|
||||||
result = result.replace(regex, '<mark>$1</mark>');
|
`(${word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`,
|
||||||
|
"gi"
|
||||||
|
);
|
||||||
|
const parts = result.split(/(<[^>]+>)/g);
|
||||||
|
let inMark = false;
|
||||||
|
result = parts
|
||||||
|
.map((part) => {
|
||||||
|
if (part.startsWith("<")) {
|
||||||
|
if (part.toLowerCase() === "<mark>") inMark = true;
|
||||||
|
if (part.toLowerCase() === "</mark>") inMark = false;
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inMark) {
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
|
||||||
|
return part.replace(regex, "<mark>$1</mark>");
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -153,7 +176,7 @@ export const globalSearch = new FuzzySearch<SearchItem>();
|
|||||||
* Initialize global search with search index
|
* Initialize global search with search index
|
||||||
*/
|
*/
|
||||||
export async function initGlobalSearch(): Promise<FuzzySearch<SearchItem>> {
|
export async function initGlobalSearch(): Promise<FuzzySearch<SearchItem>> {
|
||||||
const searchIndex = await fetchData<SearchItem[]>('search-index.json');
|
const searchIndex = await fetchData<SearchItem[]>("search-index.json");
|
||||||
if (searchIndex) {
|
if (searchIndex) {
|
||||||
globalSearch.setItems(searchIndex);
|
globalSearch.setItems(searchIndex);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ function applyTheme(theme: 'light' | 'dark'): void {
|
|||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initialTheme = getThemePreference();
|
||||||
|
applyTheme(initialTheme);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle between light and dark theme
|
* Toggle between light and dark theme
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,22 +2,26 @@
|
|||||||
* Utility functions for the Awesome Copilot website
|
* Utility functions for the Awesome Copilot website
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const REPO_BASE_URL = 'https://raw.githubusercontent.com/github/awesome-copilot/main';
|
const REPO_BASE_URL =
|
||||||
const REPO_GITHUB_URL = 'https://github.com/github/awesome-copilot/blob/main';
|
"https://raw.githubusercontent.com/github/awesome-copilot/main";
|
||||||
|
const REPO_GITHUB_URL = "https://github.com/github/awesome-copilot/blob/main";
|
||||||
|
|
||||||
// VS Code install URL configurations
|
// VS Code install URL configurations
|
||||||
const VSCODE_INSTALL_CONFIG: Record<string, { baseUrl: string; scheme: string }> = {
|
const VSCODE_INSTALL_CONFIG: Record<
|
||||||
|
string,
|
||||||
|
{ baseUrl: string; scheme: string }
|
||||||
|
> = {
|
||||||
instructions: {
|
instructions: {
|
||||||
baseUrl: 'https://aka.ms/awesome-copilot/install/instructions',
|
baseUrl: "https://aka.ms/awesome-copilot/install/instructions",
|
||||||
scheme: 'chat-instructions'
|
scheme: "chat-instructions",
|
||||||
},
|
},
|
||||||
prompt: {
|
prompt: {
|
||||||
baseUrl: 'https://aka.ms/awesome-copilot/install/prompt',
|
baseUrl: "https://aka.ms/awesome-copilot/install/prompt",
|
||||||
scheme: 'chat-prompt'
|
scheme: "chat-prompt",
|
||||||
},
|
},
|
||||||
agent: {
|
agent: {
|
||||||
baseUrl: 'https://aka.ms/awesome-copilot/install/agent',
|
baseUrl: "https://aka.ms/awesome-copilot/install/agent",
|
||||||
scheme: 'chat-agent'
|
scheme: "chat-agent",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -27,16 +31,18 @@ const VSCODE_INSTALL_CONFIG: Record<string, { baseUrl: string; scheme: string }>
|
|||||||
export function getBasePath(): string {
|
export function getBasePath(): string {
|
||||||
// In Astro, import.meta.env.BASE_URL is available at build time
|
// In Astro, import.meta.env.BASE_URL is available at build time
|
||||||
// At runtime, we use a data attribute on the body
|
// At runtime, we use a data attribute on the body
|
||||||
if (typeof document !== 'undefined') {
|
if (typeof document !== "undefined") {
|
||||||
return document.body.dataset.basePath || '/';
|
return document.body.dataset.basePath || "/";
|
||||||
}
|
}
|
||||||
return '/';
|
return "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch JSON data from the data directory
|
* Fetch JSON data from the data directory
|
||||||
*/
|
*/
|
||||||
export async function fetchData<T = unknown>(filename: string): Promise<T | null> {
|
export async function fetchData<T = unknown>(
|
||||||
|
filename: string
|
||||||
|
): Promise<T | null> {
|
||||||
try {
|
try {
|
||||||
const basePath = getBasePath();
|
const basePath = getBasePath();
|
||||||
const response = await fetch(`${basePath}data/${filename}`);
|
const response = await fetch(`${basePath}data/${filename}`);
|
||||||
@@ -51,7 +57,9 @@ export async function fetchData<T = unknown>(filename: string): Promise<T | null
|
|||||||
/**
|
/**
|
||||||
* Fetch raw file content from GitHub
|
* Fetch raw file content from GitHub
|
||||||
*/
|
*/
|
||||||
export async function fetchFileContent(filePath: string): Promise<string | null> {
|
export async function fetchFileContent(
|
||||||
|
filePath: string
|
||||||
|
): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${REPO_BASE_URL}/${filePath}`);
|
const response = await fetch(`${REPO_BASE_URL}/${filePath}`);
|
||||||
if (!response.ok) throw new Error(`Failed to fetch ${filePath}`);
|
if (!response.ok) throw new Error(`Failed to fetch ${filePath}`);
|
||||||
@@ -70,14 +78,14 @@ export async function copyToClipboard(text: string): Promise<boolean> {
|
|||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback for older browsers
|
// Deprecated fallback for older browsers that lack the async clipboard API.
|
||||||
const textarea = document.createElement('textarea');
|
const textarea = document.createElement("textarea");
|
||||||
textarea.value = text;
|
textarea.value = text;
|
||||||
textarea.style.position = 'fixed';
|
textarea.style.position = "fixed";
|
||||||
textarea.style.opacity = '0';
|
textarea.style.opacity = "0";
|
||||||
document.body.appendChild(textarea);
|
document.body.appendChild(textarea);
|
||||||
textarea.select();
|
textarea.select();
|
||||||
const success = document.execCommand('copy');
|
const success = document.execCommand("copy");
|
||||||
document.body.removeChild(textarea);
|
document.body.removeChild(textarea);
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
@@ -89,13 +97,19 @@ export async function copyToClipboard(text: string): Promise<boolean> {
|
|||||||
* @param filePath - Path to the file
|
* @param filePath - Path to the file
|
||||||
* @param insiders - Whether to use VS Code Insiders
|
* @param insiders - Whether to use VS Code Insiders
|
||||||
*/
|
*/
|
||||||
export function getVSCodeInstallUrl(type: string, filePath: string, insiders = false): string | null {
|
export function getVSCodeInstallUrl(
|
||||||
|
type: string,
|
||||||
|
filePath: string,
|
||||||
|
insiders = false
|
||||||
|
): string | null {
|
||||||
const config = VSCODE_INSTALL_CONFIG[type];
|
const config = VSCODE_INSTALL_CONFIG[type];
|
||||||
if (!config) return null;
|
if (!config) return null;
|
||||||
|
|
||||||
const rawUrl = `${REPO_BASE_URL}/${filePath}`;
|
const rawUrl = `${REPO_BASE_URL}/${filePath}`;
|
||||||
const vscodeScheme = insiders ? 'vscode-insiders' : 'vscode';
|
const vscodeScheme = insiders ? "vscode-insiders" : "vscode";
|
||||||
const innerUrl = `${vscodeScheme}:${config.scheme}/install?url=${encodeURIComponent(rawUrl)}`;
|
const innerUrl = `${vscodeScheme}:${
|
||||||
|
config.scheme
|
||||||
|
}/install?url=${encodeURIComponent(rawUrl)}`;
|
||||||
|
|
||||||
return `${config.baseUrl}?url=${encodeURIComponent(innerUrl)}`;
|
return `${config.baseUrl}?url=${encodeURIComponent(innerUrl)}`;
|
||||||
}
|
}
|
||||||
@@ -120,15 +134,15 @@ export function getRawGitHubUrl(filePath: string): string {
|
|||||||
export async function downloadFile(filePath: string): Promise<boolean> {
|
export async function downloadFile(filePath: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${REPO_BASE_URL}/${filePath}`);
|
const response = await fetch(`${REPO_BASE_URL}/${filePath}`);
|
||||||
if (!response.ok) throw new Error('Failed to fetch file');
|
if (!response.ok) throw new Error("Failed to fetch file");
|
||||||
|
|
||||||
const content = await response.text();
|
const content = await response.text();
|
||||||
const filename = filePath.split('/').pop() || 'file.md';
|
const filename = filePath.split("/").pop() || "file.md";
|
||||||
|
|
||||||
const blob = new Blob([content], { type: 'text/markdown' });
|
const blob = new Blob([content], { type: "text/markdown" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
const a = document.createElement('a');
|
const a = document.createElement("a");
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = filename;
|
a.download = filename;
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
@@ -138,7 +152,7 @@ export async function downloadFile(filePath: string): Promise<boolean> {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Download failed:', error);
|
console.error("Download failed:", error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,18 +161,23 @@ export async function downloadFile(filePath: string): Promise<boolean> {
|
|||||||
* Share/copy link to clipboard (deep link to current page with file hash)
|
* Share/copy link to clipboard (deep link to current page with file hash)
|
||||||
*/
|
*/
|
||||||
export async function shareFile(filePath: string): Promise<boolean> {
|
export async function shareFile(filePath: string): Promise<boolean> {
|
||||||
const deepLinkUrl = `${window.location.origin}${window.location.pathname}#file=${encodeURIComponent(filePath)}`;
|
const deepLinkUrl = `${window.location.origin}${
|
||||||
|
window.location.pathname
|
||||||
|
}#file=${encodeURIComponent(filePath)}`;
|
||||||
return copyToClipboard(deepLinkUrl);
|
return copyToClipboard(deepLinkUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a toast notification
|
* Show a toast notification
|
||||||
*/
|
*/
|
||||||
export function showToast(message: string, type: 'success' | 'error' = 'success'): void {
|
export function showToast(
|
||||||
const existing = document.querySelector('.toast');
|
message: string,
|
||||||
|
type: "success" | "error" = "success"
|
||||||
|
): void {
|
||||||
|
const existing = document.querySelector(".toast");
|
||||||
if (existing) existing.remove();
|
if (existing) existing.remove();
|
||||||
|
|
||||||
const toast = document.createElement('div');
|
const toast = document.createElement("div");
|
||||||
toast.className = `toast ${type}`;
|
toast.className = `toast ${type}`;
|
||||||
toast.textContent = message;
|
toast.textContent = message;
|
||||||
document.body.appendChild(toast);
|
document.body.appendChild(toast);
|
||||||
@@ -190,7 +209,7 @@ export function debounce<T extends (...args: unknown[]) => void>(
|
|||||||
* Escape HTML to prevent XSS
|
* Escape HTML to prevent XSS
|
||||||
*/
|
*/
|
||||||
export function escapeHtml(text: string): string {
|
export function escapeHtml(text: string): string {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement("div");
|
||||||
div.textContent = text;
|
div.textContent = text;
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
@@ -199,20 +218,21 @@ export function escapeHtml(text: string): string {
|
|||||||
* Truncate text with ellipsis
|
* Truncate text with ellipsis
|
||||||
*/
|
*/
|
||||||
export function truncate(text: string | undefined, maxLength: number): string {
|
export function truncate(text: string | undefined, maxLength: number): string {
|
||||||
if (!text || text.length <= maxLength) return text || '';
|
if (!text || text.length <= maxLength) return text || "";
|
||||||
return text.slice(0, maxLength).trim() + '...';
|
return text.slice(0, maxLength).trim() + "...";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get resource type from file path
|
* Get resource type from file path
|
||||||
*/
|
*/
|
||||||
export function getResourceType(filePath: string): string {
|
export function getResourceType(filePath: string): string {
|
||||||
if (filePath.endsWith('.agent.md')) return 'agent';
|
if (filePath.endsWith(".agent.md")) return "agent";
|
||||||
if (filePath.endsWith('.prompt.md')) return 'prompt';
|
if (filePath.endsWith(".prompt.md")) return "prompt";
|
||||||
if (filePath.endsWith('.instructions.md')) return 'instruction';
|
if (filePath.endsWith(".instructions.md")) return "instruction";
|
||||||
if (filePath.includes('/skills/') && filePath.endsWith('SKILL.md')) return 'skill';
|
if (filePath.includes("/skills/") && filePath.endsWith("SKILL.md"))
|
||||||
if (filePath.endsWith('.collection.yml')) return 'collection';
|
return "skill";
|
||||||
return 'unknown';
|
if (filePath.endsWith(".collection.yml")) return "collection";
|
||||||
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -220,11 +240,11 @@ export function getResourceType(filePath: string): string {
|
|||||||
*/
|
*/
|
||||||
export function formatResourceType(type: string): string {
|
export function formatResourceType(type: string): string {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
agent: '🤖 Agent',
|
agent: "🤖 Agent",
|
||||||
prompt: '🎯 Prompt',
|
prompt: "🎯 Prompt",
|
||||||
instruction: '📋 Instruction',
|
instruction: "📋 Instruction",
|
||||||
skill: '⚡ Skill',
|
skill: "⚡ Skill",
|
||||||
collection: '📦 Collection',
|
collection: "📦 Collection",
|
||||||
};
|
};
|
||||||
return labels[type] || type;
|
return labels[type] || type;
|
||||||
}
|
}
|
||||||
@@ -234,42 +254,50 @@ export function formatResourceType(type: string): string {
|
|||||||
*/
|
*/
|
||||||
export function getResourceIcon(type: string): string {
|
export function getResourceIcon(type: string): string {
|
||||||
const icons: Record<string, string> = {
|
const icons: Record<string, string> = {
|
||||||
agent: '🤖',
|
agent: "🤖",
|
||||||
prompt: '🎯',
|
prompt: "🎯",
|
||||||
instruction: '📋',
|
instruction: "📋",
|
||||||
skill: '⚡',
|
skill: "⚡",
|
||||||
collection: '📦',
|
collection: "📦",
|
||||||
};
|
};
|
||||||
return icons[type] || '📄';
|
return icons[type] || "📄";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate HTML for install dropdown button
|
* Generate HTML for install dropdown button
|
||||||
*/
|
*/
|
||||||
export function getInstallDropdownHtml(type: string, filePath: string, small = false): string {
|
export function getInstallDropdownHtml(
|
||||||
|
type: string,
|
||||||
|
filePath: string,
|
||||||
|
small = false
|
||||||
|
): string {
|
||||||
const vscodeUrl = getVSCodeInstallUrl(type, filePath, false);
|
const vscodeUrl = getVSCodeInstallUrl(type, filePath, false);
|
||||||
const insidersUrl = getVSCodeInstallUrl(type, filePath, true);
|
const insidersUrl = getVSCodeInstallUrl(type, filePath, true);
|
||||||
|
|
||||||
if (!vscodeUrl) return '';
|
if (!vscodeUrl) return "";
|
||||||
|
|
||||||
const sizeClass = small ? 'install-dropdown-small' : '';
|
const sizeClass = small ? "install-dropdown-small" : "";
|
||||||
const uniqueId = `install-${filePath.replace(/[^a-zA-Z0-9]/g, '-')}`;
|
const uniqueId = `install-${filePath.replace(/[^a-zA-Z0-9]/g, "-")}`;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="install-dropdown ${sizeClass}" id="${uniqueId}" onclick="event.stopPropagation()">
|
<div class="install-dropdown ${sizeClass}" id="${uniqueId}" data-install-scope="list">
|
||||||
<a href="${vscodeUrl}" class="btn btn-primary ${small ? 'btn-small' : ''} install-btn-main" target="_blank" rel="noopener">
|
<a href="${vscodeUrl}" class="btn btn-primary ${
|
||||||
|
small ? "btn-small" : ""
|
||||||
|
} install-btn-main" target="_blank" rel="noopener">
|
||||||
Install
|
Install
|
||||||
</a>
|
</a>
|
||||||
<button type="button" class="btn btn-primary ${small ? 'btn-small' : ''} install-btn-toggle" aria-label="Install options" onclick="event.preventDefault(); this.parentElement.classList.toggle('open');">
|
<button type="button" class="btn btn-primary ${
|
||||||
|
small ? "btn-small" : ""
|
||||||
|
} install-btn-toggle" aria-label="Install options" aria-expanded="false">
|
||||||
<svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor">
|
<svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor">
|
||||||
<path d="M4.427 7.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 7H4.604a.25.25 0 00-.177.427z"/>
|
<path d="M4.427 7.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 7H4.604a.25.25 0 00-.177.427z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="install-dropdown-menu">
|
<div class="install-dropdown-menu">
|
||||||
<a href="${vscodeUrl}" target="_blank" rel="noopener" onclick="this.closest('.install-dropdown').classList.remove('open')">
|
<a href="${vscodeUrl}" target="_blank" rel="noopener">
|
||||||
VS Code
|
VS Code
|
||||||
</a>
|
</a>
|
||||||
<a href="${insidersUrl}" target="_blank" rel="noopener" onclick="this.closest('.install-dropdown').classList.remove('open')">
|
<a href="${insidersUrl}" target="_blank" rel="noopener">
|
||||||
VS Code Insiders
|
VS Code Insiders
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -281,33 +309,77 @@ export function getInstallDropdownHtml(type: string, filePath: string, small = f
|
|||||||
* Setup dropdown close handlers for dynamically created dropdowns
|
* Setup dropdown close handlers for dynamically created dropdowns
|
||||||
*/
|
*/
|
||||||
export function setupDropdownCloseHandlers(): void {
|
export function setupDropdownCloseHandlers(): void {
|
||||||
document.addEventListener('click', (e) => {
|
if (dropdownHandlersReady) return;
|
||||||
const target = e.target as HTMLElement;
|
dropdownHandlersReady = true;
|
||||||
// Close all open dropdowns if clicking outside
|
|
||||||
if (!target.closest('.install-dropdown')) {
|
document.addEventListener(
|
||||||
document.querySelectorAll('.install-dropdown.open').forEach(dropdown => {
|
"click",
|
||||||
dropdown.classList.remove('open');
|
(e) => {
|
||||||
});
|
const target = e.target as HTMLElement;
|
||||||
}
|
const dropdown = target.closest(
|
||||||
});
|
'.install-dropdown[data-install-scope="list"]'
|
||||||
|
);
|
||||||
|
const toggle = target.closest(
|
||||||
|
".install-btn-toggle"
|
||||||
|
) as HTMLButtonElement | null;
|
||||||
|
const menuLink = target.closest(
|
||||||
|
".install-dropdown-menu a"
|
||||||
|
) as HTMLAnchorElement | null;
|
||||||
|
|
||||||
|
if (dropdown) {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (toggle) {
|
||||||
|
e.preventDefault();
|
||||||
|
const isOpen = dropdown.classList.toggle("open");
|
||||||
|
toggle.setAttribute("aria-expanded", String(isOpen));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuLink) {
|
||||||
|
dropdown.classList.remove("open");
|
||||||
|
const toggleBtn = dropdown.querySelector<HTMLButtonElement>(
|
||||||
|
".install-btn-toggle"
|
||||||
|
);
|
||||||
|
toggleBtn?.setAttribute("aria-expanded", "false");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelectorAll('.install-dropdown[data-install-scope="list"].open')
|
||||||
|
.forEach((openDropdown) => {
|
||||||
|
openDropdown.classList.remove("open");
|
||||||
|
const toggleBtn = openDropdown.querySelector<HTMLButtonElement>(
|
||||||
|
".install-btn-toggle"
|
||||||
|
);
|
||||||
|
toggleBtn?.setAttribute("aria-expanded", "false");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate HTML for action buttons (download, share) in list view
|
* Generate HTML for action buttons (download, share) in list view
|
||||||
*/
|
*/
|
||||||
export function getActionButtonsHtml(filePath: string, small = false): string {
|
export function getActionButtonsHtml(filePath: string, small = false): string {
|
||||||
const btnClass = small ? 'btn-small' : '';
|
const btnClass = small ? "btn-small" : "";
|
||||||
const iconSize = small ? 14 : 16;
|
const iconSize = small ? 14 : 16;
|
||||||
// Escape backslashes first, then single quotes to prevent breaking out of the JavaScript string literal in the onclick attribute
|
|
||||||
const escapedPath = filePath.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<button class="btn btn-secondary ${btnClass} action-download" data-path="${escapeHtml(filePath)}" onclick="event.stopPropagation(); window.__downloadFile && window.__downloadFile('${escapedPath}')" title="Download file">
|
<button class="btn btn-secondary ${btnClass} action-download" data-path="${escapeHtml(
|
||||||
|
filePath
|
||||||
|
)}" title="Download file">
|
||||||
<svg viewBox="0 0 16 16" width="${iconSize}" height="${iconSize}" fill="currentColor">
|
<svg viewBox="0 0 16 16" width="${iconSize}" height="${iconSize}" fill="currentColor">
|
||||||
<path d="M7.47 10.78a.75.75 0 0 0 1.06 0l3.75-3.75a.75.75 0 0 0-1.06-1.06L8.75 8.44V1.75a.75.75 0 0 0-1.5 0v6.69L4.78 5.97a.75.75 0 0 0-1.06 1.06l3.75 3.75ZM3.75 13a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z"/>
|
<path d="M7.47 10.78a.75.75 0 0 0 1.06 0l3.75-3.75a.75.75 0 0 0-1.06-1.06L8.75 8.44V1.75a.75.75 0 0 0-1.5 0v6.69L4.78 5.97a.75.75 0 0 0-1.06 1.06l3.75 3.75ZM3.75 13a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary ${btnClass} action-share" data-path="${escapeHtml(filePath)}" onclick="event.stopPropagation(); window.__shareFile && window.__shareFile('${escapedPath}')" title="Copy link">
|
<button class="btn btn-secondary ${btnClass} action-share" data-path="${escapeHtml(
|
||||||
|
filePath
|
||||||
|
)}" title="Copy link">
|
||||||
<svg viewBox="0 0 16 16" width="${iconSize}" height="${iconSize}" fill="currentColor">
|
<svg viewBox="0 0 16 16" width="${iconSize}" height="${iconSize}" fill="currentColor">
|
||||||
<path d="M7.775 3.275a.75.75 0 0 0 1.06 1.06l1.25-1.25a2 2 0 1 1 2.83 2.83l-2.5 2.5a2 2 0 0 1-2.83 0 .75.75 0 0 0-1.06 1.06 3.5 3.5 0 0 0 4.95 0l2.5-2.5a3.5 3.5 0 0 0-4.95-4.95l-1.25 1.25zm-.025 5.45a.75.75 0 0 0-1.06-1.06l-1.25 1.25a2 2 0 1 1-2.83-2.83l2.5-2.5a2 2 0 0 1 2.83 0 .75.75 0 0 0 1.06-1.06 3.5 3.5 0 0 0-4.95 0l-2.5 2.5a3.5 3.5 0 0 0 4.95 4.95l1.25-1.25z"/>
|
<path d="M7.775 3.275a.75.75 0 0 0 1.06 1.06l1.25-1.25a2 2 0 1 1 2.83 2.83l-2.5 2.5a2 2 0 0 1-2.83 0 .75.75 0 0 0-1.06 1.06 3.5 3.5 0 0 0 4.95 0l2.5-2.5a3.5 3.5 0 0 0-4.95-4.95l-1.25 1.25zm-.025 5.45a.75.75 0 0 0-1.06-1.06l-1.25 1.25a2 2 0 1 1-2.83-2.83l2.5-2.5a2 2 0 0 1 2.83 0 .75.75 0 0 0 1.06-1.06 3.5 3.5 0 0 0-4.95 0l-2.5 2.5a3.5 3.5 0 0 0 4.95 4.95l1.25-1.25z"/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -319,14 +391,41 @@ export function getActionButtonsHtml(filePath: string, small = false): string {
|
|||||||
* Setup global action handlers for download and share buttons
|
* Setup global action handlers for download and share buttons
|
||||||
*/
|
*/
|
||||||
export function setupActionHandlers(): void {
|
export function setupActionHandlers(): void {
|
||||||
// Expose functions globally for inline onclick handlers
|
if (actionHandlersReady) return;
|
||||||
(window as Window & { __downloadFile?: (path: string) => void; __shareFile?: (path: string) => void }).__downloadFile = async (path: string) => {
|
actionHandlersReady = true;
|
||||||
const success = await downloadFile(path);
|
|
||||||
showToast(success ? 'Download started!' : 'Download failed', success ? 'success' : 'error');
|
|
||||||
};
|
|
||||||
|
|
||||||
(window as Window & { __downloadFile?: (path: string) => void; __shareFile?: (path: string) => void }).__shareFile = async (path: string) => {
|
document.addEventListener(
|
||||||
const success = await shareFile(path);
|
"click",
|
||||||
showToast(success ? 'Link copied!' : 'Failed to copy link', success ? 'success' : 'error');
|
async (e) => {
|
||||||
};
|
const target = (e.target as HTMLElement).closest(
|
||||||
|
".action-download, .action-share"
|
||||||
|
) as HTMLElement | null;
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const path = target.dataset.path;
|
||||||
|
if (!path) return;
|
||||||
|
|
||||||
|
if (target.classList.contains("action-download")) {
|
||||||
|
const success = await downloadFile(path);
|
||||||
|
showToast(
|
||||||
|
success ? "Download started!" : "Download failed",
|
||||||
|
success ? "success" : "error"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await shareFile(path);
|
||||||
|
showToast(
|
||||||
|
success ? "Link copied!" : "Failed to copy link",
|
||||||
|
success ? "success" : "error"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let dropdownHandlersReady = false;
|
||||||
|
let actionHandlersReady = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user