1916 lines
81 KiB
JavaScript
1916 lines
81 KiB
JavaScript
// Component Page JavaScript
|
|
class ComponentPageManager {
|
|
constructor() {
|
|
this.component = null;
|
|
this.dataLoader = null;
|
|
this.init();
|
|
}
|
|
|
|
async init() {
|
|
// Initialize data loader
|
|
this.dataLoader = window.dataLoader || new DataLoader();
|
|
|
|
// Load component from URL parameters
|
|
await this.loadComponentFromURL();
|
|
|
|
// Setup event listeners
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
async loadComponentFromURL() {
|
|
let componentType, componentName, componentPath;
|
|
|
|
// Check if we're using SEO-friendly URLs: /component/type/name
|
|
const pathParts = window.location.pathname.split('/').filter(part => part);
|
|
if (pathParts.length >= 3 && pathParts[0] === 'component') {
|
|
componentType = decodeURIComponent(pathParts[1]);
|
|
componentName = decodeURIComponent(pathParts[2]);
|
|
} else {
|
|
// Fallback to query parameters
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
componentType = urlParams.get('type');
|
|
componentName = urlParams.get('name');
|
|
componentPath = urlParams.get('path');
|
|
}
|
|
|
|
if (!componentType || (!componentName && !componentPath)) {
|
|
this.showError('Missing component parameters in URL');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Load all components data
|
|
const componentsData = await this.dataLoader.loadAllComponents();
|
|
|
|
if (!componentsData) {
|
|
throw new Error('Failed to load components data');
|
|
}
|
|
|
|
// Find the component
|
|
const categoryKey = componentType + 's'; // Convert 'agent' to 'agents'
|
|
const components = componentsData[categoryKey] || [];
|
|
|
|
let component = null;
|
|
|
|
// Try to find by name first, then by path
|
|
if (componentName) {
|
|
component = components.find(c => c.name === componentName);
|
|
|
|
// If not found, try to find by path that ends with the component name
|
|
if (!component) {
|
|
component = components.find(c => {
|
|
// Check if path ends with componentName.md or componentName.json
|
|
const pathEnd = `${componentName}.md`;
|
|
const pathEndJson = `${componentName}.json`;
|
|
return c.path && (c.path.endsWith(pathEnd) || c.path.endsWith(pathEndJson));
|
|
});
|
|
}
|
|
}
|
|
|
|
if (!component && componentPath) {
|
|
component = components.find(c => c.path === componentPath);
|
|
}
|
|
|
|
if (!component) {
|
|
throw new Error(`Component not found: ${componentType}/${componentName || componentPath}`);
|
|
}
|
|
|
|
this.component = {
|
|
...component,
|
|
type: componentType
|
|
};
|
|
|
|
await this.renderComponent();
|
|
|
|
} catch (error) {
|
|
console.error('Error loading component:', error);
|
|
this.showError(error.message);
|
|
}
|
|
}
|
|
|
|
async renderComponent() {
|
|
if (!this.component) {
|
|
this.showError('No component data available');
|
|
return;
|
|
}
|
|
|
|
// Hide loading state
|
|
document.getElementById('loadingState').style.display = 'none';
|
|
|
|
// Show component content
|
|
document.getElementById('componentContent').style.display = 'block';
|
|
|
|
// Render component header
|
|
this.renderComponentHeader();
|
|
|
|
// Render component description
|
|
this.renderComponentDescription();
|
|
|
|
// Render security validation section
|
|
this.renderSecurityValidation();
|
|
|
|
// Render metadata section
|
|
await this.renderMetadataSection();
|
|
|
|
// Render installation section
|
|
this.renderInstallationSection();
|
|
|
|
// Render component code
|
|
await this.renderComponentCode();
|
|
|
|
// Render GitHub link
|
|
this.renderGitHubLink();
|
|
|
|
// Populate sidebar quick install
|
|
this.renderSidebarQuickInstall();
|
|
|
|
// Update page metadata
|
|
this.updatePageMetadata();
|
|
|
|
// Load Giscus comments
|
|
this.loadGiscusComments();
|
|
|
|
// Activate tab from URL hash
|
|
this.activateTabFromHash();
|
|
|
|
// Track component view
|
|
window.eventTracker?.track('component_view', {
|
|
component_type: this.component.type,
|
|
component_name: this.component.name,
|
|
category: this.component.category || null
|
|
});
|
|
}
|
|
|
|
|
|
renderComponentHeader() {
|
|
const typeConfig = {
|
|
agent: { icon: '🤖', color: '#ff6b6b', badge: 'AGENT' },
|
|
command: { icon: '⚡', color: '#4ecdc4', badge: 'COMMAND' },
|
|
mcp: { icon: '🔌', color: '#45b7d1', badge: 'MCP' },
|
|
setting: { icon: '⚙️', color: '#9c88ff', badge: 'SETTING' },
|
|
hook: { icon: '🪝', color: '#ff8c42', badge: 'HOOK' },
|
|
template: { icon: '📦', color: '#f9a825', badge: 'TEMPLATE' }
|
|
};
|
|
|
|
const config = typeConfig[this.component.type] || typeConfig.template;
|
|
const formattedName = this.formatComponentName(this.component.name);
|
|
|
|
// Update icon with null check
|
|
const iconElement = document.getElementById('componentIcon');
|
|
if (iconElement) {
|
|
iconElement.textContent = config.icon;
|
|
}
|
|
|
|
// Update title with null check
|
|
const titleElement = document.getElementById('componentTitle');
|
|
if (titleElement) {
|
|
titleElement.textContent = formattedName;
|
|
}
|
|
|
|
// Update type badge with null check
|
|
const typeBadge = document.getElementById('componentTypeBadge');
|
|
if (typeBadge) {
|
|
typeBadge.textContent = config.badge;
|
|
typeBadge.style.backgroundColor = config.color;
|
|
}
|
|
|
|
// Update category - restored for original design
|
|
const category = this.component.category || 'General';
|
|
const categoryElement = document.getElementById('componentCategory');
|
|
if (categoryElement) {
|
|
categoryElement.textContent = category.charAt(0).toUpperCase() + category.slice(1);
|
|
}
|
|
|
|
// Update download badge
|
|
const downloadBadge = document.getElementById('componentDownloadBadge');
|
|
const downloadCount = document.getElementById('downloadCount');
|
|
if (downloadBadge && downloadCount) {
|
|
const downloads = this.component.downloads || 0;
|
|
if (downloads > 0) {
|
|
downloadBadge.style.display = 'inline-flex';
|
|
downloadCount.textContent = this.formatNumber(downloads);
|
|
downloadBadge.title = `${downloads.toLocaleString()} downloads`;
|
|
} else {
|
|
downloadBadge.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Add to cart button is set up in setupEventListeners()
|
|
}
|
|
|
|
renderComponentDescription() {
|
|
const description = this.getComponentDescription();
|
|
const descriptionElement = document.getElementById('componentDescription');
|
|
if (descriptionElement) {
|
|
descriptionElement.textContent = description;
|
|
}
|
|
}
|
|
|
|
renderSecurityValidation() {
|
|
const security = this.component.security;
|
|
|
|
// If no security data, hide everything
|
|
if (!security || !security.validated) {
|
|
const validationSection = document.getElementById('validationSection');
|
|
if (validationSection) validationSection.style.display = 'none';
|
|
|
|
const validationBadge = document.getElementById('componentValidationBadge');
|
|
if (validationBadge) validationBadge.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
// Show validation section
|
|
const validationSection = document.getElementById('validationSection');
|
|
if (validationSection) validationSection.style.display = 'block';
|
|
|
|
const score = security.score || 0;
|
|
const isValid = security.valid;
|
|
const errors = security.errorCount || 0;
|
|
const warnings = security.warningCount || 0;
|
|
|
|
// Determine color based on score
|
|
let scoreColor = '#48bb78'; // Green
|
|
let scoreStatus = 'Excellent';
|
|
|
|
if (score < 70) {
|
|
scoreColor = '#f56565'; // Red
|
|
scoreStatus = 'Needs Attention';
|
|
} else if (score < 85) {
|
|
scoreColor = '#ed8936'; // Orange
|
|
scoreStatus = 'Good';
|
|
} else if (score < 100) {
|
|
scoreColor = '#48bb78'; // Green
|
|
scoreStatus = 'Very Good';
|
|
} else if (score === 100) {
|
|
scoreColor = '#48bb78'; // Green
|
|
scoreStatus = 'Excellent';
|
|
}
|
|
|
|
// Only override to red if score is below 70 OR (invalid AND low errors)
|
|
if (!isValid && score < 70) {
|
|
scoreColor = '#f56565'; // Red
|
|
scoreStatus = 'Needs Attention';
|
|
}
|
|
|
|
// Update header badge
|
|
const validationBadge = document.getElementById('componentValidationBadge');
|
|
const validationScoreSpan = document.getElementById('validationScore');
|
|
if (validationBadge && validationScoreSpan) {
|
|
validationBadge.style.display = 'inline-flex';
|
|
validationBadge.style.backgroundColor = scoreColor + '20';
|
|
validationBadge.style.borderColor = scoreColor + '50';
|
|
validationBadge.style.color = scoreColor;
|
|
validationScoreSpan.textContent = score;
|
|
validationBadge.title = `Quality Score: ${score}/100 - Click to see details`;
|
|
validationBadge.style.cursor = 'pointer';
|
|
|
|
// Add click event to scroll to validation section
|
|
validationBadge.onclick = function() {
|
|
const validationSection = document.getElementById('validationSection');
|
|
if (validationSection) {
|
|
validationSection.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'start'
|
|
});
|
|
|
|
// Add a highlight animation
|
|
validationSection.style.animation = 'highlight 1s ease';
|
|
setTimeout(() => {
|
|
validationSection.style.animation = '';
|
|
}, 1000);
|
|
}
|
|
};
|
|
}
|
|
|
|
// Update score circle
|
|
const scoreCircle = document.getElementById('scoreCircle');
|
|
const scoreValue = document.getElementById('scoreValue');
|
|
if (scoreCircle && scoreValue) {
|
|
scoreCircle.style.borderColor = scoreColor;
|
|
scoreValue.textContent = score;
|
|
scoreValue.style.color = scoreColor;
|
|
}
|
|
|
|
// Update score status
|
|
const scoreStatusEl = document.getElementById('scoreStatus');
|
|
if (scoreStatusEl) {
|
|
scoreStatusEl.textContent = scoreStatus;
|
|
scoreStatusEl.style.color = scoreColor;
|
|
}
|
|
|
|
// Update inline stats
|
|
const inlineErrors = document.getElementById('inlineErrors');
|
|
const inlineWarnings = document.getElementById('inlineWarnings');
|
|
if (inlineErrors) {
|
|
inlineErrors.textContent = errors;
|
|
inlineErrors.style.color = errors > 0 ? '#f56565' : '#48bb78';
|
|
}
|
|
if (inlineWarnings) {
|
|
inlineWarnings.textContent = warnings;
|
|
inlineWarnings.style.color = warnings > 0 ? '#ed8936' : '#48bb78';
|
|
}
|
|
|
|
// Render validation checks with detailed messages
|
|
const validationChecks = document.getElementById('validationChecks');
|
|
if (validationChecks && security.validators) {
|
|
const checkDescriptions = {
|
|
structural: 'Verifies file format, YAML frontmatter, required fields, and encoding',
|
|
integrity: 'Checks for tampering using SHA256 hash and version tracking',
|
|
semantic: 'Detects malicious patterns, prompt injection, and dangerous commands',
|
|
references: 'Validates external URLs and prevents SSRF attacks',
|
|
provenance: 'Confirms author metadata and repository information'
|
|
};
|
|
|
|
const checkLabels = {
|
|
structural: 'Structure',
|
|
integrity: 'Integrity',
|
|
semantic: 'Content Safety',
|
|
references: 'references',
|
|
provenance: 'Source'
|
|
};
|
|
|
|
validationChecks.innerHTML = '';
|
|
Object.entries(security.validators).forEach(([key, validatorData]) => {
|
|
// Handle both old format (boolean) and new format (object with detailed data)
|
|
const passed = typeof validatorData === 'boolean' ? validatorData : validatorData.valid;
|
|
const hasErrors = typeof validatorData === 'object' && validatorData.errors && validatorData.errors.length > 0;
|
|
const hasWarnings = typeof validatorData === 'object' && validatorData.warnings && validatorData.warnings.length > 0;
|
|
const hasInfo = typeof validatorData === 'object' && validatorData.info && validatorData.info.length > 0;
|
|
const hasDetails = hasErrors || hasWarnings || hasInfo || passed;
|
|
|
|
// Determine status: passed, warning, or failed
|
|
let status = 'passed';
|
|
let icon = '✓';
|
|
if (hasErrors) {
|
|
status = 'failed';
|
|
icon = '✗';
|
|
} else if (hasWarnings) {
|
|
status = 'warning';
|
|
icon = '⚠';
|
|
}
|
|
|
|
const checkItem = document.createElement('div');
|
|
checkItem.className = `validation-check-item ${status}`;
|
|
checkItem.style.cursor = 'pointer';
|
|
|
|
// Build HTML for check item
|
|
checkItem.innerHTML = `
|
|
<div class="check-header">
|
|
<div class="check-main">
|
|
<span class="check-icon">${icon}</span>
|
|
<span class="check-label">${checkLabels[key] || key}</span>
|
|
</div>
|
|
<div class="check-stats">
|
|
${typeof validatorData === 'object' ? `
|
|
${validatorData.errorCount > 0 ? `<span class="check-error-count">${validatorData.errorCount} errors</span>` : ''}
|
|
${validatorData.warningCount > 0 ? `<span class="check-warning-count">${validatorData.warningCount} warnings</span>` : ''}
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Add click event listener - all items are clickable now
|
|
checkItem.addEventListener('click', () => {
|
|
window.openValidationModal(key, checkLabels[key] || key, validatorData, checkDescriptions[key]);
|
|
});
|
|
|
|
validationChecks.appendChild(checkItem);
|
|
});
|
|
}
|
|
}
|
|
|
|
async renderMetadataSection() {
|
|
const metadataSection = document.getElementById('metadataSection');
|
|
|
|
// Check if component has metadata fields directly from frontmatter
|
|
const hasDirectAuthor = this.component.author && this.component.author.trim() !== '';
|
|
const hasDirectRepo = this.component.repo && this.component.repo.trim() !== '';
|
|
const hasDirectVersion = this.component.version && this.component.version.trim() !== '';
|
|
const hasDirectLicense = this.component.license && this.component.license.trim() !== '';
|
|
const hasDirectKeywords = this.component.keywords && this.component.keywords.length > 0;
|
|
|
|
// For agents, try to load marketplace data
|
|
let agentMetadata = null;
|
|
if (this.component.type === 'agent') {
|
|
try {
|
|
const marketplace = await this.loadComponentsMarketplace();
|
|
|
|
if (marketplace && marketplace.agents) {
|
|
const componentName = this.component.name.replace('.md', '');
|
|
|
|
agentMetadata = marketplace.agents.find(
|
|
agent => agent.name === componentName
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading marketplace metadata:', error);
|
|
}
|
|
}
|
|
|
|
// Show metadata section if we have any metadata
|
|
const hasMetadata = hasDirectAuthor || hasDirectRepo || hasDirectVersion || hasDirectLicense || hasDirectKeywords || agentMetadata;
|
|
|
|
if (!hasMetadata) {
|
|
if (metadataSection) metadataSection.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
// Show metadata section
|
|
if (metadataSection) metadataSection.style.display = 'block';
|
|
|
|
// Populate version - prioritize direct version field, then marketplace
|
|
const versionElement = document.getElementById('metadataVersion');
|
|
if (versionElement) {
|
|
const version = this.component.version || agentMetadata?.version || '--';
|
|
versionElement.textContent = version;
|
|
}
|
|
|
|
// Populate author - prioritize direct author field, then marketplace
|
|
const authorElement = document.getElementById('metadataAuthor');
|
|
if (authorElement) {
|
|
if (hasDirectAuthor) {
|
|
authorElement.textContent = this.component.author;
|
|
} else if (agentMetadata) {
|
|
const authorName = agentMetadata.author?.name || agentMetadata.author || '--';
|
|
authorElement.textContent = authorName;
|
|
} else {
|
|
authorElement.textContent = '--';
|
|
}
|
|
}
|
|
|
|
// Populate license - prioritize direct license field, then marketplace
|
|
const licenseElement = document.getElementById('metadataLicense');
|
|
if (licenseElement) {
|
|
const license = this.component.license || agentMetadata?.license || '--';
|
|
licenseElement.textContent = license;
|
|
}
|
|
|
|
// Populate repository - prioritize direct repo field, then marketplace
|
|
const repositoryLink = document.getElementById('metadataRepository');
|
|
const repositoryNone = document.getElementById('metadataRepositoryNone');
|
|
|
|
let repositoryUrl = null;
|
|
|
|
// Priority 1: Direct repo field
|
|
if (hasDirectRepo) {
|
|
repositoryUrl = this.component.repo;
|
|
}
|
|
// Priority 2: Marketplace repository
|
|
else if (agentMetadata?.repository) {
|
|
repositoryUrl = agentMetadata.repository;
|
|
}
|
|
|
|
if (repositoryUrl) {
|
|
if (repositoryLink) {
|
|
repositoryLink.href = repositoryUrl;
|
|
repositoryLink.style.display = 'inline-flex';
|
|
}
|
|
if (repositoryNone) {
|
|
repositoryNone.style.display = 'none';
|
|
}
|
|
} else {
|
|
if (repositoryLink) {
|
|
repositoryLink.style.display = 'none';
|
|
}
|
|
if (repositoryNone) {
|
|
repositoryNone.style.display = 'inline';
|
|
}
|
|
}
|
|
|
|
// Populate keywords - prioritize direct keywords field, then marketplace
|
|
const keywordsContainer = document.getElementById('metadataKeywords');
|
|
if (keywordsContainer) {
|
|
const keywords = this.component.keywords || agentMetadata?.keywords || [];
|
|
if (keywords && keywords.length > 0) {
|
|
keywordsContainer.innerHTML = '';
|
|
keywords.forEach(keyword => {
|
|
const keywordChip = document.createElement('span');
|
|
keywordChip.className = 'metadata-keyword';
|
|
keywordChip.textContent = keyword;
|
|
keywordsContainer.appendChild(keywordChip);
|
|
});
|
|
} else {
|
|
keywordsContainer.innerHTML = '<span class="metadata-value">--</span>';
|
|
}
|
|
}
|
|
}
|
|
|
|
async loadComponentsMarketplace() {
|
|
try {
|
|
// Check if components marketplace is already cached
|
|
if (this.componentsMarketplace) {
|
|
return this.componentsMarketplace;
|
|
}
|
|
|
|
// Use the already loaded components data from dataLoader
|
|
const componentsData = await this.dataLoader.loadAllComponents();
|
|
|
|
if (componentsData && componentsData.componentsMarketplace) {
|
|
this.componentsMarketplace = componentsData.componentsMarketplace;
|
|
return this.componentsMarketplace;
|
|
}
|
|
|
|
// Fallback: try to fetch marketplace.json directly from components/.claude-plugin/
|
|
const marketplaceResponse = await fetch('https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/.claude-plugin/marketplace.json');
|
|
if (marketplaceResponse.ok) {
|
|
this.componentsMarketplace = await marketplaceResponse.json();
|
|
return this.componentsMarketplace;
|
|
}
|
|
|
|
console.warn('No components marketplace found');
|
|
return null;
|
|
} catch (error) {
|
|
console.error('Error loading components marketplace:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
renderInstallationSection() {
|
|
const componentPath = this.getCleanPath();
|
|
const basicInstallCommand = `npx claude-code-templates@latest --${this.component.type}=${componentPath} --yes`;
|
|
|
|
// Update basic installation command
|
|
const basicInstallElement = document.getElementById('basicInstallCommand');
|
|
if (basicInstallElement) {
|
|
basicInstallElement.textContent = basicInstallCommand;
|
|
}
|
|
|
|
// Handle agent-specific sections
|
|
if (this.component.type === 'agent') {
|
|
this.renderGlobalAgentSection(componentPath);
|
|
this.renderCloudSandboxSection(componentPath);
|
|
} else {
|
|
// Hide agent-specific sections for non-agents
|
|
const globalAgentSection = document.getElementById('globalAgentSection');
|
|
const cloudSandboxSection = document.getElementById('cloudSandboxSection');
|
|
|
|
if (globalAgentSection) globalAgentSection.style.display = 'none';
|
|
if (cloudSandboxSection) cloudSandboxSection.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
renderGlobalAgentSection(componentPath) {
|
|
const globalAgentSection = document.getElementById('globalAgentSection');
|
|
const globalAgentCommand = `npx claude-code-templates@latest --create-agent ${componentPath}`;
|
|
const globalUsageCommand = `${componentPath.split('/').pop()} "your prompt here"`;
|
|
|
|
const globalAgentCommandElement = document.getElementById('globalAgentCommand');
|
|
const globalUsageCommandElement = document.getElementById('globalUsageCommand');
|
|
|
|
if (globalAgentCommandElement) {
|
|
globalAgentCommandElement.textContent = globalAgentCommand;
|
|
}
|
|
if (globalUsageCommandElement) {
|
|
globalUsageCommandElement.textContent = globalUsageCommand;
|
|
}
|
|
|
|
if (globalAgentSection) {
|
|
globalAgentSection.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
renderCloudSandboxSection(componentPath) {
|
|
const cloudSandboxSection = document.getElementById('cloudSandboxSection');
|
|
|
|
// Update all sandbox command examples with the component path
|
|
const e2bSandboxCommand = `npx claude-code-templates@latest --sandbox e2b --agent=${componentPath} --prompt "your development task"`;
|
|
const cloudflareSandboxCommand = `npx claude-code-templates@latest --sandbox cloudflare --agent=${componentPath} --prompt "your development task"`;
|
|
const dockerSandboxCommand = `npx claude-code-templates@latest --sandbox docker --agent=${componentPath} --prompt "your development task"`;
|
|
|
|
const e2bSandboxCommandElement = document.getElementById('e2bSandboxCommand');
|
|
const cloudflareSandboxCommandElement = document.getElementById('cloudflareSandboxCommand');
|
|
const dockerSandboxCommandElement = document.getElementById('dockerSandboxCommand');
|
|
|
|
if (e2bSandboxCommandElement) {
|
|
e2bSandboxCommandElement.textContent = e2bSandboxCommand;
|
|
}
|
|
if (cloudflareSandboxCommandElement) {
|
|
cloudflareSandboxCommandElement.textContent = cloudflareSandboxCommand;
|
|
}
|
|
if (dockerSandboxCommandElement) {
|
|
dockerSandboxCommandElement.textContent = dockerSandboxCommand;
|
|
}
|
|
|
|
if (cloudSandboxSection) {
|
|
cloudSandboxSection.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
async renderComponentCode() {
|
|
try {
|
|
let content = this.component.content || 'No content available.';
|
|
let language = 'plaintext';
|
|
|
|
// Determine language based on file extension
|
|
if (this.component.path) {
|
|
const extension = this.component.path.split('.').pop();
|
|
switch (extension) {
|
|
case 'md':
|
|
language = 'markdown';
|
|
break;
|
|
case 'json':
|
|
language = 'json';
|
|
// Pretty print JSON
|
|
try {
|
|
content = JSON.stringify(JSON.parse(content), null, 2);
|
|
} catch (e) {
|
|
console.warn('Could not parse JSON content:', e);
|
|
}
|
|
break;
|
|
case 'js':
|
|
language = 'javascript';
|
|
break;
|
|
case 'yml':
|
|
case 'yaml':
|
|
language = 'yaml';
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Collect line numbers with errors and warnings
|
|
const errorLines = new Set();
|
|
const warningLines = new Set();
|
|
|
|
if (this.component.security && this.component.security.validators) {
|
|
Object.values(this.component.security.validators).forEach(validator => {
|
|
if (validator.errors) {
|
|
validator.errors.forEach(error => {
|
|
// Check direct line property
|
|
if (error.line) {
|
|
errorLines.add(error.line);
|
|
}
|
|
// Check examples array
|
|
if (error.examples && error.examples.length > 0) {
|
|
error.examples.forEach(example => {
|
|
if (example.line) {
|
|
errorLines.add(example.line);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
if (validator.warnings) {
|
|
validator.warnings.forEach(warning => {
|
|
// Check direct line property
|
|
if (warning.line) {
|
|
warningLines.add(warning.line);
|
|
}
|
|
// Check examples array
|
|
if (warning.examples && warning.examples.length > 0) {
|
|
warning.examples.forEach(example => {
|
|
if (example.line) {
|
|
warningLines.add(example.line);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Update code language indicator
|
|
const codeLanguageElement = document.getElementById('codeLanguage');
|
|
if (codeLanguageElement) {
|
|
codeLanguageElement.textContent = language;
|
|
}
|
|
|
|
// Render code with syntax highlighting
|
|
const codeElement = document.querySelector('#codeContent code');
|
|
if (codeElement) {
|
|
codeElement.innerHTML = this.highlightCode(content, language);
|
|
codeElement.className = `language-${language}`;
|
|
}
|
|
|
|
// Generate line numbers with error/warning indicators
|
|
const lines = content.split('\n');
|
|
const lineNumbers = lines.map((_, index) => {
|
|
const lineNum = index + 1;
|
|
let className = '';
|
|
let style = '';
|
|
let title = '';
|
|
|
|
if (errorLines.has(lineNum)) {
|
|
className = 'line-with-error';
|
|
style = 'background-color: rgba(245, 101, 101, 0.15); color: #f56565; font-weight: 600; border-left: 3px solid #f56565; padding-left: 5px; cursor: pointer;';
|
|
title = 'Line has validation errors - Click to see details';
|
|
} else if (warningLines.has(lineNum)) {
|
|
className = 'line-with-warning';
|
|
style = 'background-color: rgba(237, 137, 54, 0.15); color: #ed8936; font-weight: 600; border-left: 3px solid #ed8936; padding-left: 5px; cursor: pointer;';
|
|
title = 'Line has validation warnings - Click to see details';
|
|
}
|
|
|
|
return `<span class="${className}" style="${style}" title="${title}" data-line-number="${lineNum}">${lineNum}</span>`;
|
|
}).join('');
|
|
|
|
const lineNumbersElement = document.getElementById('lineNumbers');
|
|
if (lineNumbersElement) {
|
|
lineNumbersElement.innerHTML = lineNumbers;
|
|
|
|
// Add click event listeners to error/warning line numbers
|
|
const clickableLines = lineNumbersElement.querySelectorAll('.line-with-error, .line-with-warning');
|
|
clickableLines.forEach(lineSpan => {
|
|
lineSpan.addEventListener('click', () => {
|
|
// Find which validator has the error for this line
|
|
const lineNum = parseInt(lineSpan.getAttribute('data-line-number'));
|
|
|
|
if (this.component.security && this.component.security.validators) {
|
|
// Look for the validator with this line number
|
|
for (const [validatorKey, validatorData] of Object.entries(this.component.security.validators)) {
|
|
if (typeof validatorData === 'object') {
|
|
// Check if this validator has errors/warnings for this line
|
|
const hasError = validatorData.errors && validatorData.errors.some(error => {
|
|
if (error.line === lineNum) return true;
|
|
if (error.examples && error.examples.some(ex => ex.line === lineNum)) return true;
|
|
return false;
|
|
});
|
|
|
|
const hasWarning = validatorData.warnings && validatorData.warnings.some(warning => {
|
|
if (warning.line === lineNum) return true;
|
|
if (warning.examples && warning.examples.some(ex => ex.line === lineNum)) return true;
|
|
return false;
|
|
});
|
|
|
|
if (hasError || hasWarning) {
|
|
// Found the validator with this line, open its modal
|
|
const checkLabels = {
|
|
structural: 'Structure',
|
|
integrity: 'Integrity',
|
|
semantic: 'Content Safety',
|
|
references: 'references',
|
|
provenance: 'Source'
|
|
};
|
|
|
|
const checkDescriptions = {
|
|
structural: 'Verifies file format, YAML frontmatter, required fields, and encoding',
|
|
integrity: 'Checks for tampering using SHA256 hash and version tracking',
|
|
semantic: 'Detects malicious patterns, prompt injection, and dangerous commands',
|
|
references: 'Validates external URLs and prevents SSRF attacks',
|
|
provenance: 'Confirms author metadata and repository information'
|
|
};
|
|
|
|
window.openValidationModal(
|
|
validatorKey,
|
|
checkLabels[validatorKey] || validatorKey,
|
|
validatorData,
|
|
checkDescriptions[validatorKey]
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Add corresponding highlighting to code lines
|
|
if (errorLines.size > 0 || warningLines.size > 0) {
|
|
const codeLines = codeElement.innerHTML.split('\n');
|
|
const highlightedLines = codeLines.map((line, index) => {
|
|
const lineNum = index + 1;
|
|
if (errorLines.has(lineNum)) {
|
|
return `<span style="background-color: rgba(245, 101, 101, 0.08); display: block; margin: 0 -10px; padding: 0 10px;">${line}</span>`;
|
|
} else if (warningLines.has(lineNum)) {
|
|
return `<span style="background-color: rgba(237, 137, 54, 0.08); display: block; margin: 0 -10px; padding: 0 10px;">${line}</span>`;
|
|
}
|
|
return line;
|
|
});
|
|
codeElement.innerHTML = highlightedLines.join('\n');
|
|
}
|
|
|
|
// Sync scroll between line numbers and code content
|
|
const codeContentDiv = document.getElementById('codeContent');
|
|
if (lineNumbersElement && codeContentDiv) {
|
|
codeContentDiv.addEventListener('scroll', function() {
|
|
lineNumbersElement.scrollTop = codeContentDiv.scrollTop;
|
|
});
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error rendering component code:', error);
|
|
const codeElement = document.querySelector('#codeContent code');
|
|
if (codeElement) {
|
|
codeElement.textContent = 'Error loading component content.';
|
|
}
|
|
}
|
|
}
|
|
|
|
loadGiscusComments() {
|
|
const container = document.getElementById('giscusContainer');
|
|
if (!container) return;
|
|
|
|
container.innerHTML = '';
|
|
|
|
const script = document.createElement('script');
|
|
script.src = 'https://giscus.app/client.js';
|
|
script.setAttribute('data-repo', 'davila7/claude-code-templates');
|
|
script.setAttribute('data-repo-id', 'R_kgDOPGh7XA');
|
|
script.setAttribute('data-category', 'Comments');
|
|
script.setAttribute('data-category-id', 'DIC_kwDOPGh7XM4C2H0y');
|
|
script.setAttribute('data-mapping', 'pathname');
|
|
script.setAttribute('data-strict', '0');
|
|
script.setAttribute('data-reactions-enabled', '0');
|
|
script.setAttribute('data-emit-metadata', '0');
|
|
script.setAttribute('data-input-position', 'bottom');
|
|
script.setAttribute('data-theme', 'dark');
|
|
script.setAttribute('data-lang', 'en');
|
|
script.setAttribute('crossorigin', 'anonymous');
|
|
script.async = true;
|
|
|
|
container.appendChild(script);
|
|
}
|
|
|
|
renderSidebarQuickInstall() {
|
|
const commandEl = document.getElementById('quickInstallCommand');
|
|
if (commandEl) {
|
|
const componentPath = this.getCleanPath();
|
|
commandEl.textContent = `npx claude-code-templates@latest --${this.component.type}=${componentPath} --yes`;
|
|
}
|
|
|
|
// Populate sidebar GitHub link
|
|
const sidebarLink = document.getElementById('githubLinkSidebar');
|
|
if (sidebarLink) {
|
|
sidebarLink.href = this.generateGitHubURL();
|
|
}
|
|
}
|
|
|
|
activateTabFromHash() {
|
|
const hash = window.location.hash.slice(1);
|
|
if (hash) {
|
|
this.switchTab(hash);
|
|
}
|
|
}
|
|
|
|
switchTab(tabName) {
|
|
const validTabs = ['overview', 'code', 'installation'];
|
|
if (!validTabs.includes(tabName)) return;
|
|
|
|
// Update buttons
|
|
document.querySelectorAll('.component-tabs .tab-btn').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.tab === tabName);
|
|
});
|
|
|
|
// Update panes
|
|
document.querySelectorAll('.tab-pane').forEach(pane => {
|
|
pane.classList.toggle('active', pane.id === `tab-${tabName}`);
|
|
});
|
|
}
|
|
|
|
renderGitHubLink() {
|
|
const githubUrl = this.generateGitHubURL();
|
|
const githubLinkElement = document.getElementById('githubLink');
|
|
if (githubLinkElement) {
|
|
githubLinkElement.href = githubUrl;
|
|
}
|
|
}
|
|
|
|
generateGitHubURL() {
|
|
let githubUrl = 'https://github.com/davila7/claude-code-templates/';
|
|
|
|
if (this.component.type === 'template') {
|
|
githubUrl += `tree/main/cli-tool/templates/${this.component.folderPath || ''}`;
|
|
} else {
|
|
const componentPath = this.component.path || this.component.name;
|
|
githubUrl += `blob/main/cli-tool/components/${this.component.type}s/${componentPath}`;
|
|
}
|
|
|
|
return githubUrl;
|
|
}
|
|
|
|
|
|
|
|
setupEventListeners() {
|
|
// Track copy command clicks on installation buttons
|
|
document.addEventListener('click', (e) => {
|
|
const copyBtn = e.target.closest('.copy-btn, .quick-copy-btn');
|
|
if (copyBtn && this.component) {
|
|
window.eventTracker?.track('copy_command', {
|
|
component_type: this.component.type,
|
|
component_name: this.component.name,
|
|
source: 'detail_page'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Tab switching
|
|
document.querySelectorAll('.component-tabs .tab-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const tab = btn.dataset.tab;
|
|
this.switchTab(tab);
|
|
history.replaceState(null, '', `#${tab}`);
|
|
});
|
|
});
|
|
|
|
// Handle hash changes
|
|
window.addEventListener('hashchange', () => {
|
|
this.activateTabFromHash();
|
|
});
|
|
|
|
// Handle browser back/forward buttons
|
|
window.addEventListener('popstate', () => {
|
|
this.loadComponentFromURL();
|
|
});
|
|
|
|
// Handle Add to Stack button
|
|
const addToCartBtn = document.getElementById('addToCartBtn');
|
|
if (addToCartBtn) {
|
|
addToCartBtn.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
if (this.component) {
|
|
if (typeof addToCart === 'function') {
|
|
// Convert component type to plural format
|
|
const componentType = this.getComponentTypePlural();
|
|
|
|
if (!componentType) {
|
|
console.error('Failed to get component type plural');
|
|
alert('Unable to determine component type. Please refresh the page.');
|
|
return;
|
|
}
|
|
|
|
if (this.component.name && this.component.path) {
|
|
// Create component object that matches cart manager expectations
|
|
// Clean component name (remove .md extension and format properly)
|
|
const cleanName = this.getCleanComponentName();
|
|
const componentItem = {
|
|
name: cleanName,
|
|
path: this.component.path,
|
|
category: this.component.category
|
|
};
|
|
addToCart(componentItem, componentType);
|
|
} else {
|
|
console.error('Missing component name or path:', this.component);
|
|
alert('Unable to add component: missing component information');
|
|
}
|
|
} else {
|
|
console.error('Cart functionality not available');
|
|
// Fallback: show a message or redirect to main page
|
|
alert('Cart functionality not available. Please return to the main page to add components to your stack.');
|
|
}
|
|
} else {
|
|
console.error('No component data available');
|
|
alert('Component data not loaded. Please refresh the page.');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
getComponentType() {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
return urlParams.get('type');
|
|
}
|
|
|
|
getComponentTypePlural() {
|
|
// Get type from the component object itself
|
|
const type = this.component?.type;
|
|
|
|
if (!type) {
|
|
console.error('Component type not found in component object');
|
|
return null;
|
|
}
|
|
|
|
// Convert singular type to plural for cart manager
|
|
const typeMapping = {
|
|
'agent': 'agents',
|
|
'command': 'commands',
|
|
'setting': 'settings',
|
|
'hook': 'hooks',
|
|
'mcp': 'mcps',
|
|
'template': 'templates'
|
|
};
|
|
|
|
return typeMapping[type] || type + 's';
|
|
}
|
|
|
|
getComponentFromURL() {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
return urlParams.get('name') || urlParams.get('path');
|
|
}
|
|
|
|
getCleanComponentName() {
|
|
if (!this.component || !this.component.name) {
|
|
return 'Unknown Component';
|
|
}
|
|
|
|
let cleanName = this.component.name;
|
|
|
|
// Remove .md extension if present
|
|
if (cleanName.endsWith('.md')) {
|
|
cleanName = cleanName.slice(0, -3);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
updatePageMetadata() {
|
|
const cleanName = this.getCleanComponentName(this.component.name);
|
|
const description = this.getComponentDescription();
|
|
const typeCapitalized = this.component.type.charAt(0).toUpperCase() + this.component.type.slice(1);
|
|
const category = this.component.category || 'Development';
|
|
|
|
// Enhanced page title with component type and category
|
|
const pageTitle = `${cleanName} ${typeCapitalized} - Claude Code Templates`;
|
|
const enhancedDescription = `${description} | ${typeCapitalized} for ${category} | Claude Code Templates - AI-powered development tools`;
|
|
|
|
// Generate proper canonical URL for SEO
|
|
const canonicalURL = this.generateCanonicalURL();
|
|
|
|
// Update page title
|
|
document.title = pageTitle;
|
|
const pageTitleElement = document.getElementById('page-title');
|
|
if (pageTitleElement) {
|
|
pageTitleElement.textContent = pageTitle;
|
|
}
|
|
|
|
// Update meta description
|
|
const metaDescription = document.querySelector('meta[name="description"]');
|
|
if (metaDescription) {
|
|
metaDescription.content = enhancedDescription;
|
|
}
|
|
|
|
// Update keywords
|
|
const metaKeywords = document.querySelector('meta[name="keywords"]');
|
|
if (metaKeywords) {
|
|
metaKeywords.content = `${cleanName}, ${this.component.type}, ${category}, Claude Code, AI development, automation, ${this.component.type}s, templates`;
|
|
}
|
|
|
|
// Update canonical URL
|
|
const canonicalLink = document.getElementById('canonical-url');
|
|
if (canonicalLink) {
|
|
canonicalLink.href = canonicalURL;
|
|
}
|
|
|
|
// Update Open Graph meta tags
|
|
const ogUrl = document.getElementById('og-url');
|
|
const ogTitle = document.getElementById('og-title');
|
|
const ogDescription = document.getElementById('og-description');
|
|
|
|
if (ogUrl) ogUrl.content = canonicalURL;
|
|
if (ogTitle) ogTitle.content = pageTitle;
|
|
if (ogDescription) ogDescription.content = enhancedDescription;
|
|
|
|
// Update Twitter meta tags
|
|
const twitterUrl = document.getElementById('twitter-url');
|
|
const twitterTitle = document.getElementById('twitter-title');
|
|
const twitterDescription = document.getElementById('twitter-description');
|
|
|
|
if (twitterUrl) twitterUrl.content = canonicalURL;
|
|
if (twitterTitle) twitterTitle.content = pageTitle;
|
|
if (twitterDescription) twitterDescription.content = enhancedDescription;
|
|
|
|
// Add structured data for better SEO
|
|
this.addStructuredData(cleanName, description, typeCapitalized, category, canonicalURL);
|
|
}
|
|
|
|
generateCanonicalURL() {
|
|
// Generate SEO-friendly canonical URL
|
|
const isLocal = window.location.hostname === 'localhost' ||
|
|
window.location.hostname === '127.0.0.1' ||
|
|
window.location.hostname.includes('5500');
|
|
|
|
if (!isLocal && this.component.type && this.component.name) {
|
|
// Use SEO-friendly URL for production
|
|
let cleanName = this.component.name;
|
|
if (cleanName.endsWith('.md')) {
|
|
cleanName = cleanName.slice(0, -3);
|
|
}
|
|
if (cleanName.endsWith('.json')) {
|
|
cleanName = cleanName.slice(0, -5);
|
|
}
|
|
|
|
const baseUrl = window.location.origin;
|
|
return `${baseUrl}/component/${encodeURIComponent(this.component.type)}/${encodeURIComponent(cleanName)}`;
|
|
}
|
|
|
|
// Fallback to current URL
|
|
return window.location.href;
|
|
}
|
|
|
|
addStructuredData(name, description, type, category, url) {
|
|
// Remove existing structured data
|
|
const existingScript = document.getElementById('component-structured-data');
|
|
if (existingScript) {
|
|
existingScript.remove();
|
|
}
|
|
|
|
// Add new structured data
|
|
const structuredData = {
|
|
"@context": "https://schema.org",
|
|
"@type": "SoftwareApplication",
|
|
"name": name,
|
|
"applicationCategory": "DeveloperApplication",
|
|
"applicationSubCategory": `${type} Component`,
|
|
"description": description,
|
|
"operatingSystem": ["Windows", "macOS", "Linux"],
|
|
"url": url,
|
|
"author": {
|
|
"@type": "Organization",
|
|
"name": "Claude Code Templates Community"
|
|
},
|
|
"offers": {
|
|
"@type": "Offer",
|
|
"price": "0",
|
|
"priceCurrency": "USD"
|
|
},
|
|
"keywords": `${name}, ${type}, ${category}, Claude Code, AI development`,
|
|
"programmingLanguage": type === "command" ? "Shell" : "Configuration",
|
|
"relatedLink": "https://www.anthropic.com/claude-code"
|
|
};
|
|
|
|
const script = document.createElement('script');
|
|
script.id = 'component-structured-data';
|
|
script.type = 'application/ld+json';
|
|
script.textContent = JSON.stringify(structuredData, null, 2);
|
|
document.head.appendChild(script);
|
|
}
|
|
|
|
|
|
// Utility methods
|
|
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).replace(/\.0$/, '') + 'M';
|
|
}
|
|
if (num >= 1000) {
|
|
return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'K';
|
|
}
|
|
return num.toString();
|
|
}
|
|
|
|
getComponentDescription() {
|
|
let description = this.component.description || '';
|
|
|
|
if (!description && this.component.content) {
|
|
const descMatch = this.component.content.match(/description:\s*(.+?)(?:\n|$)/);
|
|
if (descMatch) {
|
|
description = descMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
} else {
|
|
const lines = this.component.content.split('\n');
|
|
const firstParagraph = lines.find(line =>
|
|
line.trim() && !line.startsWith('---') && !line.startsWith('#')
|
|
);
|
|
if (firstParagraph) {
|
|
description = firstParagraph.trim();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!description) {
|
|
description = `A ${this.component.type} component for enhanced development workflow.`;
|
|
}
|
|
|
|
return description;
|
|
}
|
|
|
|
getCleanPath() {
|
|
let componentPath = this.component.path || this.component.name;
|
|
|
|
// Remove file extensions
|
|
if (componentPath.endsWith('.md') || componentPath.endsWith('.json')) {
|
|
componentPath = componentPath.replace(/\.(md|json)$/, '');
|
|
}
|
|
|
|
return componentPath;
|
|
}
|
|
|
|
highlightCode(content, language) {
|
|
// Basic syntax highlighting (same as modal)
|
|
let highlighted = content
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
|
|
if (language === 'markdown' || language === 'yaml') {
|
|
// Highlight YAML/Markdown frontmatter keys (blue)
|
|
highlighted = highlighted.replace(/^([a-zA-Z_-]+):/gm, '<span style="color: #569cd6;">$1</span>:');
|
|
|
|
// Highlight strings in quotes (orange)
|
|
highlighted = highlighted.replace(/"([^&]+?)"/g, '<span style="color: #ce9178;">"$1"</span>');
|
|
|
|
// Highlight important keywords (light blue)
|
|
highlighted = highlighted.replace(/\b(hackathon|strategy|AI|solution|ideation|evaluation|projects|feedback|concepts|feasibility|guidance|agent|specialist|brainstorming|winning|judge|feedback|Context|User|Examples)\b/gi, '<span style="color: #4fc1ff;">$1</span>');
|
|
|
|
// Highlight markdown headers (blue)
|
|
highlighted = highlighted.replace(/^(#+)\s*(.+)$/gm, '<span style="color: #569cd6;">$1</span> <span style="color: #dcdcaa;">$2</span>');
|
|
|
|
// Highlight code in backticks
|
|
highlighted = highlighted.replace(/`([^`]+)`/g, '<span style="color: #ce9178;">$1</span>');
|
|
|
|
// Highlight YAML separators
|
|
highlighted = highlighted.replace(/^---$/gm, '<span style="color: #808080;">---</span>');
|
|
}
|
|
|
|
return highlighted;
|
|
}
|
|
|
|
showError(message) {
|
|
console.error('Component page error:', message);
|
|
|
|
document.getElementById('loadingState').style.display = 'none';
|
|
document.getElementById('componentContent').style.display = 'none';
|
|
document.getElementById('errorState').style.display = 'block';
|
|
|
|
// Update error message if needed
|
|
const errorElement = document.querySelector('#errorState p');
|
|
if (errorElement && message !== 'Missing component parameters in URL') {
|
|
errorElement.textContent = message;
|
|
}
|
|
}
|
|
|
|
// Static method to create URL for component
|
|
static createComponentURL(type, name, path) {
|
|
// Detect if we're in local development
|
|
const isLocal = window.location.hostname === 'localhost' ||
|
|
window.location.hostname === '127.0.0.1' ||
|
|
window.location.hostname.includes('5500');
|
|
|
|
if (!isLocal && type && name) {
|
|
// Use SEO-friendly URL structure for production: /component/type/name
|
|
let cleanName = name;
|
|
if (cleanName.endsWith('.md')) {
|
|
cleanName = cleanName.slice(0, -3);
|
|
}
|
|
if (cleanName.endsWith('.json')) {
|
|
cleanName = cleanName.slice(0, -5);
|
|
}
|
|
|
|
return `component/${encodeURIComponent(type)}/${encodeURIComponent(cleanName)}`;
|
|
}
|
|
|
|
// Use query parameters for local development or fallback
|
|
const params = new URLSearchParams();
|
|
params.set('type', type);
|
|
if (name) params.set('name', name);
|
|
if (path) params.set('path', path);
|
|
|
|
return `component.html?${params.toString()}`;
|
|
}
|
|
}
|
|
|
|
// Global function for creating component URLs (used by other scripts)
|
|
function createComponentURL(type, name, path) {
|
|
return ComponentPageManager.createComponentURL(type, name, path);
|
|
}
|
|
|
|
// Initialize when DOM is loaded
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
window.componentPageManager = new ComponentPageManager();
|
|
});
|
|
|
|
// Export for other scripts
|
|
window.ComponentPageManager = ComponentPageManager;
|
|
|
|
// Component Share functionality
|
|
function toggleComponentShareDropdown() {
|
|
const shareDropdown = document.getElementById('componentShareDropdown');
|
|
if (shareDropdown) {
|
|
shareDropdown.classList.toggle('open');
|
|
}
|
|
}
|
|
|
|
function shareComponentOnTwitter() {
|
|
const componentManager = window.componentPageManager;
|
|
let message;
|
|
|
|
if (componentManager && componentManager.component) {
|
|
const cleanName = componentManager.getCleanComponentName(componentManager.component.name);
|
|
const type = componentManager.component.type.charAt(0).toUpperCase() + componentManager.component.type.slice(1);
|
|
const category = componentManager.component.category || 'Development';
|
|
const canonicalURL = componentManager.generateCanonicalURL();
|
|
|
|
message = `🚀 Found this amazing ${cleanName} ${type} for Claude Code!
|
|
|
|
Perfect for ${category.toLowerCase()} workflows with AI-powered automation.
|
|
|
|
${canonicalURL}
|
|
|
|
#ClaudeCode #AI #Development #${category.replace(/\s+/g, '')} #Automation`;
|
|
} else {
|
|
// Fallback message
|
|
const componentTitle = document.getElementById('componentTitle')?.textContent || 'Claude Code Component';
|
|
const currentURL = window.location.href;
|
|
message = `🚀 Check out this ${componentTitle} for Claude Code!
|
|
|
|
Perfect for AI-powered development workflows.
|
|
|
|
${currentURL}
|
|
|
|
#ClaudeCode #AI #Development`;
|
|
}
|
|
|
|
const twitterUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(message)}`;
|
|
window.open(twitterUrl, '_blank');
|
|
|
|
// Close dropdown after sharing
|
|
const shareDropdown = document.getElementById('componentShareDropdown');
|
|
if (shareDropdown) {
|
|
shareDropdown.classList.remove('open');
|
|
}
|
|
}
|
|
|
|
function shareComponentOnThreads() {
|
|
const componentManager = window.componentPageManager;
|
|
let message;
|
|
|
|
if (componentManager && componentManager.component) {
|
|
const cleanName = componentManager.getCleanComponentName(componentManager.component.name);
|
|
const type = componentManager.component.type.charAt(0).toUpperCase() + componentManager.component.type.slice(1);
|
|
const category = componentManager.component.category || 'Development';
|
|
const canonicalURL = componentManager.generateCanonicalURL();
|
|
|
|
message = `🚀 Found this amazing ${cleanName} ${type} for Claude Code!
|
|
|
|
Perfect for ${category.toLowerCase()} workflows with AI-powered automation.
|
|
|
|
${canonicalURL}
|
|
|
|
#ClaudeCode #AI #Development #${category.replace(/\s+/g, '')} #Automation`;
|
|
} else {
|
|
// Fallback message
|
|
const componentTitle = document.getElementById('componentTitle')?.textContent || 'Claude Code Component';
|
|
const currentURL = window.location.href;
|
|
message = `🚀 Check out this ${componentTitle} for Claude Code!
|
|
|
|
Perfect for AI-powered development workflows.
|
|
|
|
${currentURL}
|
|
|
|
#ClaudeCode #AI #Development`;
|
|
}
|
|
|
|
const threadsUrl = `https://threads.net/intent/post?text=${encodeURIComponent(message)}`;
|
|
window.open(threadsUrl, '_blank');
|
|
|
|
// Close dropdown after sharing
|
|
const shareDropdown = document.getElementById('componentShareDropdown');
|
|
if (shareDropdown) {
|
|
shareDropdown.classList.remove('open');
|
|
}
|
|
}
|
|
|
|
// Close dropdown when clicking outside
|
|
document.addEventListener('click', (e) => {
|
|
const shareDropdown = document.getElementById('componentShareDropdown');
|
|
if (shareDropdown && !shareDropdown.contains(e.target)) {
|
|
shareDropdown.classList.remove('open');
|
|
}
|
|
});
|
|
|
|
// Validation Modal System
|
|
window.openValidationModal = function(validatorKey, validatorLabel, validatorData, description) {
|
|
// Create modal overlay
|
|
const modalOverlay = document.createElement('div');
|
|
modalOverlay.className = 'modal-overlay';
|
|
modalOverlay.style.cssText = `
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 10000;
|
|
padding: 20px;
|
|
backdrop-filter: blur(3px);
|
|
overflow: hidden;
|
|
`;
|
|
|
|
// Create modal container
|
|
const modalContainer = document.createElement('div');
|
|
modalContainer.style.cssText = `
|
|
background: #1a1a1a;
|
|
border: 1px solid #333;
|
|
border-radius: 8px;
|
|
max-width: 700px;
|
|
max-height: 80vh;
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
|
overflow: hidden;
|
|
`;
|
|
|
|
// Handle both old format (boolean) and new format (object)
|
|
const passed = typeof validatorData === 'boolean' ? validatorData : validatorData.valid;
|
|
const hasErrors = typeof validatorData === 'object' && validatorData.errors && validatorData.errors.length > 0;
|
|
const hasWarnings = typeof validatorData === 'object' && validatorData.warnings && validatorData.warnings.length > 0;
|
|
const hasInfo = typeof validatorData === 'object' && validatorData.info && validatorData.info.length > 0;
|
|
|
|
// Determine status
|
|
let status = 'passed';
|
|
let statusColor = '#48bb78';
|
|
let statusText = 'All Checks Passed';
|
|
let statusIcon = '✓';
|
|
|
|
if (hasErrors) {
|
|
status = 'failed';
|
|
statusColor = '#f56565';
|
|
statusText = 'Validation Failed';
|
|
statusIcon = '✗';
|
|
} else if (hasWarnings) {
|
|
status = 'warning';
|
|
statusColor = '#ed8936';
|
|
statusText = 'Validation Passed with Warnings';
|
|
statusIcon = '⚠';
|
|
}
|
|
|
|
// Build modal content - Header (fixed)
|
|
let modalHeader = `
|
|
<div style="padding: 30px 30px 20px 30px; flex-shrink: 0; border-bottom: 1px solid #333;">
|
|
<!-- Header -->
|
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 16px;">
|
|
<div style="flex: 1;">
|
|
<h2 style="margin: 0 0 8px 0; color: #fff; font-size: 24px; font-weight: 600;">
|
|
${validatorLabel}
|
|
</h2>
|
|
<p style="margin: 0; color: #999; font-size: 14px; line-height: 1.6;">
|
|
${description || 'Validation check details'}
|
|
</p>
|
|
</div>
|
|
<button id="closeValidationModal" style="
|
|
background: transparent;
|
|
border: none;
|
|
color: #999;
|
|
font-size: 28px;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
width: 32px;
|
|
height: 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: color 0.2s;
|
|
margin-left: 20px;
|
|
flex-shrink: 0;
|
|
" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='#999'">
|
|
×
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Status Badge -->
|
|
<div style="
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 16px;
|
|
background: ${statusColor}20;
|
|
border: 1px solid ${statusColor}40;
|
|
border-radius: 6px;
|
|
color: ${statusColor};
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
">
|
|
<span style="font-size: 16px;">${statusIcon}</span>
|
|
${statusText}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Build modal body content (scrollable)
|
|
let modalBodyContent = '';
|
|
|
|
// Add errors section if present
|
|
if (hasErrors) {
|
|
modalBodyContent += `
|
|
<div style="margin-bottom: 24px;">
|
|
<h3 style="color: #f56565; font-size: 16px; font-weight: 600; margin: 0 0 12px 0; display: flex; align-items: center; gap: 8px;">
|
|
<span>✗</span>
|
|
Errors (${validatorData.errorCount || validatorData.errors.length})
|
|
</h3>
|
|
<div style="
|
|
background: rgba(245, 101, 101, 0.1);
|
|
border-left: 3px solid #f56565;
|
|
border-radius: 4px;
|
|
padding: 16px;
|
|
">
|
|
`;
|
|
|
|
validatorData.errors.forEach((error, index) => {
|
|
// Build location info if available
|
|
let locationInfo = '';
|
|
if (error.position || error.line) {
|
|
locationInfo = `
|
|
<div style="
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-top: 4px;
|
|
padding: 4px 8px;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
color: #999;
|
|
font-family: 'Courier New', monospace;
|
|
">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,7H13V13H11V7M11,15H13V17H11V15Z"/>
|
|
</svg>
|
|
${error.position ? `Line ${error.position}` : `Line ${error.line}` + (error.column ? `:${error.column}` : '')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Build line text preview if available
|
|
let lineTextPreview = '';
|
|
if (error.lineText) {
|
|
lineTextPreview = `
|
|
<div style="margin-top: 8px; padding: 8px 12px; background: rgba(0, 0, 0, 0.4); border-left: 2px solid #f56565; border-radius: 4px; font-family: 'Courier New', monospace; font-size: 12px; color: #ddd; white-space: normal; word-wrap: break-word; overflow-wrap: break-word;">
|
|
${error.lineText.replace(/</g, '<').replace(/>/g, '>')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Handle examples array (for patterns with multiple matches)
|
|
let examplesSection = '';
|
|
if (error.examples && error.examples.length > 0) {
|
|
examplesSection = `
|
|
<div style="margin-top: 12px;">
|
|
<div style="color: #999; font-size: 12px; margin-bottom: 6px; font-weight: 600;">
|
|
Found ${error.examples.length} occurrence${error.examples.length > 1 ? 's' : ''}:
|
|
</div>
|
|
`;
|
|
|
|
error.examples.slice(0, 5).forEach((example, exIdx) => {
|
|
examplesSection += `
|
|
<div style="margin-bottom: 8px; padding: 8px 12px; background: rgba(0, 0, 0, 0.3); border-radius: 4px;">
|
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
|
|
<span style="color: #f56565; font-size: 11px; font-weight: 600; font-family: 'Courier New', monospace;">
|
|
${example.position || `Line ${example.line}`}
|
|
</span>
|
|
${example.text ? `
|
|
<span style="color: #ed8936; font-size: 11px; font-family: 'Courier New', monospace;">
|
|
"${example.text.replace(/</g, '<').replace(/>/g, '>')}"
|
|
</span>
|
|
` : ''}
|
|
</div>
|
|
${example.lineText ? `
|
|
<div style="color: #999; font-size: 11px; font-family: 'Courier New', monospace; white-space: normal; word-wrap: break-word; overflow-wrap: break-word;">
|
|
${example.lineText.replace(/</g, '<').replace(/>/g, '>')}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
if (error.examples.length > 5) {
|
|
examplesSection += `
|
|
<div style="color: #666; font-size: 11px; font-style: italic; margin-top: 4px;">
|
|
... and ${error.examples.length - 5} more occurrence${error.examples.length - 5 > 1 ? 's' : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
examplesSection += `</div>`;
|
|
}
|
|
|
|
modalBodyContent += `
|
|
<div style="margin-bottom: ${index < validatorData.errors.length - 1 ? '12px' : '0'}; padding-bottom: ${index < validatorData.errors.length - 1 ? '12px' : '0'}; border-bottom: ${index < validatorData.errors.length - 1 ? '1px solid rgba(245, 101, 101, 0.2)' : 'none'};">
|
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px;">
|
|
<div style="color: #f56565; font-weight: 600; font-size: 13px;">
|
|
${error.code || 'ERROR'}
|
|
</div>
|
|
${locationInfo}
|
|
</div>
|
|
<div style="color: #ddd; font-size: 16px; line-height: 1.6; margin-bottom: 8px;">
|
|
${error.message || error}
|
|
</div>
|
|
${lineTextPreview}
|
|
${examplesSection}
|
|
${error.context && !error.examples ? `
|
|
<div style="margin-top: 8px; padding: 8px; background: rgba(0, 0, 0, 0.3); border-radius: 4px; font-family: 'Courier New', monospace; font-size: 12px; color: #999;">
|
|
${JSON.stringify(error.context, null, 2).replace(/\n/g, '<br>').replace(/ /g, ' ')}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
modalBodyContent += `
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Add warnings section if present
|
|
if (hasWarnings) {
|
|
modalBodyContent += `
|
|
<div style="margin-bottom: 24px;">
|
|
<h3 style="color: #ed8936; font-size: 16px; font-weight: 600; margin: 0 0 12px 0; display: flex; align-items: center; gap: 8px;">
|
|
<span>⚠</span>
|
|
Warnings (${validatorData.warningCount || validatorData.warnings.length})
|
|
</h3>
|
|
<div style="
|
|
background: rgba(237, 137, 54, 0.1);
|
|
border-left: 3px solid #ed8936;
|
|
border-radius: 4px;
|
|
padding: 16px;
|
|
">
|
|
`;
|
|
|
|
validatorData.warnings.forEach((warning, index) => {
|
|
// Build location info if available
|
|
let locationInfo = '';
|
|
if (warning.position || warning.line) {
|
|
locationInfo = `
|
|
<div style="
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-top: 4px;
|
|
padding: 4px 8px;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
color: #999;
|
|
font-family: 'Courier New', monospace;
|
|
">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,7H13V13H11V7M11,15H13V17H11V15Z"/>
|
|
</svg>
|
|
${warning.position ? `Line ${warning.position}` : `Line ${warning.line}` + (warning.column ? `:${warning.column}` : '')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Build line text preview if available
|
|
let lineTextPreview = '';
|
|
if (warning.lineText) {
|
|
lineTextPreview = `
|
|
<div style="margin-top: 8px; padding: 8px 12px; background: rgba(0, 0, 0, 0.4); border-left: 2px solid #ed8936; border-radius: 4px; font-family: 'Courier New', monospace; font-size: 12px; color: #ddd; white-space: normal; word-wrap: break-word; overflow-wrap: break-word;">
|
|
${warning.lineText.replace(/</g, '<').replace(/>/g, '>')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Handle examples array (for patterns with multiple matches)
|
|
let examplesSection = '';
|
|
if (warning.examples && warning.examples.length > 0) {
|
|
examplesSection = `
|
|
<div style="margin-top: 12px;">
|
|
<div style="color: #999; font-size: 12px; margin-bottom: 6px; font-weight: 600;">
|
|
Found ${warning.examples.length} occurrence${warning.examples.length > 1 ? 's' : ''}:
|
|
</div>
|
|
`;
|
|
|
|
warning.examples.slice(0, 5).forEach((example, exIdx) => {
|
|
examplesSection += `
|
|
<div style="margin-bottom: 8px; padding: 8px 12px; background: rgba(0, 0, 0, 0.3); border-radius: 4px;">
|
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
|
|
<span style="color: #ed8936; font-size: 11px; font-weight: 600; font-family: 'Courier New', monospace;">
|
|
${example.position || `Line ${example.line}`}
|
|
</span>
|
|
${example.text ? `
|
|
<span style="color: #f9a825; font-size: 11px; font-family: 'Courier New', monospace;">
|
|
"${example.text.replace(/</g, '<').replace(/>/g, '>')}"
|
|
</span>
|
|
` : ''}
|
|
</div>
|
|
${example.lineText ? `
|
|
<div style="color: #999; font-size: 11px; font-family: 'Courier New', monospace; white-space: normal; word-wrap: break-word; overflow-wrap: break-word;">
|
|
${example.lineText.replace(/</g, '<').replace(/>/g, '>')}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
if (warning.examples.length > 5) {
|
|
examplesSection += `
|
|
<div style="color: #666; font-size: 11px; font-style: italic; margin-top: 4px;">
|
|
... and ${warning.examples.length - 5} more occurrence${warning.examples.length - 5 > 1 ? 's' : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
examplesSection += `</div>`;
|
|
}
|
|
|
|
modalBodyContent += `
|
|
<div style="margin-bottom: ${index < validatorData.warnings.length - 1 ? '12px' : '0'}; padding-bottom: ${index < validatorData.warnings.length - 1 ? '12px' : '0'}; border-bottom: ${index < validatorData.warnings.length - 1 ? '1px solid rgba(237, 137, 54, 0.2)' : 'none'};">
|
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px;">
|
|
<div style="color: #ed8936; font-weight: 600; font-size: 13px;">
|
|
${warning.code || 'WARNING'}
|
|
</div>
|
|
${locationInfo}
|
|
</div>
|
|
<div style="color: #ddd; font-size: 16px; line-height: 1.6; margin-bottom: 8px;">
|
|
${warning.message || warning}
|
|
</div>
|
|
${lineTextPreview}
|
|
${examplesSection}
|
|
${warning.context && !warning.examples ? `
|
|
<div style="margin-top: 8px; padding: 8px; background: rgba(0, 0, 0, 0.3); border-radius: 4px; font-family: 'Courier New', monospace; font-size: 12px; color: #999;">
|
|
${JSON.stringify(warning.context, null, 2).replace(/\n/g, '<br>').replace(/ /g, ' ')}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
modalBodyContent += `
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Add success/info section if everything passed
|
|
if (!hasErrors && !hasWarnings && (hasInfo || passed)) {
|
|
// Generate validator-specific success summary
|
|
let successSummary = '';
|
|
|
|
switch(validatorKey) {
|
|
case 'structural':
|
|
successSummary = `
|
|
<div style="color: #bbb; font-size: 14px; line-height: 1.6; margin-bottom: 12px;">
|
|
This component has a valid structure with proper formatting:
|
|
</div>
|
|
<ul style="margin: 0; padding-left: 20px; color: #999; font-size: 13px; line-height: 1.8;">
|
|
<li>YAML frontmatter is properly formatted and contains all required fields</li>
|
|
<li>File size is within acceptable limits</li>
|
|
<li>UTF-8 encoding is valid with no binary content</li>
|
|
<li>Content structure follows markdown conventions</li>
|
|
<li>Section count is optimal for readability</li>
|
|
</ul>
|
|
`;
|
|
break;
|
|
|
|
case 'integrity':
|
|
successSummary = `
|
|
<div style="color: #bbb; font-size: 14px; line-height: 1.6; margin-bottom: 12px;">
|
|
This component's integrity has been verified:
|
|
</div>
|
|
<ul style="margin: 0; padding-left: 20px; color: #999; font-size: 13px; line-height: 1.8;">
|
|
<li>SHA256 hash matches expected value</li>
|
|
<li>No tampering or unauthorized modifications detected</li>
|
|
<li>Version tracking is consistent</li>
|
|
<li>File has not been corrupted during transmission</li>
|
|
</ul>
|
|
`;
|
|
break;
|
|
|
|
case 'semantic':
|
|
successSummary = `
|
|
<div style="color: #bbb; font-size: 14px; line-height: 1.6; margin-bottom: 12px;">
|
|
This component is safe and contains no malicious content:
|
|
</div>
|
|
<ul style="margin: 0; padding-left: 20px; color: #999; font-size: 13px; line-height: 1.8;">
|
|
<li>No prompt injection attempts detected</li>
|
|
<li>No dangerous shell commands or system calls</li>
|
|
<li>No obfuscated or suspicious code patterns</li>
|
|
<li>No attempts to access sensitive file paths</li>
|
|
<li>Content follows security best practices</li>
|
|
</ul>
|
|
`;
|
|
break;
|
|
|
|
case 'references':
|
|
successSummary = `
|
|
<div style="color: #bbb; font-size: 14px; line-height: 1.6; margin-bottom: 12px;">
|
|
All external references have been validated:
|
|
</div>
|
|
<ul style="margin: 0; padding-left: 20px; color: #999; font-size: 13px; line-height: 1.8;">
|
|
<li>All URLs are properly formatted and accessible</li>
|
|
<li>No SSRF (Server-Side Request Forgery) vulnerabilities</li>
|
|
<li>External links point to trusted domains</li>
|
|
<li>No private network or localhost references</li>
|
|
<li>HTTP links have been upgraded to HTTPS where possible</li>
|
|
</ul>
|
|
`;
|
|
break;
|
|
|
|
case 'provenance':
|
|
successSummary = `
|
|
<div style="color: #bbb; font-size: 14px; line-height: 1.6; margin-bottom: 12px;">
|
|
Component origin and authorship have been confirmed:
|
|
</div>
|
|
<ul style="margin: 0; padding-left: 20px; color: #999; font-size: 13px; line-height: 1.8;">
|
|
<li>Author metadata is present and valid</li>
|
|
<li>Repository information is correct</li>
|
|
<li>License information is specified</li>
|
|
<li>Component source can be traced to original repository</li>
|
|
<li>No signs of unauthorized redistribution</li>
|
|
</ul>
|
|
`;
|
|
break;
|
|
|
|
default:
|
|
successSummary = `
|
|
<div style="color: #bbb; font-size: 14px; line-height: 1.6;">
|
|
This component has been validated and meets all ${validatorLabel.toLowerCase()} requirements.
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
modalBodyContent += `
|
|
<div style="margin-bottom: 24px;">
|
|
<h3 style="color: #48bb78; font-size: 16px; font-weight: 600; margin: 0 0 12px 0; display: flex; align-items: center; gap: 8px;">
|
|
<span>✓</span>
|
|
Validation Passed
|
|
</h3>
|
|
<div style="
|
|
background: rgba(72, 187, 120, 0.1);
|
|
border-left: 3px solid #48bb78;
|
|
border-radius: 4px;
|
|
padding: 16px;
|
|
">
|
|
<div style="color: #48bb78; font-weight: 600; font-size: 14px; margin-bottom: 12px;">
|
|
All ${validatorLabel} checks passed successfully
|
|
</div>
|
|
${successSummary}
|
|
`;
|
|
|
|
// Add info items if present
|
|
if (hasInfo) {
|
|
modalBodyContent += `
|
|
<div style="margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(72, 187, 120, 0.2);">
|
|
<div style="color: #48bb78; font-weight: 600; font-size: 13px; margin-bottom: 8px;">
|
|
Additional Details:
|
|
</div>
|
|
`;
|
|
validatorData.info.forEach((info, index) => {
|
|
modalBodyContent += `
|
|
<div style="margin-bottom: ${index < validatorData.info.length - 1 ? '8px' : '0'}; color: #999; font-size: 13px; padding-left: 12px;">
|
|
• ${info.message || info}
|
|
</div>
|
|
`;
|
|
});
|
|
modalBodyContent += `
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
modalBodyContent += `
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Build modal footer (fixed)
|
|
let modalFooter = `
|
|
<div style="display: flex; justify-content: flex-end; margin-top: 24px; padding-top: 24px; border-top: 1px solid #333;">
|
|
<button id="closeValidationModalBtn" style="
|
|
background: #2d2d2d;
|
|
border: 1px solid #444;
|
|
color: #fff;
|
|
padding: 10px 24px;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
" onmouseover="this.style.background='#3d3d3d'; this.style.borderColor='#555'" onmouseout="this.style.background='#2d2d2d'; this.style.borderColor='#444'">
|
|
Close
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
// Assemble the complete modal with scrollable body
|
|
modalContainer.innerHTML = `
|
|
${modalHeader}
|
|
<div style="flex: 1; overflow-y: auto; padding: 30px;">
|
|
${modalBodyContent}
|
|
</div>
|
|
<div style="flex-shrink: 0; padding: 20px 30px; border-top: 1px solid #333;">
|
|
${modalFooter}
|
|
</div>
|
|
`;
|
|
modalOverlay.appendChild(modalContainer);
|
|
document.body.appendChild(modalOverlay);
|
|
|
|
// Close modal handlers
|
|
function closeModal() {
|
|
document.body.removeChild(modalOverlay);
|
|
}
|
|
|
|
document.getElementById('closeValidationModal').addEventListener('click', closeModal);
|
|
document.getElementById('closeValidationModalBtn').addEventListener('click', closeModal);
|
|
|
|
// Close on overlay click (but not on modal content click)
|
|
modalOverlay.addEventListener('click', (e) => {
|
|
if (e.target === modalOverlay) {
|
|
closeModal();
|
|
}
|
|
});
|
|
|
|
// Close on Escape key
|
|
function handleEscape(e) {
|
|
if (e.key === 'Escape') {
|
|
closeModal();
|
|
document.removeEventListener('keydown', handleEscape);
|
|
}
|
|
}
|
|
document.addEventListener('keydown', handleEscape);
|
|
}; |