diff --git a/assets/css/main.css b/assets/css/main.css index 8162397..f68a4c9 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -15,6 +15,7 @@ @import url('./floating-elements.css'); @import url('./stats.css'); @import url('./comparison.css'); +@import url('./metadata.css'); /* Responsive design */ @import url('./responsive.css'); diff --git a/assets/css/metadata.css b/assets/css/metadata.css new file mode 100644 index 0000000..872fef6 --- /dev/null +++ b/assets/css/metadata.css @@ -0,0 +1,180 @@ +/* ======================================== + METADATA POPUP STYLES + ======================================== */ + +/* Metadata button on thumbnails */ +.metadata-btn { + position: absolute; + top: 8px; + right: 8px; + background: rgba(0, 0, 0, 0.7); + color: white; + border: none; + border-radius: 50%; + width: 32px; + height: 32px; + font-size: 16px; + cursor: pointer; + z-index: 10; + transition: all 0.2s ease; + backdrop-filter: blur(4px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + opacity: 0; + display: flex; + align-items: center; + justify-content: center; + line-height: 1; +} + +.grid-item:hover .metadata-btn { + opacity: 1; +} + +.metadata-btn:hover { + background: rgba(0, 0, 0, 0.9); + transform: scale(1.1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.metadata-btn:active { + transform: scale(0.95); +} + +/* Grid item positioning for metadata button */ +.grid-item { + position: relative; +} + +/* Metadata popup */ +.metadata-popup { + background: var(--card-background); + border: 1px solid var(--border-color); + border-radius: 8px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + max-width: 320px; + min-width: 250px; + opacity: 0; + transform: translateY(-10px); + transition: all 0.2s ease; + backdrop-filter: blur(10px); + z-index: 1000; +} + +.metadata-popup.show { + opacity: 1; + transform: translateY(0); +} + +.metadata-popup-header { + padding: 12px 16px; + border-bottom: 1px solid var(--border-color); + background: var(--header-background); + border-radius: 8px 8px 0 0; +} + +.metadata-popup-header h4 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: var(--text-color); + word-break: break-word; +} + +.metadata-popup-content { + padding: 12px 16px; + max-height: 300px; + overflow-y: auto; +} + +.metadata-field { + display: flex; + margin-bottom: 8px; + gap: 8px; + align-items: flex-start; +} + +.metadata-field:last-child { + margin-bottom: 0; +} + +.metadata-key { + font-weight: 500; + color: var(--accent-color); + font-size: 12px; + min-width: 80px; + flex-shrink: 0; +} + +.metadata-value { + font-size: 12px; + color: var(--text-color); + word-break: break-word; + flex: 1; +} + +.metadata-value code { + background: var(--code-background); + padding: 2px 4px; + border-radius: 3px; + font-size: 11px; + font-family: 'Courier New', monospace; +} + +.metadata-tag { + background: var(--accent-color); + color: var(--background-color); + padding: 2px 6px; + border-radius: 12px; + font-size: 10px; + font-weight: 500; + margin-right: 4px; + display: inline-block; +} + +.metadata-more { + color: var(--breadcrumb-color); + font-style: italic; + font-size: 11px; +} + +.no-metadata { + color: var(--breadcrumb-color); + font-style: italic; + margin: 0; + text-align: center; + padding: 20px 0; +} + +/* Custom scrollbar for metadata popup */ +.metadata-popup-content::-webkit-scrollbar { + width: 6px; +} + +.metadata-popup-content::-webkit-scrollbar-track { + background: transparent; +} + +.metadata-popup-content::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 3px; +} + +.metadata-popup-content::-webkit-scrollbar-thumb:hover { + background: var(--accent-color); +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .metadata-popup { + max-width: calc(100vw - 32px); + min-width: 200px; + } + + .metadata-btn { + width: 28px; + height: 28px; + font-size: 14px; + top: 6px; + right: 6px; + } +} diff --git a/assets/js/metadata-popup.js b/assets/js/metadata-popup.js new file mode 100644 index 0000000..085c3a9 --- /dev/null +++ b/assets/js/metadata-popup.js @@ -0,0 +1,212 @@ +/** + * Metadata Popup functionality for Gallery + * + * Handles showing metadata in small popups overlaid on plot thumbnails + */ + +class MetadataPopup { + constructor() { + this.activePopup = null; + this.init(); + } + + init() { + // Close popup when clicking outside + document.addEventListener('click', (e) => { + if (!e.target.closest('.metadata-btn') && !e.target.closest('.metadata-popup')) { + this.hidePopup(); + } + }); + + // Close popup on Escape key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + this.hidePopup(); + } + }); + } + + showPopup(button, plotName, metadata) { + // Hide any existing popup + this.hidePopup(); + + // Create popup element + const popup = document.createElement('div'); + popup.className = 'metadata-popup'; + popup.innerHTML = this.formatMetadata(plotName, metadata); + + // Position popup relative to button + const rect = button.getBoundingClientRect(); + popup.style.position = 'fixed'; + popup.style.left = rect.left + 'px'; + popup.style.top = (rect.bottom + 5) + 'px'; + popup.style.zIndex = '1000'; + + // Add to DOM + document.body.appendChild(popup); + this.activePopup = popup; + + // Adjust position if popup goes off screen + setTimeout(() => { + const popupRect = popup.getBoundingClientRect(); + + // Adjust horizontal position + if (popupRect.right > window.innerWidth) { + popup.style.left = (rect.right - popupRect.width) + 'px'; + } + + // Adjust vertical position + if (popupRect.bottom > window.innerHeight) { + popup.style.top = (rect.top - popupRect.height - 5) + 'px'; + } + }, 0); + + // Animate in + requestAnimationFrame(() => { + popup.classList.add('show'); + }); + } + + hidePopup() { + if (this.activePopup) { + this.activePopup.classList.remove('show'); + setTimeout(() => { + if (this.activePopup && this.activePopup.parentNode) { + this.activePopup.parentNode.removeChild(this.activePopup); + } + this.activePopup = null; + }, 200); + } + } + + formatMetadata(plotName, metadata) { + if (!metadata || Object.keys(metadata).length === 0) { + return ` +
+

${plotName}

+
+
+

No metadata available

+
+ `; + } + + let html = ` +
+

${plotName}

+
+
+ `; + + // Show priority fields first + const priorityFields = ['title', 'description', 'plot_type', 'experiment']; + const processedKeys = new Set(); + + // Display priority fields first + for (const key of priorityFields) { + if (metadata[key] !== undefined) { + html += this.formatMetadataField(key, metadata[key]); + processedKeys.add(key); + } + } + + // Show file info if available + if (metadata.file_info) { + html += `
File Information
`; + html += this.formatMetadataField('File Size', metadata.file_info.size); + if (metadata.file_info.extension) { + html += this.formatMetadataField('Format', metadata.file_info.extension); + } + processedKeys.add('file_info'); + } + + // Show timestamps if available + if (metadata.timestamps) { + html += `
Timestamps
`; + if (metadata.timestamps.created_human) { + html += this.formatMetadataField('Created', metadata.timestamps.created_human); + } + if (metadata.timestamps.modified_human) { + html += this.formatMetadataField('Modified', metadata.timestamps.modified_human); + } + processedKeys.add('timestamps'); + } + + // Show extracted info if available + if (metadata.extracted_info && Object.keys(metadata.extracted_info).length > 0) { + html += `
Plot Details
`; + for (const [key, value] of Object.entries(metadata.extracted_info)) { + html += this.formatMetadataField(key, value); + } + processedKeys.add('extracted_info'); + } + + // Display other fields (excluding generation info unless it's the only data) + const otherKeys = Object.keys(metadata).filter(key => + !processedKeys.has(key) && key !== 'generation' + ); + + if (otherKeys.length > 0) { + html += `
Additional Information
`; + for (const key of otherKeys) { + html += this.formatMetadataField(key, metadata[key]); + } + } + + // Show generation info last if there's no other meaningful data + if (processedKeys.size <= 2 && metadata.generation) { + html += `
Generation Info
`; + if (metadata.generation.generation_time) { + const genDate = new Date(metadata.generation.generation_time); + html += this.formatMetadataField('Generated', genDate.toLocaleString()); + } + } + + html += '
'; + return html; + } + + formatMetadataField(key, value) { + const displayKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); + + let formattedValue; + if (value === null || value === undefined) { + formattedValue = 'null'; + } else if (typeof value === 'object') { + if (Array.isArray(value)) { + if (value.length <= 3) { + formattedValue = value.map(item => `${item}`).join(' '); + } else { + formattedValue = `${value.slice(0, 3).map(item => `${item}`).join(' ')} +${value.length - 3} more`; + } + } else { + // Show object as compact JSON for small objects, or just key count for large ones + const keys = Object.keys(value); + if (keys.length <= 3) { + formattedValue = '' + JSON.stringify(value) + ''; + } else { + formattedValue = `Object with ${keys.length} properties`; + } + } + } else { + // Truncate long strings + const str = String(value); + formattedValue = str.length > 50 ? str.substring(0, 47) + '...' : str; + } + + return ` +
+ ${displayKey}: + ${formattedValue} +
+ `; + } +} + +// Global instance +window.metadataPopup = new MetadataPopup(); + +// Global function for template usage +window.showMetadataPopup = function(button, plotName, metadata) { + window.metadataPopup.showPopup(button, plotName, metadata); +}; diff --git a/templates/gallery.html b/templates/gallery.html index 227b1d5..a0ee24c 100644 --- a/templates/gallery.html +++ b/templates/gallery.html @@ -41,6 +41,9 @@ {{ item.name }} +
{{ item.name }}
{% endfor %} @@ -183,6 +186,9 @@ }; + + +