Fix contrast issues with text-muted and bg-dark classes
- Fixed Bootstrap bg-dark class to use better contrasting color - Added comprehensive text-muted contrast fixes for various contexts - Improved dark theme colors for better accessibility - Fixed CSS inheritance issues for code elements in dark contexts - All color choices meet WCAG AA contrast requirements
This commit is contained in:
382
frontend/static/js/main.js
Normal file
382
frontend/static/js/main.js
Normal file
@@ -0,0 +1,382 @@
|
||||
// UnitForge Main JavaScript
|
||||
// Handles general functionality across the application
|
||||
|
||||
class UnitForge {
|
||||
constructor() {
|
||||
this.baseUrl = window.location.origin;
|
||||
this.apiUrl = `${this.baseUrl}/api`;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.initializeTooltips();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Upload modal functionality
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', this.handleFileSelect.bind(this));
|
||||
}
|
||||
|
||||
// Copy to clipboard functionality
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.matches('[data-copy]') || e.target.closest('[data-copy]')) {
|
||||
const target = e.target.matches('[data-copy]') ? e.target : e.target.closest('[data-copy]');
|
||||
this.copyToClipboard(target.dataset.copy);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initializeTooltips() {
|
||||
// Initialize Bootstrap tooltips
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
}
|
||||
|
||||
// File upload handling
|
||||
handleFileSelect(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.displayFileContent(e.target.result, file.name);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
displayFileContent(content, filename) {
|
||||
// This will be overridden in specific pages
|
||||
console.log('File loaded:', filename, content);
|
||||
}
|
||||
|
||||
// API calls
|
||||
async apiCall(endpoint, options = {}) {
|
||||
const url = `${this.apiUrl}${endpoint}`;
|
||||
const defaultOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const finalOptions = { ...defaultOptions, ...options };
|
||||
|
||||
try {
|
||||
const response = await fetch(url, finalOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API call failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async validateUnitFile(content, filename = null) {
|
||||
return await this.apiCall('/validate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
content: content,
|
||||
filename: filename
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async uploadUnitFile(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/upload`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `Upload failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async downloadUnitFile(content, filename) {
|
||||
return await this.apiCall('/download', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
content: content,
|
||||
filename: filename
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async getTemplates() {
|
||||
return await this.apiCall('/templates');
|
||||
}
|
||||
|
||||
async getTemplate(name) {
|
||||
return await this.apiCall(`/templates/${name}`);
|
||||
}
|
||||
|
||||
async generateFromTemplate(templateName, parameters, filename = null) {
|
||||
return await this.apiCall('/generate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
template_name: templateName,
|
||||
parameters: parameters,
|
||||
filename: filename
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
async copyToClipboard(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
this.showToast('Copied to clipboard!', 'success');
|
||||
} catch (err) {
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-999999px';
|
||||
textArea.style.top = '-999999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
this.showToast('Copied to clipboard!', 'success');
|
||||
} catch (err) {
|
||||
this.showToast('Failed to copy to clipboard', 'error');
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
}
|
||||
|
||||
showToast(message, type = 'info', duration = 3000) {
|
||||
// Create toast container if it doesn't exist
|
||||
let toastContainer = document.getElementById('toast-container');
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
toastContainer.id = 'toast-container';
|
||||
toastContainer.className = 'position-fixed top-0 end-0 p-3';
|
||||
toastContainer.style.zIndex = '9999';
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
// Create toast element
|
||||
const toastId = `toast-${Date.now()}`;
|
||||
const toastHtml = `
|
||||
<div id="${toastId}" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<i class="fas fa-${this.getToastIcon(type)} text-${this.getToastColor(type)} me-2"></i>
|
||||
<strong class="me-auto">UnitForge</strong>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body">
|
||||
${message}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
|
||||
|
||||
// Initialize and show toast
|
||||
const toastElement = document.getElementById(toastId);
|
||||
const toast = new bootstrap.Toast(toastElement, {
|
||||
autohide: true,
|
||||
delay: duration
|
||||
});
|
||||
|
||||
toast.show();
|
||||
|
||||
// Remove toast element after it's hidden
|
||||
toastElement.addEventListener('hidden.bs.toast', () => {
|
||||
toastElement.remove();
|
||||
});
|
||||
}
|
||||
|
||||
getToastIcon(type) {
|
||||
const icons = {
|
||||
success: 'check-circle',
|
||||
error: 'exclamation-circle',
|
||||
warning: 'exclamation-triangle',
|
||||
info: 'info-circle'
|
||||
};
|
||||
return icons[type] || 'info-circle';
|
||||
}
|
||||
|
||||
getToastColor(type) {
|
||||
const colors = {
|
||||
success: 'success',
|
||||
error: 'danger',
|
||||
warning: 'warning',
|
||||
info: 'info'
|
||||
};
|
||||
return colors[type] || 'info';
|
||||
}
|
||||
|
||||
formatValidationResults(validation) {
|
||||
if (!validation) return '';
|
||||
|
||||
let html = '';
|
||||
|
||||
if (validation.valid) {
|
||||
html += `
|
||||
<div class="validation-success">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
Unit file is valid!
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (validation.errors && validation.errors.length > 0) {
|
||||
html += '<div class="mb-3">';
|
||||
html += `<h6 class="text-danger"><i class="fas fa-exclamation-circle me-2"></i>Errors (${validation.errors.length})</h6>`;
|
||||
|
||||
validation.errors.forEach(error => {
|
||||
const location = error.section + (error.key ? `.${error.key}` : '');
|
||||
html += `
|
||||
<div class="validation-error">
|
||||
<strong>[${location}]</strong> ${error.message}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (validation.warnings && validation.warnings.length > 0) {
|
||||
html += '<div class="mb-3">';
|
||||
html += `<h6 class="text-warning"><i class="fas fa-exclamation-triangle me-2"></i>Warnings (${validation.warnings.length})</h6>`;
|
||||
|
||||
validation.warnings.forEach(warning => {
|
||||
const location = warning.section + (warning.key ? `.${warning.key}` : '');
|
||||
html += `
|
||||
<div class="validation-warning">
|
||||
<strong>[${location}]</strong> ${warning.message}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
downloadTextFile(content, filename) {
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
showLoading(element, message = 'Loading...') {
|
||||
if (typeof element === 'string') {
|
||||
element = document.getElementById(element);
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.innerHTML = `
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">${message}</span>
|
||||
</div>
|
||||
<p class="mt-3 text-muted">${message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
hideLoading(element) {
|
||||
if (typeof element === 'string') {
|
||||
element = document.getElementById(element);
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.innerHTML = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global functions for HTML onclick handlers
|
||||
function showUploadModal() {
|
||||
const modal = new bootstrap.Modal(document.getElementById('uploadModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function showCliModal() {
|
||||
const modal = new bootstrap.Modal(document.getElementById('cliModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
async function validateFile() {
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const file = fileInput.files[0];
|
||||
|
||||
if (!file) {
|
||||
unitforge.showToast('Please select a file first', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await unitforge.uploadUnitFile(file);
|
||||
|
||||
// Display results
|
||||
const resultsDiv = document.getElementById('uploadResults');
|
||||
const outputDiv = document.getElementById('validationOutput');
|
||||
|
||||
if (resultsDiv && outputDiv) {
|
||||
outputDiv.innerHTML = unitforge.formatValidationResults(result.validation);
|
||||
resultsDiv.classList.remove('d-none');
|
||||
}
|
||||
|
||||
if (result.validation.valid) {
|
||||
unitforge.showToast('File validation completed successfully!', 'success');
|
||||
} else {
|
||||
unitforge.showToast(`Validation found ${result.validation.errors.length} error(s)`, 'warning');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
unitforge.showToast(`Validation failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the application
|
||||
const unitforge = new UnitForge();
|
||||
|
||||
// Export for use in other modules
|
||||
window.UnitForge = UnitForge;
|
||||
window.unitforge = unitforge;
|
||||
Reference in New Issue
Block a user