项目文件夹

文件
wehub-resource-sync bb5c75ce05
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:58 +08:00

2019 行
89 KiB
JavaScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
// Index Events - Handles events specific to index.html
// Global function to focus search input when clicking wrapper
function focusSearchInput() {
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.focus();
}
}
class IndexPageManager {
constructor() {
this.currentFilter = 'skills';
this.currentCategoryFilter = 'all';
this.currentSort = 'downloads'; // Default sort by downloads
this.templatesData = null;
this.componentsData = null;
this.availableCategories = {
agents: new Set(),
commands: new Set(),
mcps: new Set(),
settings: new Set(),
hooks: new Set(),
skills: new Set(),
templates: new Set(),
plugins: new Set()
};
// Pagination settings
this.currentPage = 1;
this.itemsPerPage = 24; // 3x3 grid
this.totalPages = 1;
// Framework icons mapping (from script.js)
this.FRAMEWORK_ICONS = {
// Languages
'common': 'devicon-gear-plain',
'javascript-typescript': 'devicon-javascript-plain',
'python': 'devicon-python-plain',
'ruby': 'devicon-ruby-plain',
'rust': 'devicon-rust-plain',
'go': 'devicon-go-plain',
// JavaScript/TypeScript frameworks
'react': 'devicon-react-original',
'vue': 'devicon-vuejs-plain',
'angular': 'devicon-angularjs-plain',
'node': 'devicon-nodejs-plain',
// Python frameworks
'django': 'devicon-django-plain',
'flask': 'devicon-flask-original',
'fastapi': 'devicon-fastapi-plain',
// Ruby frameworks
'rails': 'devicon-rails-plain',
'sinatra': 'devicon-ruby-plain',
// Default fallback
'default': 'devicon-devicon-plain'
};
}
async init() {
try {
// Setup event listeners first (they don't depend on data)
this.setupEventListeners();
// Show loading state
this.showLoadingState(true);
// Load all components and templates at once
await this.loadComponentsData();
await this.loadTemplatesData();
// Check if URL has a specific filter and update accordingly
const filterFromURL = this.getFilterFromURL();
if (filterFromURL && filterFromURL !== this.currentFilter) {
this.currentFilter = filterFromURL;
// Update active filter chip
document.querySelectorAll('.component-type-filters .filter-chip').forEach(btn => {
btn.classList.remove('active');
});
const activeBtn = document.querySelector(`.component-type-filters [data-filter="${filterFromURL}"]`);
if (activeBtn) {
activeBtn.classList.add('active');
}
}
// Update sort selector for initial filter
this.updateSortSelector();
// Display components
this.displayCurrentFilter();
// Show category filters for the current filter
if (typeof showCategoryFilters === 'function') {
showCategoryFilters(this.currentFilter);
}
} catch (error) {
console.error('Error initializing index page:', error);
this.showError('Failed to load data. Please refresh the page.');
} finally {
this.showLoadingState(false);
}
}
// Get filter from URL path
getFilterFromURL() {
const path = window.location.pathname;
const segments = path.split('/').filter(segment => segment);
// Check if first segment is a valid filter
const validFilters = ['agents', 'commands', 'settings', 'hooks', 'mcps', 'skills', 'templates', 'plugins'];
const firstSegment = segments[0];
if (firstSegment && validFilters.includes(firstSegment)) {
return firstSegment;
}
// Default to skills
return 'skills';
}
async loadTemplatesData() {
try {
// Templates are now loaded from components.json, not GitHub
this.templatesData = await window.dataLoader.loadTemplates();
// Update display if templates were found
if (this.templatesData && Object.keys(this.templatesData).length > 0) {
this.displayCurrentFilter();
}
} catch (error) {
console.warn('Templates not available in components.json:', error);
// Continue without templates - this is not critical
}
}
async loadComponentsData() {
try {
// Load all components at once - the performance issue was mostly due to GitHub fetching
// Now that we only use components.json, we can load all data safely
this.componentsData = await window.dataLoader.loadAllComponents();
this.collectAvailableCategories();
} catch (error) {
console.error('Error loading components:', error);
// Use fallback data
this.componentsData = window.dataLoader.getFallbackComponentData();
this.collectAvailableCategories();
}
}
// This method is no longer needed since we load all components at once
// Kept for backward compatibility but does nothing
async loadMoreComponentsInBackground() {
// No-op: All components are now loaded in the initial request
}
// Check if data object is empty
isDataEmpty(data) {
return !data || ((!data.agents || data.agents.length === 0) &&
(!data.commands || data.commands.length === 0) &&
(!data.mcps || data.mcps.length === 0) &&
(!data.settings || data.settings.length === 0) &&
(!data.hooks || data.hooks.length === 0));
}
// Show/hide loading state
showLoadingState(isLoading) {
const loadingElements = document.querySelectorAll('.loading-indicator, .loading-spinner');
const contentElements = document.querySelectorAll('#unifiedGrid, .filter-controls');
loadingElements.forEach(el => {
el.style.display = isLoading ? 'flex' : 'none';
});
contentElements.forEach(el => {
el.style.opacity = isLoading ? '0.7' : '1';
});
}
setupEventListeners() {
// Filter buttons
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const filter = e.target.dataset.filter;
this.setFilter(filter);
});
});
// Copy buttons
document.addEventListener('click', (e) => {
if (e.target.classList.contains('copy-btn')) {
const command = e.target.previousElementSibling.textContent;
this.copyToClipboard(command);
}
});
// Card flip functionality for template cards
document.addEventListener('click', (e) => {
const card = e.target.closest('.template-card');
// Prevent flipping for "add new" cards or if a button is clicked
if (card && !card.classList.contains('add-template-card') && !e.target.closest('button')) {
card.classList.toggle('flipped');
}
});
// ESC key to close all flipped cards
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
document.querySelectorAll('.template-card.flipped').forEach(card => {
card.classList.remove('flipped');
});
}
});
}
setFilter(filter) {
this.currentFilter = filter;
this.currentPage = 1; // Reset to first page when changing filter
this.currentCategoryFilter = 'all'; // Reset category filter to show all items
// Update active button
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.filter === filter);
});
// Update sort selector options based on filter
this.updateSortSelector();
this.displayCurrentFilter();
// Show category filters for the new filter
if (typeof showCategoryFilters === 'function') {
showCategoryFilters(filter);
}
}
// Update sort selector based on current filter
updateSortSelector() {
const sortSelector = document.getElementById('sortSelector');
if (!sortSelector) return;
const currentValue = sortSelector.value;
if (this.currentFilter === 'agents') {
// Add "Verified" option for agents if it doesn't exist
const verifiedOption = Array.from(sortSelector.options).find(opt => opt.value === 'verified');
if (!verifiedOption) {
const option = document.createElement('option');
option.value = 'verified';
option.textContent = 'Verified First';
sortSelector.insertBefore(option, sortSelector.options[0]);
}
} else {
// Remove "Verified" option for other filters
const verifiedOption = Array.from(sortSelector.options).find(opt => opt.value === 'verified');
if (verifiedOption) {
verifiedOption.remove();
// Reset to downloads if verified was selected
if (currentValue === 'verified') {
sortSelector.value = 'downloads';
this.currentSort = 'downloads';
}
}
}
}
// Set category filter
setCategoryFilter(category) {
this.currentCategoryFilter = category;
this.currentPage = 1; // Reset to first page when changing category
// Update category filter buttons
document.querySelectorAll('.category-filter-btn').forEach(btn => {
btn.classList.remove('active');
});
const targetBtn = document.querySelector(`[data-category="${category}"]`);
if (targetBtn) {
targetBtn.classList.add('active');
}
// Regenerate the component display
this.displayCurrentFilter();
}
// Update category sub-filters in the unified-filter-bar
updateCategorySubFilters() {
const unifiedFilterBar = document.querySelector('.unified-filter-bar');
if (!unifiedFilterBar) return;
// Remove existing category filters
const existingCategoryFilters = unifiedFilterBar.querySelector('.category-filter-row');
if (existingCategoryFilters) {
existingCategoryFilters.remove();
}
// Get categories for current filter type
const currentCategories = Array.from(this.availableCategories[this.currentFilter] || []).sort();
if (currentCategories.length <= 1) {
// Don't show sub-filters if there's only one category or none
return;
}
// Create category filter row
const categoryFilterRow = document.createElement('div');
categoryFilterRow.className = 'category-filter-row';
categoryFilterRow.innerHTML = `
<div class="category-filter-label">Categories:</div>
<div class="category-filter-buttons">
<button class="category-filter-btn ${this.currentCategoryFilter === 'all' ? 'active' : ''}"
data-category="all">
All
</button>
${currentCategories.map(category => `
<button class="category-filter-btn ${this.currentCategoryFilter === category ? 'active' : ''}"
data-category="${category}">
${this.formatComponentName(category)}
</button>
`).join('')}
</div>
`;
// Add click event listeners
categoryFilterRow.querySelectorAll('.category-filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
this.setCategoryFilter(btn.getAttribute('data-category'));
});
});
// Append to unified filter bar
unifiedFilterBar.appendChild(categoryFilterRow);
}
// Get filtered components based on current filter and category filter
getFilteredComponents(type) {
if (!type) type = this.currentFilter;
if (type === 'templates') {
return [];
}
let components = this.componentsData[type] || [];
// Apply category filter if not 'all'
if (this.currentCategoryFilter !== 'all') {
components = components.filter(component => {
const category = component.category || 'general';
return category === this.currentCategoryFilter;
});
}
// Apply sorting
components = this.sortComponents(components);
return components;
}
// Sort components based on current sort option
sortComponents(components) {
const sortedComponents = [...components]; // Create a copy to avoid mutating original
if (this.currentSort === 'downloads') {
// Sort by downloads (descending) - components with no downloads go to the end
sortedComponents.sort((a, b) => {
const downloadsA = a.downloads || 0;
const downloadsB = b.downloads || 0;
return downloadsB - downloadsA;
});
} else if (this.currentSort === 'alphabetical') {
// Sort alphabetically by name
sortedComponents.sort((a, b) => {
const nameA = (a.name || '').toLowerCase();
const nameB = (b.name || '').toLowerCase();
return nameA.localeCompare(nameB);
});
} else if (this.currentSort === 'verified') {
// Sort by verified status first (100% score), then by downloads
sortedComponents.sort((a, b) => {
// Check if component has 100% validation score
const aVerified = a.security && a.security.validated && a.security.score === 100 && a.security.valid;
const bVerified = b.security && b.security.validated && b.security.score === 100 && b.security.valid;
// Verified components come first
if (aVerified && !bVerified) return -1;
if (!aVerified && bVerified) return 1;
// If both verified or both not verified, sort by downloads
const downloadsA = a.downloads || 0;
const downloadsB = b.downloads || 0;
return downloadsB - downloadsA;
});
}
return sortedComponents;
}
// Handle sort change from the dropdown
handleSortChange(sortValue) {
this.currentSort = sortValue;
this.currentPage = 1; // Reset to first page when changing sort
this.displayCurrentFilter();
}
// Collect available categories from loaded components
collectAvailableCategories() {
// Reset categories
this.availableCategories.agents.clear();
this.availableCategories.commands.clear();
this.availableCategories.mcps.clear();
this.availableCategories.settings.clear();
this.availableCategories.hooks.clear();
this.availableCategories.skills.clear();
this.availableCategories.templates.clear();
// Collect categories from each component type
if (this.componentsData.agents && Array.isArray(this.componentsData.agents)) {
this.componentsData.agents.forEach(component => {
const category = component.category || 'general';
this.availableCategories.agents.add(category);
});
}
if (this.componentsData.commands && Array.isArray(this.componentsData.commands)) {
this.componentsData.commands.forEach(component => {
const category = component.category || 'general';
this.availableCategories.commands.add(category);
});
}
if (this.componentsData.mcps && Array.isArray(this.componentsData.mcps)) {
this.componentsData.mcps.forEach(component => {
const category = component.category || 'general';
this.availableCategories.mcps.add(category);
});
}
if (this.componentsData.settings && Array.isArray(this.componentsData.settings)) {
this.componentsData.settings.forEach(component => {
const category = component.category || 'general';
this.availableCategories.settings.add(category);
});
}
if (this.componentsData.hooks && Array.isArray(this.componentsData.hooks)) {
this.componentsData.hooks.forEach(component => {
const category = component.category || 'general';
this.availableCategories.hooks.add(category);
});
}
if (this.componentsData.skills && Array.isArray(this.componentsData.skills)) {
this.componentsData.skills.forEach(component => {
const category = component.category || 'general';
this.availableCategories.skills.add(category);
});
}
// Collect categories from templates (use language as category for language templates)
if (this.componentsData.templates && Array.isArray(this.componentsData.templates)) {
this.componentsData.templates.forEach(template => {
if (template.subtype === 'language') {
this.availableCategories.templates.add(template.name);
} else if (template.subtype === 'framework' && template.language) {
this.availableCategories.templates.add(template.language);
}
});
}
}
displayCurrentFilter() {
const grid = document.getElementById('unifiedGrid');
if (!grid) return;
// Set proper grid class based on current filter
if (this.currentFilter === 'templates') {
grid.className = 'unified-grid templates-mode';
} else {
grid.className = 'unified-grid components-mode';
}
// Update filter button counts
this.updateFilterCounts();
switch (this.currentFilter) {
case 'templates':
this.displayTemplates(grid);
break;
case 'plugins':
this.displayPlugins(grid);
break;
case 'agents':
case 'commands':
case 'mcps':
case 'settings':
case 'hooks':
case 'skills':
this.displayComponents(grid, this.currentFilter);
break;
default:
grid.innerHTML = '<div class="error">Unknown filter</div>';
}
}
displayTemplates(grid) {
if (!this.componentsData || !this.componentsData.templates) {
grid.innerHTML = '<div class="loading">Loading templates...</div>';
return;
}
// Update category sub-filters for templates
this.updateCategorySubFilters();
// Clear the grid
grid.innerHTML = '';
// Add the "Add New Template" card first
const addTemplateCard = this.createAddTemplateCard();
grid.appendChild(addTemplateCard);
// Filter templates based on category selection
let filteredTemplates = this.componentsData.templates;
if (this.currentCategoryFilter !== 'all') {
filteredTemplates = this.componentsData.templates.filter(template => {
if (template.subtype === 'language') {
return template.name === this.currentCategoryFilter;
} else if (template.subtype === 'framework') {
return template.language === this.currentCategoryFilter;
}
return false;
});
}
// Apply sorting to templates
filteredTemplates = this.sortComponents(filteredTemplates);
// Create template cards from the filtered list
filteredTemplates.forEach(template => {
const templateCard = this.createTemplateCardFromJSON(template);
grid.appendChild(templateCard);
});
}
displayPlugins(grid) {
if (!this.componentsData || !this.componentsData.plugins) {
grid.innerHTML = '<div class="loading">Loading plugins...</div>';
return;
}
// Clear the grid
grid.innerHTML = '';
// Get plugins from loaded components data
const plugins = this.componentsData.plugins;
if (plugins.length === 0) {
grid.innerHTML = '<div class="no-data">No plugins available</div>';
return;
}
// Add marketplace setup notice
const marketplaceNotice = document.createElement('div');
marketplaceNotice.className = 'plugin-marketplace-notice';
marketplaceNotice.innerHTML = `
<div class="notice-icon">️</div>
<div class="notice-content">
<h4>First Time Setup Required</h4>
<p>Before installing any plugin, you need to add the marketplace to Claude Code:</p>
<div class="notice-command">
<code>/plugin marketplace add https://github.com/davila7/claude-code-templates</code>
<button class="notice-copy-btn" onclick="copyToClipboard('/plugin marketplace add https://github.com/davila7/claude-code-templates'); event.stopPropagation();" title="Copy command">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
</svg>
</button>
</div>
<p class="notice-footer">After adding the marketplace, you can install any plugin below.</p>
</div>
`;
grid.appendChild(marketplaceNotice);
// Create plugin cards
plugins.forEach(plugin => {
const pluginCard = this.createPluginCard(plugin);
grid.appendChild(pluginCard);
});
}
createPluginCard(plugin) {
const card = document.createElement('div');
card.className = 'template-card plugin-card';
// Count components
const totalComponents = (plugin.commands || 0) + (plugin.agents || 0) + (plugin.mcpServers || 0);
card.innerHTML = `
<div class="card-inner">
<div class="card-front">
<div class="plugin-content-wrapper">
<div class="plugin-version">v${plugin.version}</div>
<div class="framework-logo" style="color: #a855f7">
<span class="component-icon">🧩</span>
</div>
<h3 class="template-title">${escapeHTML(this.formatComponentName(plugin.name))}</h3>
<p class="template-description">${escapeHTML(plugin.description)}</p>
<div class="plugin-stats">
${plugin.commands > 0 ? `<span class="plugin-stat"><span class="stat-icon">⚡</span>${plugin.commands}</span>` : ''}
${plugin.agents > 0 ? `<span class="plugin-stat"><span class="stat-icon">🤖</span>${plugin.agents}</span>` : ''}
${plugin.mcpServers > 0 ? `<span class="plugin-stat"><span class="stat-icon">🔌</span>${plugin.mcpServers}</span>` : ''}
</div>
</div>
<button class="plugin-view-details-btn" onclick="window.location.href='/plugin/${plugin.name}'; event.stopPropagation();">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"/>
</svg>
View Details
</button>
</div>
<div class="card-back">
<div class="command-display">
<h3>Installation Command</h3>
<div class="command-code-container">
<div class="command-code">${plugin.installCommand}</div>
<button class="copy-overlay-btn" onclick="copyToClipboard('${plugin.installCommand.replace(/'/g, "\\'")}'); event.stopPropagation();" title="Copy command">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
</svg>
Copy Command
</button>
</div>
<div class="plugin-components-list">
<h4>Included Components (${totalComponents})</h4>
${plugin.commands > 0 ? `<div class="component-type-item">⚡ ${plugin.commands} Command${plugin.commands > 1 ? 's' : ''}</div>` : ''}
${plugin.agents > 0 ? `<div class="component-type-item">🤖 ${plugin.agents} Agent${plugin.agents > 1 ? 's' : ''}</div>` : ''}
${plugin.mcpServers > 0 ? `<div class="component-type-item">🔌 ${plugin.mcpServers} MCP${plugin.mcpServers > 1 ? 's' : ''}</div>` : ''}
</div>
</div>
</div>
</div>
`;
return card;
}
displayComponents(grid, type) {
if (!this.componentsData) {
grid.innerHTML = '<div class="loading">Loading components...</div>';
return;
}
// Update category sub-filters in the unified-filter-bar
this.updateCategorySubFilters();
const allComponents = this.getFilteredComponents(type);
// Calculate pagination
const totalItems = allComponents.length + 1; // +1 for "Add New" card
this.totalPages = Math.ceil(totalItems / this.itemsPerPage);
// Get components for current page
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
const endIndex = startIndex + this.itemsPerPage;
let html = '';
let itemsToShow = [];
// Add "Add New" card to the beginning
itemsToShow.push({ type: 'add-new', data: type });
// Add components
allComponents.forEach(component => {
itemsToShow.push({ type: 'component', data: component });
});
// Get items for current page
const pageItems = itemsToShow.slice(startIndex, endIndex);
// Generate HTML for page items
pageItems.forEach(item => {
if (item.type === 'add-new') {
html += this.createAddComponentCard(item.data);
} else {
html += this.generateComponentCard(item.data);
}
});
// Create pagination controls
const paginationHTML = this.createPaginationControls();
grid.innerHTML = html || '<div class="no-data">No components available</div>';
// Add pagination after the grid
this.updatePagination(paginationHTML);
}
generateComponentCard(component) {
// Generate install command - remove .md extension from path
let componentPath = component.path || component.name;
// Remove .md or .json extensions from path
if (componentPath.endsWith('.md') || componentPath.endsWith('.json')) {
componentPath = componentPath.replace(/\.(md|json)$/, '');
}
if (componentPath.endsWith('.json')) {
componentPath = componentPath.replace(/\.json$/, '');
}
const installCommand = `npx claude-code-templates@latest --${component.type}=${componentPath} --yes`;
const typeConfig = {
agent: { icon: '🤖', color: '#ff6b6b' },
command: { icon: '⚡', color: '#4ecdc4' },
mcp: { icon: '🔌', color: '#45b7d1' },
setting: { icon: '⚙️', color: '#9c88ff' },
hook: { icon: '🪝', color: '#ff8c42' },
skill: { icon: '🎨', color: '#f59e0b' }
};
const config = typeConfig[component.type];
// Escape quotes and special characters for onclick attributes
const escapedType = component.type.replace(/'/g, "\\'");
const escapedName = (component.name || '').replace(/'/g, "\\'");
const escapedPath = (component.path || component.name || '').replace(/'/g, "\\'");
const escapedCategory = (component.category || 'general').replace(/'/g, "\\'");
const escapedCommand = installCommand.replace(/'/g, "\\'");
// Create category label (use "General" if no category)
const categoryName = component.category || 'general';
const categoryLabel = `<div class="category-label">${escapeHTML(this.formatComponentName(categoryName))}</div>`;
// Create download badge if downloads data exists
const downloadBadge = component.downloads && component.downloads > 0 ?
`<div class="download-badge" title="${component.downloads} downloads">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
<path d="M5 20h14v-2H5v2zM19 9h-4V3H9v6H5l7 7 7-7z"/>
</svg>
${this.formatNumber(component.downloads)}
</div>` : '';
// Create validation badge for agents with security data
const validationBadge = this.createValidationBadge(component.security);
return `
<div class="template-card" data-type="${component.type}">
<div class="card-inner">
<div class="card-front">
${downloadBadge}
${categoryLabel}
<div class="framework-logo" style="color: ${config.color}">
<span class="component-icon">${config.icon}</span>
${validationBadge}
</div>
<h3 class="template-title">${escapeHTML(this.formatComponentName(component.name))}</h3>
${component.type === 'mcp' ?
`<p class="template-description">${escapeHTML(this.truncateDescription(component.description || 'MCP integration for enhanced development workflow', 80))}</p>` :
`<p class="template-description">${escapeHTML(this.getComponentDescription(component))}</p>`
}
</div>
<div class="card-back">
<div class="command-display">
<h3>Installation Command</h3>
<div class="command-code-container">
<div class="command-code">${escapeHTML(installCommand)}</div>
<button class="copy-overlay-btn" onclick="copyToClipboard('${escapedCommand}'); event.stopPropagation();" title="Copy command">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
</svg>
Copy Command
</button>
</div>
<div class="card-actions">
<button class="view-files-btn" onclick="showComponentDetails('${escapedType}', '${escapedName}', '${escapedPath}', '${escapedCategory}')">
📁 View Details
</button>
<button class="add-to-cart-btn"
data-type="${component.type}s"
data-path="${componentPath}"
onclick="handleAddToCart('${escapedName}', '${componentPath}', '${component.type}s', '${escapedCategory}', this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M19,7H18V6A2,2 0 0,0 16,4H8A2,2 0 0,0 6,6V7H5A1,1 0 0,0 4,8A1,1 0 0,0 5,9H6V19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V9H19A1,1 0 0,0 20,8A1,1 0 0,0 19,7M8,6H16V7H8V6M16,19H8V9H16V19Z"/>
</svg>
Add to Stack
</button>
</div>
</div>
</div>
</div>
</div>
`;
}
copyToClipboard(text) {
// Use the global function from utils.js
if (typeof window.copyToClipboard === 'function') {
window.copyToClipboard(text);
} else {
copyToClipboard(text);
}
}
showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.textContent = message;
notification.className = `notification notification-${type}`;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'};
color: white;
padding: 12px 24px;
border-radius: 6px;
z-index: 10000;
font-family: system-ui, -apple-system, sans-serif;
animation: slideIn 0.3s ease;
`;
document.body.appendChild(notification);
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 3000);
}
formatComponentName(name) {
return name.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
}
formatNumber(num) {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
}
truncateDescription(description, maxLength = 80) {
if (!description) return '';
if (description.length <= maxLength) return description;
return description.substring(0, maxLength).trim() + '...';
}
getComponentDescription(component) {
let description = '';
if (component.description) {
description = component.description;
} else if (component.content) {
// Try to extract description from frontmatter
const descMatch = component.content.match(/description:\s*(.+?)(?:\n|$)/);
if (descMatch) {
description = descMatch[1].trim().replace(/^["']|["']$/g, '');
} else {
// Use first paragraph if no frontmatter description
const lines = component.content.split('\n');
const firstParagraph = lines.find(line => line.trim() && !line.startsWith('---') && !line.startsWith('#'));
if (firstParagraph) {
description = firstParagraph.trim();
}
}
}
if (!description) {
description = `A ${component.type} component for Claude Code.`;
}
// Truncate description to max 120 characters for proper card display
if (description.length > 120) {
description = description.substring(0, 117) + '...';
}
return description;
}
/**
* Create validation badge HTML - Only for perfect 100% score
*/
createValidationBadge(validation) {
if (!validation || !validation.validated) return '';
const score = validation.score || 0;
const isValid = validation.valid;
// ONLY show badge for perfect score (100%)
if (score === 100 && isValid) {
return `<div class="verified-checkmark" title="100% Validated - Perfect Security Score">
<svg width="20" height="20" viewBox="0 0 24 24" fill="#1DA1F2">
<path d="M22.5 12.5c0-1.58-.875-2.95-2.148-3.6.154-.435.238-.905.238-1.4 0-2.21-1.71-3.998-3.818-3.998-.47 0-.92.084-1.336.25C14.818 2.415 13.51 1.5 12 1.5s-2.816.917-3.437 2.25c-.415-.165-.866-.25-1.336-.25-2.11 0-3.818 1.79-3.818 4 0 .494.083.964.237 1.4-1.272.65-2.147 2.018-2.147 3.6 0 1.495.782 2.798 1.942 3.486-.02.17-.032.34-.032.514 0 2.21 1.708 4 3.818 4 .47 0 .92-.086 1.335-.25.62 1.334 1.926 2.25 3.437 2.25 1.512 0 2.818-.916 3.437-2.25.415.163.865.248 1.336.248 2.11 0 3.818-1.79 3.818-4 0-.174-.012-.344-.033-.513 1.158-.687 1.943-1.99 1.943-3.484zm-6.616-3.334l-4.334 6.5c-.145.217-.382.334-.625.334-.143 0-.288-.04-.416-.126l-.115-.094-2.415-2.415c-.293-.293-.293-.768 0-1.06s.768-.294 1.06 0l1.77 1.767 3.825-5.74c.23-.345.696-.436 1.04-.207.346.23.44.696.21 1.04z"/>
</svg>
</div>`;
}
// Don't show anything for scores below 100
return '';
}
// Update filter button counts
updateFilterCounts() {
// Get accurate total counts from data loader (includes full data counts)
const totalCounts = window.dataLoader.getTotalCounts();
if (!totalCounts) return;
// Update each filter button with accurate total count
const agentsBtn = document.querySelector('[data-filter="agents"]');
const commandsBtn = document.querySelector('[data-filter="commands"]');
const mcpsBtn = document.querySelector('[data-filter="mcps"]');
const settingsBtn = document.querySelector('[data-filter="settings"]');
const hooksBtn = document.querySelector('[data-filter="hooks"]');
const skillsBtn = document.querySelector('[data-filter="skills"]');
const templatesBtn = document.querySelector('[data-filter="templates"]');
if (agentsBtn) {
agentsBtn.innerHTML = `<span class="chip-icon">🤖</span>agents (${totalCounts.agents})`;
}
if (commandsBtn) {
commandsBtn.innerHTML = `<span class="chip-icon">⚡</span>commands (${totalCounts.commands})`;
}
if (mcpsBtn) {
mcpsBtn.innerHTML = `<span class="chip-icon">🔌</span>mcps (${totalCounts.mcps})`;
}
if (settingsBtn) {
settingsBtn.innerHTML = `<span class="chip-icon">⚙️</span>settings (${totalCounts.settings})`;
}
if (hooksBtn) {
hooksBtn.innerHTML = `<span class="chip-icon">🪝</span>hooks (${totalCounts.hooks})`;
}
if (skillsBtn) {
skillsBtn.innerHTML = `<span class="new-label">NEW</span><span class="chip-icon">🎨</span>skills (${totalCounts.skills})`;
}
if (templatesBtn) {
templatesBtn.innerHTML = `<span class="chip-icon">📦</span>templates (${totalCounts.templates})`;
}
}
// Create Add Component card
createAddComponentCard(type) {
const typeConfig = {
agents: {
icon: '🤖',
name: 'Agent',
description: 'Create a new AI specialist agent',
color: '#ff6b6b'
},
commands: {
icon: '⚡',
name: 'Command',
description: 'Add a custom slash command',
color: '#4ecdc4'
},
mcps: {
icon: '🔌',
name: 'MCP',
description: 'Build a Model Context Protocol integration',
color: '#45b7d1'
},
settings: {
icon: '⚙️',
name: 'Setting',
description: 'Configure Claude Code behavior',
color: '#9c88ff'
},
hooks: {
icon: '🪝',
name: 'Hook',
description: 'Automate tool execution workflows',
color: '#ff8c42'
},
skills: {
icon: '🎨',
name: 'Skill',
description: 'Add modular capabilities with progressive disclosure',
color: '#f59e0b'
}
};
const config = typeConfig[type];
if (!config) return '';
return `
<div class="template-card add-template-card add-component-card" onclick="showComponentContributeModal('${type}')">
<div class="card-inner">
<div class="card-front">
<div class="framework-logo" style="color: ${config.color}">
<svg width="32" height="32" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/>
</svg>
</div>
<h3 class="template-title">Add New ${config.name}</h3>
<p class="template-description">${config.description}</p>
</div>
</div>
</div>
`;
}
showError(message) {
const grid = document.getElementById('unifiedGrid');
if (grid) {
grid.innerHTML = `
<div class="error-message">
<h3>Error</h3>
<p>${message}</p>
<button onclick="location.reload()" class="btn-primary">Retry</button>
</div>
`;
}
}
// Get framework icon from mapping
getFrameworkIcon(framework) {
return this.FRAMEWORK_ICONS[framework] || this.FRAMEWORK_ICONS['default'];
}
// Create Add Template card
createAddTemplateCard() {
const card = document.createElement('div');
card.className = 'template-card add-template-card';
card.innerHTML = `
<div class="card-inner">
<div class="card-front">
<div class="framework-logo">
<svg width="32" height="32" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/>
</svg>
</div>
<h3 class="template-title">Add New Template</h3>
<p class="template-description">Contribute a new language or framework to the community</p>
</div>
</div>
`;
// Add click handler
card.addEventListener('click', () => {
showComponentContributeModal('templates');
});
return card;
}
// Create template card from JSON structure
createTemplateCardFromJSON(template) {
const card = document.createElement('div');
card.className = 'template-card';
// Determine the icon based on template name/type
const icon = this.getFrameworkIcon(template.name);
// Create the display name
const displayName = template.subtype === 'framework'
? `${template.language}/${template.name}`
: template.name;
card.innerHTML = `
<div class="card-inner">
<div class="card-front">
<div class="framework-logo">
<i class="${icon} colored"></i>
</div>
<h3 class="template-title">${escapeHTML(displayName)}</h3>
<p class="template-description">${escapeHTML(template.description)}</p>
</div>
<div class="card-back">
<div class="command-display">
<h3>Installation Command</h3>
<div class="command-code">${escapeHTML(template.installCommand)}</div>
<div class="action-buttons">
<button class="view-files-btn" onclick="showTemplateDetails('${template.id}', '${template.name}', '${template.subtype}')">
📁 View Files
</button>
<button class="copy-command-btn" onclick="copyToClipboard('${template.installCommand}')">
📋 Copy Command
</button>
</div>
</div>
</div>
</div>
`;
// Card flip is handled by global event listener in setupEventListeners
return card;
}
// Create individual template card (legacy method, keep for compatibility)
createTemplateCard(languageKey, languageData, frameworkKey, frameworkData) {
const card = document.createElement('div');
card.className = `template-card ${languageData.comingSoon ? 'coming-soon' : ''}`;
const displayName = frameworkKey === 'none' ?
frameworkData.name :
`${languageData.name.split('/')[0]}/${frameworkData.name}`;
card.innerHTML = `
<div class="card-inner">
<div class="card-front">
${languageData.comingSoon ? '<div class="coming-soon-badge">Coming Soon</div>' : ''}
<div class="framework-logo">
<i class="${frameworkData.icon} colored"></i>
</div>
<h3 class="template-title">${escapeHTML(displayName)}</h3>
<p class="template-description">${escapeHTML((languageData.description || '').substring(0, 120))}${(languageData.description || '').length > 120 ? '...' : ''}</p>
</div>
<div class="card-back">
<div class="command-display">
<h3>Installation Options</h3>
<div class="command-code">${frameworkData.command}</div>
<div class="action-buttons">
<button class="view-files-btn" onclick="showInstallationFiles('${languageKey}', '${frameworkKey}', '${displayName}')">
📁 View Files
</button>
<button class="copy-command-btn" onclick="copyToClipboard('${frameworkData.command}')">
📋 Copy Command
</button>
</div>
</div>
</div>
</div>
`;
// Add click handler for card flip (only if not coming soon)
if (!languageData.comingSoon) {
card.addEventListener('click', (e) => {
// Don't flip if clicking on buttons
if (!e.target.closest('button')) {
card.classList.toggle('flipped');
}
});
}
return card;
}
// Fetch templates configuration from GitHub
async fetchTemplatesConfig() {
const GITHUB_CONFIG = {
owner: 'davila7',
repo: 'claude-code-templates',
branch: 'main',
templatesPath: 'cli-tool/src/templates.js'
};
try {
const url = `https://raw.githubusercontent.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/${GITHUB_CONFIG.branch}/${GITHUB_CONFIG.templatesPath}?t=${Date.now()}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const templateFileContent = await response.text();
this.templatesData = this.parseTemplatesConfig(templateFileContent);
return this.templatesData;
} catch (error) {
console.error('Error fetching templates:', error);
throw error;
}
}
// Parse templates configuration
parseTemplatesConfig(fileContent) {
try {
const configMatch = fileContent.match(/const TEMPLATES_CONFIG = ({[\s\S]*?});/);
if (!configMatch) {
throw new Error('TEMPLATES_CONFIG not found in file');
}
let configString = configMatch[1];
configString = configString.replace(/'/g, '"');
configString = configString.replace(/(\w+):/g, '"$1":');
configString = configString.replace(/,(\s*[}\]])/g, '$1');
return JSON.parse(configString);
} catch (error) {
console.error('Error parsing templates config:', error);
return null;
}
}
// Show contribute modal
showContributeModal() {
alert('Contribute modal would open here - this needs to be implemented with the full modal HTML from script.js');
}
// Create pagination controls
createPaginationControls() {
if (this.totalPages <= 1) return '';
let paginationHTML = '<div class="pagination-container">';
paginationHTML += '<div class="pagination">';
// Previous button
const prevDisabled = this.currentPage === 1 ? 'disabled' : '';
paginationHTML += `<button class="pagination-btn prev-btn ${prevDisabled}" onclick="goToPage(${this.currentPage - 1})" ${prevDisabled ? 'disabled' : ''}>`;
paginationHTML += '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>';
paginationHTML += '</button>';
// Page numbers
const startPage = Math.max(1, this.currentPage - 2);
const endPage = Math.min(this.totalPages, this.currentPage + 2);
if (startPage > 1) {
paginationHTML += `<button class="pagination-btn page-btn" onclick="goToPage(1)">1</button>`;
if (startPage > 2) {
paginationHTML += '<span class="pagination-dots">...</span>';
}
}
for (let i = startPage; i <= endPage; i++) {
const activeClass = i === this.currentPage ? 'active' : '';
paginationHTML += `<button class="pagination-btn page-btn ${activeClass}" onclick="goToPage(${i})">${i}</button>`;
}
if (endPage < this.totalPages) {
if (endPage < this.totalPages - 1) {
paginationHTML += '<span class="pagination-dots">...</span>';
}
paginationHTML += `<button class="pagination-btn page-btn" onclick="goToPage(${this.totalPages})">${this.totalPages}</button>`;
}
// Next button
const nextDisabled = this.currentPage === this.totalPages ? 'disabled' : '';
paginationHTML += `<button class="pagination-btn next-btn ${nextDisabled}" onclick="goToPage(${this.currentPage + 1})" ${nextDisabled ? 'disabled' : ''}>`;
paginationHTML += '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M10.02 6L8.61 7.41 13.19 12l-4.58 4.59L10.02 18l6-6z"/></svg>';
paginationHTML += '</button>';
paginationHTML += '</div>';
paginationHTML += `<div class="pagination-info">Page ${this.currentPage} of ${this.totalPages}</div>`;
paginationHTML += '</div>';
return paginationHTML;
}
// Navigate to specific page
goToPage(page) {
if (page < 1 || page > this.totalPages || page === this.currentPage) return;
this.currentPage = page;
this.displayCurrentFilter();
// Scroll to top of content grid
const contentGrid = document.getElementById('contentGrid');
if (contentGrid) {
contentGrid.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
// Update pagination controls
updatePagination(paginationHTML) {
// Remove any existing pagination containers from the entire document
const existingPaginations = document.querySelectorAll('.pagination-container');
existingPaginations.forEach(pagination => pagination.remove());
// Add new pagination if needed
if (this.totalPages > 1 && paginationHTML) {
const contentGrid = document.getElementById('contentGrid');
if (contentGrid) {
contentGrid.insertAdjacentHTML('afterend', paginationHTML);
}
}
}
}
// Global function for component details is now handled by modal-helpers.js
// Global function for copying is now handled by utils.js
// Global function for handling sort change (called from onchange)
function handleSortChange(sortValue) {
if (window.indexManager) {
window.indexManager.handleSortChange(sortValue);
}
}
// Global function for handling filter click with navigation
function handleFilterClick(event, filter) {
event.preventDefault(); // Prevent default link navigation
// If plugins filter, scroll to plugins section
if (filter === 'plugins') {
const contentGrid = document.getElementById('contentGrid');
if (contentGrid) {
contentGrid.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
setUnifiedFilter(filter);
}
// Global function for setting filter (called from onclick)
function setUnifiedFilter(filter) {
if (window.indexManager) {
window.indexManager.setFilter(filter);
}
// Update filter buttons - remove active from ALL filter buttons
document.querySelectorAll('.component-type-filters .filter-chip').forEach(btn => {
btn.classList.remove('active');
});
// Add active class only to the clicked filter button
const activeBtn = document.querySelector(`.component-type-filters [data-filter="${filter}"]`);
if (activeBtn) {
activeBtn.classList.add('active');
}
// Update URL with filter parameter
if (typeof updateURLWithFilter === 'function') {
updateURLWithFilter(filter);
}
// Show category filters for the selected component type
showCategoryFilters(filter);
}
// Function to show category filters based on component type
function showCategoryFilters(componentType) {
const categoryContainer = document.getElementById('componentCategories');
const categoryChips = document.getElementById('categoryChips');
if (!categoryContainer || !categoryChips) return;
// Get categories from actual component data
let categories = [];
if (window.dataLoader) {
const dataLoader = window.dataLoader;
switch(componentType) {
case 'agents':
const agents = dataLoader.getComponentsByType('agent');
categories = getUniqueCategories(agents);
break;
case 'commands':
const commands = dataLoader.getComponentsByType('command');
categories = getUniqueCategories(commands);
break;
case 'settings':
try {
categories = dataLoader.getSettingCategories ? dataLoader.getSettingCategories() : getUniqueCategories(dataLoader.getSettings());
} catch (e) {
const settings = dataLoader.getComponentsByType('setting') || dataLoader.getSettings();
categories = getUniqueCategories(settings);
}
break;
case 'hooks':
try {
categories = dataLoader.getHookCategories ? dataLoader.getHookCategories() : getUniqueCategories(dataLoader.getHooks());
} catch (e) {
const hooks = dataLoader.getComponentsByType('hook') || dataLoader.getHooks();
categories = getUniqueCategories(hooks);
}
break;
case 'mcps':
const mcps = dataLoader.getComponentsByType('mcp');
categories = getUniqueCategories(mcps);
break;
case 'skills':
const skills = dataLoader.getComponentsByType('skill');
categories = getUniqueCategories(skills);
break;
case 'templates':
const templates = dataLoader.getComponentsByType('template');
categories = getUniqueCategories(templates);
break;
case 'plugins':
// Plugins don't have categories
categories = [];
break;
default:
categories = [];
}
}
// Add "All" option at the beginning
if (categories.length > 0) {
categories.unshift('All');
}
if (categories.length > 0) {
categoryChips.innerHTML = categories.map((category, index) => {
const displayName = escapeHTML(formatCategoryName(category));
const safeCategory = escapeHTML(category.toLowerCase());
// Only the first item (All) should be active by default
const isActive = index === 0 ? 'active' : '';
return `
<button class="filter-chip ${isActive}" data-category="${safeCategory}" onclick="setCategoryFilter('${safeCategory}')">
${displayName}
</button>
`;
}).join('');
categoryContainer.style.display = 'block';
} else {
categoryContainer.style.display = 'none';
}
}
// Helper function to extract unique categories from component data
function getUniqueCategories(components) {
if (!components || !Array.isArray(components)) return [];
const categories = new Set();
components.forEach(component => {
if (component.category && component.category.trim() !== '') {
categories.add(component.category);
}
});
return Array.from(categories).sort();
}
// Helper function to format category names for display
function formatCategoryName(category) {
if (category === 'All') return 'All';
// Convert category names to proper case
return category
.split(/[-_\s]+/)
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
}
// Function to handle category filter selection
// Enhanced setCategoryFilter function that handles both old and new category systems
window.setCategoryFilter = function setCategoryFilter(category) {
// Handle the new category chips in #categoryChips
const categoryButtons = document.querySelectorAll('#categoryChips button.filter-chip');
if (categoryButtons.length > 0) {
categoryButtons.forEach((btn) => {
btn.classList.remove('active');
});
// Add active class only to the clicked button
const activeBtn = document.querySelector(`#categoryChips button[data-category="${category}"]`);
if (activeBtn) {
activeBtn.classList.add('active');
}
}
// Also call the existing IndexManager setCategoryFilter method for actual filtering
if (window.indexManager && window.indexManager.setCategoryFilter) {
window.indexManager.setCategoryFilter(category);
}
// Force re-render of the current filter to apply category filtering
if (window.indexManager && window.indexManager.displayCurrentFilter) {
window.indexManager.displayCurrentFilter();
}
}
// Global helper functions for template cards
function showInstallationFiles(languageKey, frameworkKey, displayName) {
alert(`Installation files for ${displayName} would be shown here.`);
}
// Global function for template details
function showTemplateDetails(templateId, templateName, subtype) {
if (!window.indexManager || !window.indexManager.componentsData) {
console.error('IndexManager or components data not available');
return;
}
// Find the template in the data
const template = window.indexManager.componentsData.templates.find(t => t.id === templateId);
if (!template) {
console.error('Template not found:', templateId);
return;
}
// Create modal to show template files
const modalHTML = `
<div class="modal-overlay" onclick="closeComponentModal()">
<div class="modal-content component-modal" onclick="event.stopPropagation()">
<div class="modal-header">
<div class="component-modal-title">
<span class="component-icon">📦</span>
<h3>${templateName} Template</h3>
</div>
<div class="component-type-badge" style="background-color: #f9a825;">TEMPLATE</div>
<button class="modal-close" onclick="closeComponentModal()">×</button>
</div>
<div class="modal-body">
<div class="component-details">
<div class="component-description">
${template.description}
</div>
<div class="installation-section">
<h4>📦 Installation</h4>
<div class="command-line">
<code>${template.installCommand}</code>
<button class="copy-btn" onclick="copyToClipboard('${template.installCommand}')">Copy</button>
</div>
</div>
<div class="component-content">
<h4>📁 Template Files (${template.files ? template.files.length : 0} files)</h4>
${template.files && template.files.length > 0 ? `
<div class="template-files-list">
${template.files.map(file => `
<div class="template-file-item">
<span class="file-icon">📄</span>
<span class="file-name">${file}</span>
</div>
`).join('')}
</div>
` : '<p>No files listed for this template.</p>'}
</div>
</div>
</div>
<div class="modal-actions">
<a href="https://github.com/davila7/claude-code-templates/tree/main/cli-tool/templates/${subtype === 'framework' ? `${template.language}/examples/${templateName}` : templateName}" target="_blank" class="github-folder-link">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.30.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
View on GitHub
</a>
</div>
</div>
</div>
`;
// Remove existing modal if present
const existingModal = document.querySelector('.modal-overlay');
if (existingModal) {
existingModal.remove();
}
// Add modal to body
document.body.insertAdjacentHTML('beforeend', modalHTML);
}
// Global function for setting category filter (called from onclick)
// setCategoryFilter function is now defined globally above as window.setCategoryFilter
// Show component contribute modal (copied from script.js)
function showComponentContributeModal(type) {
const typeConfig = {
agents: {
name: 'Agent',
description: 'AI specialist that handles specific development tasks',
example: 'python-testing-specialist',
structure: '- Agent metadata (name, description, color)\n- Core expertise areas\n- When to use guidelines\n- Code examples and patterns'
},
commands: {
name: 'Command',
description: 'Custom slash command for Claude Code',
example: 'optimize-bundle',
structure: '- Command description and usage\n- Task breakdown\n- Process steps\n- Best practices and examples'
},
mcps: {
name: 'MCP',
description: 'Model Context Protocol integration',
example: 'redis-integration',
structure: '- MCP server configuration\n- Connection parameters\n- Environment variables\n- Usage examples'
},
settings: {
name: 'Setting',
description: 'Claude Code configuration setting',
example: 'custom-model-config',
structure: '- Setting description\n- Configuration options\n- Environment variables\n- Usage examples and best practices'
},
hooks: {
name: 'Hook',
description: 'Automation hook for tool execution',
example: 'format-on-save',
structure: '- Hook description and trigger\n- Command to execute\n- PreToolUse or PostToolUse configuration\n- Error handling and examples'
},
templates: {
name: 'Template',
description: 'Project template with language or framework setup',
example: 'python or django-app',
structure: 'For Languages: Create folder with base files\nFor Frameworks: Add to examples/ subfolder with specific setup'
},
skills: {
name: 'Skill',
description: 'Interactive skill for specialized tasks',
example: 'data-visualization-skill',
structure: '- Skill description and purpose\n- Interactive prompts and parameters\n- Input/output specifications\n- Usage examples and best practices'
}
};
const config = typeConfig[type];
let modalHTML = '';
if (type === 'templates') {
// Special modal for templates
modalHTML = `
<div class="modal-overlay" onclick="closeComponentModal()">
<div class="modal-content contribute-modal" onclick="event.stopPropagation()">
<div class="modal-header">
<h3>📦 Contribute a New Template</h3>
<button class="modal-close" onclick="closeComponentModal()">×</button>
</div>
<div class="modal-body">
<div class="contribute-intro">
<p>Help expand Claude Code by contributing a new project template! Choose between contributing a new <strong>Language</strong> or a <strong>Framework</strong>:</p>
</div>
<div class="template-types">
<div class="template-type-section">
<h3>🏗️ Contributing a New Language</h3>
<p>Add support for a completely new programming language (e.g., Kotlin, PHP, Swift)</p>
<div class="contribute-steps">
<div class="contribute-step">
<div class="step-number-contrib">1</div>
<div class="step-content-contrib">
<h4>Create Language Folder</h4>
<p>Create a new folder in <code>cli-tool/templates/</code> with your language name:</p>
<div class="step-command">
<code>mkdir cli-tool/templates/kotlin</code>
<button class="copy-btn" onclick="copyToClipboard('mkdir cli-tool/templates/kotlin')">Copy</button>
</div>
</div>
</div>
<div class="contribute-step">
<div class="step-number-contrib">2</div>
<div class="step-content-contrib">
<h4>Add Base Files</h4>
<p>Add the essential files for your language. <strong>Required Claude Code files:</strong></p>
<div class="claude-files-info">
<ul>
<li><code>CLAUDE.md</code> - Project documentation and Claude Code instructions</li>
<li><code>.mcp.json</code> - MCP server configuration if needed</li>
<li><code>.claude/</code> - Claude Code configuration folder with agents, commands, and settings</li>
</ul>
</div>
<div class="component-structure">
<strong>Required structure:</strong>
<pre>cli-tool/templates/kotlin/
├── CLAUDE.md # Claude Code configuration
├── .mcp.json # MCP server configuration
└── .claude/ # Claude Code settings
├── agents/ # Language-specific agents
├── commands/ # Language-specific commands
└── settings.json # Claude settings
</pre>
</div>
</div>
</div>
<div class="contribute-step">
<div class="step-number-contrib">3</div>
<div class="step-content-contrib">
<h4>Create Examples Folder (Optional)</h4>
<p>If your language has popular frameworks, create an examples folder:</p>
<div class="step-command">
<code>mkdir cli-tool/templates/kotlin/examples</code>
<button class="copy-btn" onclick="copyToClipboard('mkdir cli-tool/templates/kotlin/examples')">Copy</button>
</div>
</div>
</div>
</div>
</div>
<div class="template-type-section">
<h3>⚡ Contributing a Framework</h3>
<p>Add a framework variation to an existing language (e.g., Spring Boot for Java)</p>
<div class="contribute-steps">
<div class="contribute-step">
<div class="step-number-contrib">1</div>
<div class="step-content-contrib">
<h4>Choose Existing Language</h4>
<p>Navigate to an existing language's examples folder:</p>
<div class="step-command">
<code>cd cli-tool/templates/javascript-typescript/examples</code>
<button class="copy-btn" onclick="copyToClipboard('cd cli-tool/templates/javascript-typescript/examples')">Copy</button>
</div>
</div>
</div>
<div class="contribute-step">
<div class="step-number-contrib">2</div>
<div class="step-content-contrib">
<h4>Create Framework Folder</h4>
<p>Create a folder with your framework name:</p>
<div class="step-command">
<code>mkdir nextjs-app</code>
<button class="copy-btn" onclick="copyToClipboard('mkdir nextjs-app')">Copy</button>
</div>
</div>
</div>
<div class="contribute-step">
<div class="step-number-contrib">3</div>
<div class="step-content-contrib">
<h4>Add Framework Files</h4>
<p>Add all necessary files for your framework setup. <strong>Required Claude Code files:</strong></p>
<div class="claude-files-info">
<ul>
<li><code>CLAUDE.md</code> - Framework documentation and Claude Code instructions</li>
<li><code>.mcp.json</code> - MCP server configuration for framework-specific tools</li>
<li><code>.claude/</code> - Claude Code configuration folder with framework-specific agents and commands</li>
</ul>
</div>
<div class="component-structure">
<strong>Required structure:</strong>
<pre>nextjs-app/
├── CLAUDE.md # Claude Code configuration
├── .mcp.json # MCP server configuration
└── .claude/ # Claude Code settings
├── agents/ # Framework-specific agents
├── commands/ # Framework-specific commands
└── settings.json # Claude settings
</pre>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="contribute-step">
<div class="step-number-contrib">4</div>
<div class="step-content-contrib">
<h4>Test Your Template</h4>
<p>Test your template installation:</p>
<div class="step-command">
<code>npx claude-code-templates@latest --template=your-template-name --yes</code>
<button class="copy-btn" onclick="copyToClipboard('npx claude-code-templates@latest --template=your-template-name --yes')">Copy</button>
</div>
</div>
</div>
<div class="contribute-step">
<div class="step-number-contrib">5</div>
<div class="step-content-contrib">
<h4>Submit Pull Request</h4>
<p>Submit your template contribution (replace with your actual language/framework folder):</p>
<div class="step-command">
<code>git add cli-tool/templates/&lt;your-language&gt;/</code>
<button class="copy-btn" onclick="copyToClipboard('git add cli-tool/templates/<your-language>/')">Copy</button>
</div>
<div class="step-command">
<code>git commit -m "feat: Add [language/framework] template"</code>
<button class="copy-btn" onclick="copyToClipboard('git commit -m \\"feat: Add [language/framework] template\\"')">Copy</button>
</div>
</div>
</div>
<div class="contribute-footer">
<div class="help-section">
<h4>Need Help?</h4>
<p>Check <a href="https://github.com/davila7/claude-code-templates/tree/main/cli-tool/templates" target="_blank">existing templates</a> for examples or open an issue on GitHub for guidance.</p>
</div>
</div>
</div>
</div>
</div>
`;
} else {
// Default modal for other component types
modalHTML = `
<div class="modal-overlay" onclick="closeComponentModal()">
<div class="modal-content contribute-modal" onclick="event.stopPropagation()">
<div class="modal-header">
<h3>📝 Contribute a New ${config.name}</h3>
<button class="modal-close" onclick="closeComponentModal()">×</button>
</div>
<div class="modal-body">
<div class="contribute-intro">
<p>Help expand Claude Code by contributing a new ${config.name.toLowerCase()}! Follow these steps:</p>
</div>
<div class="contribute-steps">
<div class="contribute-step">
<div class="step-number-contrib">1</div>
<div class="step-content-contrib">
<h4>Create Your ${config.name}</h4>
<p>Add your ${config.name.toLowerCase()} to a category folder in: <code>cli-tool/components/${type}/&lt;category&gt;/</code></p>
<div class="component-structure">
<strong>Structure should include:</strong>
<pre>${config.structure}</pre>
</div>
<div class="step-command">
<strong>Example path:</strong> <code>cli-tool/components/${type}/&lt;category&gt;/${config.example}.${(type === 'mcps' || type === 'settings' || type === 'hooks') ? 'json' : 'md'}</code>
</div>
</div>
</div>
<div class="contribute-step">
<div class="step-number-contrib">2</div>
<div class="step-content-contrib">
<h4>Follow the Pattern</h4>
<p>Check existing ${type} in the repository to understand the structure and conventions.</p>
<div class="step-command">
<a href="https://github.com/davila7/claude-code-templates/tree/main/cli-tool/components/${type}" target="_blank" class="github-folder-link">
📁 View existing ${type}
</a>
</div>
</div>
</div>
<div class="contribute-step">
<div class="step-number-contrib">3</div>
<div class="step-content-contrib">
<h4>Test Your Component</h4>
<p>Ensure your ${config.name.toLowerCase()} works correctly with Claude Code.</p>
<div class="step-command">
<code>cd cli-tool && npm test</code>
<button class="copy-btn" onclick="copyToClipboard('cd cli-tool && npm test')">Copy</button>
</div>
</div>
</div>
<div class="contribute-step">
<div class="step-number-contrib">4</div>
<div class="step-content-contrib">
<h4>Submit Pull Request</h4>
<p>Submit your contribution with proper documentation (replace &lt;category&gt; with your actual category folder):</p>
<div class="step-command">
<code>git add cli-tool/components/${type}/&lt;category&gt;/${config.example}.${(type === 'mcps' || type === 'settings' || type === 'hooks') ? 'json' : 'md'}</code>
<button class="copy-btn" onclick="copyToClipboard('git add cli-tool/components/${type}/<category>/${config.example}.${(type === 'mcps' || type === 'settings' || type === 'hooks') ? 'json' : 'md'}')">Copy</button>
</div>
<div class="step-command">
<code>git commit -m "feat: Add ${config.example} ${config.name.toLowerCase()}"</code>
<button class="copy-btn" onclick="copyToClipboard('git commit -m \\"feat: Add ${config.example} ${config.name.toLowerCase()}\\"')">Copy</button>
</div>
</div>
</div>
</div>
<div class="contribute-footer">
<div class="help-section">
<h4>Need Help?</h4>
<p>Check existing ${type} for examples or open an issue on GitHub for guidance.</p>
</div>
</div>
</div>
</div>
</div>
`;
}
// Remove existing modal if present
const existingModal = document.querySelector('.modal-overlay');
if (existingModal) {
existingModal.remove();
}
// Add modal to body
document.body.insertAdjacentHTML('beforeend', modalHTML);
// Add event listener for ESC key
const handleEscape = (e) => {
if (e.key === 'Escape') {
closeComponentModal();
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape);
}
// Global functions for templates functionality
function showInstallationFiles(languageKey, frameworkKey, displayName) {
alert(`Installation files for ${displayName} would be shown here`);
}
// Close modal (from script.js)
function closeModal() {
const modal = document.querySelector('.modal');
if (modal) {
modal.remove();
}
}
// Close component modal
function closeComponentModal() {
const modalOverlay = document.querySelector('.modal-overlay');
if (modalOverlay) {
modalOverlay.remove();
}
}
// Global pagination function (called from onclick)
function goToPage(page) {
if (window.indexManager) {
window.indexManager.goToPage(page);
}
}
// Clean component name by removing extensions and formatting
function getCleanComponentName(name) {
if (!name) {
return 'Unknown Component';
}
let cleanName = name;
// Remove .md extension if present
if (cleanName.endsWith('.md')) {
cleanName = cleanName.slice(0, -3);
}
// Remove .json extension if present
if (cleanName.endsWith('.json')) {
cleanName = cleanName.slice(0, -5);
}
// Convert kebab-case or snake_case to Title Case
cleanName = cleanName
.replace(/[-_]/g, ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
return cleanName;
}
// Handle Add to Cart button click
function handleAddToCart(name, path, type, category, buttonElement) {
// Prevent event propagation to avoid card flip
if (window.event) {
window.event.stopPropagation();
}
// Clean the component name for better display
const cleanName = getCleanComponentName(name);
const item = {
name: cleanName,
path: path,
category: category,
description: `${cleanName} - ${category}`
};
// Add to cart using the cart manager
const success = addToCart(item, type);
if (success) {
// Update button state
buttonElement.classList.add('added');
buttonElement.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M9,20.42L2.79,14.21L5.62,11.38L9,14.77L18.88,4.88L21.71,7.71L9,20.42Z"/>
</svg>
Added to Stack
`;
// Show a brief animation
buttonElement.style.transform = 'scale(0.95)';
setTimeout(() => {
buttonElement.style.transform = 'scale(1)';
}, 150);
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.indexManager = new IndexPageManager();
window.indexManager.init();
// Focus search input on page load and setup terminal cursor
setTimeout(() => {
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.focus();
setupTerminalCursor();
}
}, 300); // Small delay to ensure DOM is fully loaded
});
// Terminal cursor functionality
function setupTerminalCursor() {
const searchInput = document.getElementById('searchInput');
const cursor = document.getElementById('terminalCursor');
if (!searchInput || !cursor) return;
function updateCursorPosition() {
const promptWidth = 26; // Width of ">" prompt + extra space
// If input has text, position cursor at the end of the text
if (searchInput.value.length > 0) {
// Create a temporary span to measure text width
const temp = document.createElement('span');
temp.style.visibility = 'hidden';
temp.style.position = 'absolute';
temp.style.whiteSpace = 'pre';
temp.style.font = window.getComputedStyle(searchInput).font;
temp.textContent = searchInput.value;
document.body.appendChild(temp);
const textWidth = temp.getBoundingClientRect().width;
document.body.removeChild(temp);
cursor.style.left = `${promptWidth + textWidth + 2}px`;
} else {
// If input is empty, position cursor right after the prompt with space
cursor.style.left = `${promptWidth}px`;
}
}
// Update cursor position on input
searchInput.addEventListener('input', updateCursorPosition);
searchInput.addEventListener('focus', () => {
cursor.style.display = 'block';
updateCursorPosition();
});
searchInput.addEventListener('blur', () => {
cursor.style.display = 'none';
});
// Initial position
updateCursorPosition();
}