// 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 = `
Categories:
All
${currentCategories.map(category => `
${this.formatComponentName(category)}
`).join('')}
`;
// 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 = 'Unknown filter
';
}
}
displayTemplates(grid) {
if (!this.componentsData || !this.componentsData.templates) {
grid.innerHTML = 'Loading templates...
';
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 = 'Loading plugins...
';
return;
}
// Clear the grid
grid.innerHTML = '';
// Get plugins from loaded components data
const plugins = this.componentsData.plugins;
if (plugins.length === 0) {
grid.innerHTML = 'No plugins available
';
return;
}
// Add marketplace setup notice
const marketplaceNotice = document.createElement('div');
marketplaceNotice.className = 'plugin-marketplace-notice';
marketplaceNotice.innerHTML = `
âšī¸
First Time Setup Required
Before installing any plugin, you need to add the marketplace to Claude Code:
/plugin marketplace add https://github.com/davila7/claude-code-templates
`;
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 = `
v${plugin.version}
đ§Š
${escapeHTML(this.formatComponentName(plugin.name))}
${escapeHTML(plugin.description)}
${plugin.commands > 0 ? `⥠${plugin.commands} ` : ''}
${plugin.agents > 0 ? `đ¤ ${plugin.agents} ` : ''}
${plugin.mcpServers > 0 ? `đ ${plugin.mcpServers} ` : ''}
View Details
Installation Command
${plugin.installCommand}
Copy Command
Included Components (${totalComponents})
${plugin.commands > 0 ? `
⥠${plugin.commands} Command${plugin.commands > 1 ? 's' : ''}
` : ''}
${plugin.agents > 0 ? `
đ¤ ${plugin.agents} Agent${plugin.agents > 1 ? 's' : ''}
` : ''}
${plugin.mcpServers > 0 ? `
đ ${plugin.mcpServers} MCP${plugin.mcpServers > 1 ? 's' : ''}
` : ''}
`;
return card;
}
displayComponents(grid, type) {
if (!this.componentsData) {
grid.innerHTML = 'Loading components...
';
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 || 'No components available
';
// 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 = `${escapeHTML(this.formatComponentName(categoryName))}
`;
// Create download badge if downloads data exists
const downloadBadge = component.downloads && component.downloads > 0 ?
`
${this.formatNumber(component.downloads)}
` : '';
// Create validation badge for agents with security data
const validationBadge = this.createValidationBadge(component.security);
return `
${downloadBadge}
${categoryLabel}
${config.icon}
${validationBadge}
${escapeHTML(this.formatComponentName(component.name))}
${component.type === 'mcp' ?
`
${escapeHTML(this.truncateDescription(component.description || 'MCP integration for enhanced development workflow', 80))}
` :
`
${escapeHTML(this.getComponentDescription(component))}
`
}
Installation Command
${escapeHTML(installCommand)}
Copy Command
đ View Details
Add to Stack
`;
}
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 ``;
}
// 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 = `đ¤ agents (${totalCounts.agents})`;
}
if (commandsBtn) {
commandsBtn.innerHTML = `⥠commands (${totalCounts.commands})`;
}
if (mcpsBtn) {
mcpsBtn.innerHTML = `đ mcps (${totalCounts.mcps})`;
}
if (settingsBtn) {
settingsBtn.innerHTML = `âī¸ settings (${totalCounts.settings})`;
}
if (hooksBtn) {
hooksBtn.innerHTML = `đĒ hooks (${totalCounts.hooks})`;
}
if (skillsBtn) {
skillsBtn.innerHTML = `NEW đ¨ skills (${totalCounts.skills})`;
}
if (templatesBtn) {
templatesBtn.innerHTML = `đĻ 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 `
Add New ${config.name}
${config.description}
`;
}
showError(message) {
const grid = document.getElementById('unifiedGrid');
if (grid) {
grid.innerHTML = `
`;
}
}
// 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 = `
Add New Template
Contribute a new language or framework to the community
`;
// 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 = `
${escapeHTML(displayName)}
${escapeHTML(template.description)}
Installation Command
${escapeHTML(template.installCommand)}
đ View Files
đ Copy Command
`;
// 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 = `
${languageData.comingSoon ? '
Coming Soon
' : ''}
${escapeHTML(displayName)}
${escapeHTML((languageData.description || '').substring(0, 120))}${(languageData.description || '').length > 120 ? '...' : ''}
Installation Options
${frameworkData.command}
đ View Files
đ Copy Command
`;
// 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 = '';
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 `
${displayName}
`;
}).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 = `
${template.description}
đĻ Installation
${template.installCommand}
Copy
đ Template Files (${template.files ? template.files.length : 0} files)
${template.files && template.files.length > 0 ? `
${template.files.map(file => `
đ
${file}
`).join('')}
` : '
No files listed for this template.
'}
`;
// 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 = `
Help expand Claude Code by contributing a new project template! Choose between contributing a new Language or a Framework :
đī¸ Contributing a New Language
Add support for a completely new programming language (e.g., Kotlin, PHP, Swift)
1
Create Language Folder
Create a new folder in cli-tool/templates/ with your language name:
mkdir cli-tool/templates/kotlin
Copy
2
Add Base Files
Add the essential files for your language. Required Claude Code files:
CLAUDE.md - Project documentation and Claude Code instructions
.mcp.json - MCP server configuration if needed
.claude/ - Claude Code configuration folder with agents, commands, and settings
Required structure:
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
3
Create Examples Folder (Optional)
If your language has popular frameworks, create an examples folder:
mkdir cli-tool/templates/kotlin/examples
Copy
⥠Contributing a Framework
Add a framework variation to an existing language (e.g., Spring Boot for Java)
1
Choose Existing Language
Navigate to an existing language's examples folder:
cd cli-tool/templates/javascript-typescript/examples
Copy
2
Create Framework Folder
Create a folder with your framework name:
mkdir nextjs-app
Copy
3
Add Framework Files
Add all necessary files for your framework setup. Required Claude Code files:
CLAUDE.md - Framework documentation and Claude Code instructions
.mcp.json - MCP server configuration for framework-specific tools
.claude/ - Claude Code configuration folder with framework-specific agents and commands
Required structure:
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
4
Test Your Template
Test your template installation:
npx claude-code-templates@latest --template=your-template-name --yes
Copy
5
Submit Pull Request
Submit your template contribution (replace with your actual language/framework folder):
git add cli-tool/templates/<your-language>/
Copy
git commit -m "feat: Add [language/framework] template"
Copy
`;
} else {
// Default modal for other component types
modalHTML = `
Help expand Claude Code by contributing a new ${config.name.toLowerCase()}! Follow these steps:
1
Create Your ${config.name}
Add your ${config.name.toLowerCase()} to a category folder in: cli-tool/components/${type}/<category>/
Structure should include:
${config.structure}
Example path: cli-tool/components/${type}/<category>/${config.example}.${(type === 'mcps' || type === 'settings' || type === 'hooks') ? 'json' : 'md'}
2
Follow the Pattern
Check existing ${type} in the repository to understand the structure and conventions.
3
Test Your Component
Ensure your ${config.name.toLowerCase()} works correctly with Claude Code.
cd cli-tool && npm test
Copy
4
Submit Pull Request
Submit your contribution with proper documentation (replace <category> with your actual category folder):
git add cli-tool/components/${type}/<category>/${config.example}.${(type === 'mcps' || type === 'settings' || type === 'hooks') ? 'json' : 'md'}
Copy
git commit -m "feat: Add ${config.example} ${config.name.toLowerCase()}"
Copy
`;
}
// 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 = `
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();
}