From 6738c4ca69391a6fc9bdf0bc7136ae1c7e267684 Mon Sep 17 00:00:00 2001 From: Kylian Schmidt Date: Wed, 6 May 2026 08:34:40 +0200 Subject: [PATCH] Refactor: Remove obsolete JavaScript managers and utilities - Deleted NavigationManager, RecentPlotsManager, SearchManager, SortManager, StatsManager, ThemeManager, Utils, and ViewManager classes. - Updated gallery builder to improve asset copying logic with a more efficient modification time check. - Cleaned up gallery HTML template by removing subdirectory listing and embedding gallery data as JSON. - Added CLAUDE.md for project documentation and guidelines. --- CLAUDE.md | 112 ++++++ assets/css/folder-tree.css | 9 +- assets/js/navigation-manager.js | 144 ++++--- gallery/api.py | 16 +- gallery/assets | 1 + gallery/assets/css/base.css | 71 ---- gallery/assets/css/comparison.css | 180 --------- gallery/assets/css/export.css | 440 --------------------- gallery/assets/css/floating-elements.css | 112 ------ gallery/assets/css/folder-metadata.css | 179 --------- gallery/assets/css/folder-tree.css | 40 -- gallery/assets/css/grid.css | 106 ----- gallery/assets/css/html-plots.css | 38 -- gallery/assets/css/main.css | 33 -- gallery/assets/css/metadata-section.css | 383 ------------------ gallery/assets/css/metadata.css | 180 --------- gallery/assets/css/navigation.css | 57 --- gallery/assets/css/responsive.css | 59 --- gallery/assets/css/search.css | 72 ---- gallery/assets/css/sidebar.css | 121 ------ gallery/assets/css/sort-controls.css | 88 ----- gallery/assets/css/sort-debug.css | 0 gallery/assets/css/stats.css | 59 --- gallery/assets/css/variables.css | 33 -- gallery/assets/css/view-controls.css | 404 ------------------- gallery/assets/css/view-override.css | 54 --- gallery/assets/js/comparison-manager.js | 180 --------- gallery/assets/js/export-manager.js | 448 ---------------------- gallery/assets/js/folder-metadata.js | 81 ---- gallery/assets/js/gallery-app.js | 73 ---- gallery/assets/js/keyboard-manager.js | 130 ------- gallery/assets/js/main.js | 27 -- gallery/assets/js/metadata-popup.js | 212 ---------- gallery/assets/js/metadata-section.js | 142 ------- gallery/assets/js/navigation-manager.js | 208 ---------- gallery/assets/js/recent-plots-manager.js | 114 ------ gallery/assets/js/search-manager.js | 217 ----------- gallery/assets/js/sort-manager.js | 197 ---------- gallery/assets/js/stats-manager.js | 54 --- gallery/assets/js/theme-manager.js | 43 --- gallery/assets/js/utils.js | 107 ------ gallery/assets/js/view-manager.js | 192 ---------- gallery/builder.py | 17 +- gallery/templates/gallery.html | 11 +- 44 files changed, 205 insertions(+), 5239 deletions(-) create mode 100644 CLAUDE.md create mode 120000 gallery/assets delete mode 100644 gallery/assets/css/base.css delete mode 100644 gallery/assets/css/comparison.css delete mode 100644 gallery/assets/css/export.css delete mode 100644 gallery/assets/css/floating-elements.css delete mode 100644 gallery/assets/css/folder-metadata.css delete mode 100644 gallery/assets/css/folder-tree.css delete mode 100644 gallery/assets/css/grid.css delete mode 100644 gallery/assets/css/html-plots.css delete mode 100644 gallery/assets/css/main.css delete mode 100644 gallery/assets/css/metadata-section.css delete mode 100644 gallery/assets/css/metadata.css delete mode 100644 gallery/assets/css/navigation.css delete mode 100644 gallery/assets/css/responsive.css delete mode 100644 gallery/assets/css/search.css delete mode 100644 gallery/assets/css/sidebar.css delete mode 100644 gallery/assets/css/sort-controls.css delete mode 100644 gallery/assets/css/sort-debug.css delete mode 100644 gallery/assets/css/stats.css delete mode 100644 gallery/assets/css/variables.css delete mode 100644 gallery/assets/css/view-controls.css delete mode 100644 gallery/assets/css/view-override.css delete mode 100644 gallery/assets/js/comparison-manager.js delete mode 100644 gallery/assets/js/export-manager.js delete mode 100644 gallery/assets/js/folder-metadata.js delete mode 100644 gallery/assets/js/gallery-app.js delete mode 100644 gallery/assets/js/keyboard-manager.js delete mode 100644 gallery/assets/js/main.js delete mode 100644 gallery/assets/js/metadata-popup.js delete mode 100644 gallery/assets/js/metadata-section.js delete mode 100644 gallery/assets/js/navigation-manager.js delete mode 100644 gallery/assets/js/recent-plots-manager.js delete mode 100644 gallery/assets/js/search-manager.js delete mode 100644 gallery/assets/js/sort-manager.js delete mode 100644 gallery/assets/js/stats-manager.js delete mode 100644 gallery/assets/js/theme-manager.js delete mode 100644 gallery/assets/js/utils.js delete mode 100644 gallery/assets/js/view-manager.js diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..66fef6d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,112 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What This Project Does + +A Python package that generates responsive static HTML galleries from scientific plot collections (PDFs and HTMLs). It converts PDFs to PNGs via ImageMagick, organizes plots hierarchically, propagates YAML/JSON metadata through directory trees, and renders everything via a Jinja2 template into a static website served from a web directory. + +## Commands + +```bash +# Install the package (editable) +pip install -e ".[dev]" + +# Run all tests +pytest tests/ + +# Run a single test file +pytest tests/test_generate_gallery.py -v + +# Run a single test by name +pytest tests/test_generate_gallery.py::test_needs_update_missing_target -v + +# Generate gallery (CLI entry point after install) +gallery --config config.yaml --verbose + +# Generate gallery (legacy script) +python generate_gallery.py --verbose + +# Incremental update for one source only +gallery --source /path/to/plots --verbose + +# Clean regeneration +gallery --clean --verbose + +# Serve output locally +python -m http.server 8000 -d /web/kschmidt/public_html/ +``` + +Code style: black with `line-length = 120`. + +## Architecture + +### Execution Flow + +``` +generate() [api.py] + └── copy_assets() [builder.py] — copies assets/ to web output dir + └── get_template() [builder.py] — loads gallery/templates/gallery.html + └── build_gallery() [builder.py] — recursive per-source-directory walk + └── load_folder_metadata() [utils/metadata.py] + └── merge_metadata() [utils/metadata.py] — inherits from parent + └── process_plot_files() [utils/processing.py] — PDF→PNG, copy files + └── save_metadata_cache() [utils/metadata.py] + └── render_gallery_page() [utils/processing.py] — writes index.html + └── recurse into subdirs +``` + +### Key Files + +| File | Role | +|------|------| +| `gallery/api.py` | `generate()` — primary public entry point; orchestrates everything | +| `gallery/builder.py` | `build_gallery()` — recursive traversal; `get_template()`, `copy_assets()` | +| `gallery/config.py` | `GalleryConfig`, `GallerySource`, `GalleryDefaults` dataclasses; YAML loading | +| `gallery/cli.py` | CLI (`gallery` command) wrapping `generate()` | +| `gallery/utils/processing.py` | PDF→PNG via ImageMagick subprocess; `needs_update()` timestamp check; `render_gallery_page()` | +| `gallery/utils/metadata.py` | Load/merge/cache YAML+JSON metadata; per-plot metadata resolution | +| `gallery/utils/stats.py` | Directory size/count statistics | +| `gallery/templates/gallery.html` | Single Jinja2 template for all gallery pages | +| `assets/js/` | Vanilla JS modules loaded as ES modules; `GalleryApp` in `gallery-app.js` orchestrates all managers | +| `assets/css/` | Modular CSS; `main.css` imports all others via `@import` | +| `generate_gallery.py` | Legacy CLI wrapper — kept for backward compatibility | +| `config.yaml` | Local deployment config (paths are machine-specific) | + +### Config File Format + +The YAML config uses a specific structure (not flat — must match `GalleryConfig.from_yaml()`): + +```yaml +paths: + web_folder: "/web/user/public_html" # required +gallery: + plot_root: "gallery" + png_dpi: 400 +sources: + - name: "my_plots" + path: "/path/to/plots" +metadata: + cache_enabled: true + inherit_from_parent: true +``` + +### Incremental Updates + +`needs_update(source, target)` uses a **30-second buffer** on mtime comparisons to handle filesystem timing. This is intentional — avoid tightening it. + +When `source_to_update` is passed to `generate()`, only that source's subdirectory is deleted and rebuilt; all other sources stay intact and the root index is re-rendered to include them. + +### Metadata Inheritance + +`metadata.yaml` (or `.yml`/`.json`) in any source directory is loaded and **merged with parent metadata** (`inherit_from_parent=True` by default). Child directories override parent keys. Per-plot overrides can live in `.yaml` files alongside the plot. + +### Frontend (Static JS/CSS) + +The frontend is vanilla ES modules — no build step. `assets/js/main.js` imports `GalleryApp` from `gallery-app.js`, which instantiates all manager classes (`ThemeManager`, `SearchManager`, `NavigationManager`, etc.). Each manager is self-contained. The template embeds gallery data as JSON in the page; JS reads it at runtime. + +Assets are served from `/assets/` relative to gallery pages. The Python code calculates the correct `../` depth per page when rendering the template. + +### Deployment + +The project ships a `Singularity.def` / `web.sif` Apptainer container for HPC environments. CI (`.gitlab-ci.yml`) builds the container and runs pytest inside it. For local development the `.venv` is sufficient. diff --git a/assets/css/folder-tree.css b/assets/css/folder-tree.css index 3c394ad..d6d8f24 100644 --- a/assets/css/folder-tree.css +++ b/assets/css/folder-tree.css @@ -2,16 +2,9 @@ FOLDER TREE ======================================== */ .folder-tree { - background: var(--tree-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 1rem; - margin: 1rem 0; font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace; font-size: 0.85rem; - max-height: 350px; - overflow-y: auto; - transition: all 0.3s ease; + margin: 1rem 0; } .tree-item { diff --git a/assets/js/navigation-manager.js b/assets/js/navigation-manager.js index b9a69d6..71f4bc9 100644 --- a/assets/js/navigation-manager.js +++ b/assets/js/navigation-manager.js @@ -9,20 +9,20 @@ export class NavigationManager { const currentPath = window.location.pathname; const pathParts = currentPath.split('/').filter(part => part !== '' && part !== 'index.html'); const breadcrumb = document.getElementById('breadcrumb'); - + if (!breadcrumb) return; - + if (pathParts.length === 0) { breadcrumb.innerHTML = '🏠 Root'; return; } - + let html = '🏠 Root'; - + for (let i = 0; i < pathParts.length; i++) { const part = pathParts[i]; html += '/'; - + if (i === pathParts.length - 1) { html += `${decodeURIComponent(part)}`; } else { @@ -31,19 +31,35 @@ export class NavigationManager { html += `${decodeURIComponent(part)}`; } } - + breadcrumb.innerHTML = html; } + /** + * Extract subdirs and item count from a parsed page document. + * Reads from the embedded #gallery-data JSON element. + */ + extractPageData(doc) { + const dataEl = doc.getElementById('gallery-data'); + if (dataEl) { + try { + const data = JSON.parse(dataEl.textContent); + return { subdirs: data.subdirs || [], itemCount: data.item_count || 0 }; + } catch {} + } + // Fallback for pages that pre-date the data element + return { subdirs: [], itemCount: doc.querySelectorAll('.grid-item').length }; + } + /** * Build and display the folder tree structure */ async buildFolderTree() { const treeContainer = document.getElementById('folderTree'); const currentPath = window.location.pathname; - + if (!treeContainer) return; - + try { const tree = await this.buildTreeRecursive(currentPath, 0, currentPath); treeContainer.innerHTML = tree; @@ -61,52 +77,41 @@ export class NavigationManager { const indent = ' '.repeat(depth); return `
${indent}└─ ...
`; } - + try { const response = await fetch(path); const htmlContent = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(htmlContent, 'text/html'); - - const subdirs = doc.querySelectorAll('h2 + ul li a'); - const items = doc.querySelectorAll('.grid-item'); - + + const { subdirs, itemCount } = this.extractPageData(doc); + const baseUrl = path.replace(/\/[^\/]*$/, '/'); + // Check if this is an empty directory (only one subdirectory, no items) - if (items.length === 0 && subdirs.length === 1) { - const subdir = subdirs[0]; - const href = subdir.getAttribute('href'); - if (href) { - const baseUrl = path.replace(/\/[^\/]*$/, '/'); - const subPath = baseUrl + href; - - const collapsedPath = await this.getCollapsedPath(path, subPath); - return await this.buildCollapsedTreeItem(collapsedPath, depth, currentPath, maxDepth); - } + if (itemCount === 0 && subdirs.length === 1) { + const subPath = baseUrl + subdirs[0] + '/index.html'; + const collapsedPath = await this.getCollapsedPath(path, subPath); + return await this.buildCollapsedTreeItem(collapsedPath, depth, currentPath, maxDepth); } - + // Normal directory processing const indent = ' '.repeat(depth); const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery'; - const totalItems = items.length + subdirs.length; + const totalItems = itemCount + subdirs.length; const arrow = depth === 0 ? '' : '└─ '; - + let html = ''; if (path === currentPath) { html += `
${indent}${arrow}📁 ${folderName} (${totalItems} items)
`; } else { html += `
${indent}${arrow}📁 ${folderName} (${totalItems} items)
`; } - - for (let i = 0; i < subdirs.length; i++) { - const subdir = subdirs[i]; - const href = subdir.getAttribute('href'); - if (href) { - const baseUrl = path.replace(/\/[^\/]*$/, '/'); - const subPath = baseUrl + href; - html += await this.buildTreeRecursive(subPath, depth + 1, currentPath, maxDepth); - } + + for (const subdir of subdirs) { + const subPath = baseUrl + subdir + '/index.html'; + html += await this.buildTreeRecursive(subPath, depth + 1, currentPath, maxDepth); } - + return html; } catch (error) { const indent = ' '.repeat(depth); @@ -122,41 +127,30 @@ export class NavigationManager { async getCollapsedPath(startPath, currentPath) { const pathSegments = []; let path = startPath; - + while (true) { try { const response = await fetch(path); const htmlContent = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(htmlContent, 'text/html'); - - const subdirs = doc.querySelectorAll('h2 + ul li a'); - const items = doc.querySelectorAll('.grid-item'); - + + const { subdirs, itemCount } = this.extractPageData(doc); const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery'; pathSegments.push({ name: folderName, path: path }); - - if (items.length > 0 || subdirs.length !== 1) { - break; - } - - const subdir = subdirs[0]; - const href = subdir.getAttribute('href'); - if (href) { - const baseUrl = path.replace(/\/[^\/]*$/, '/'); - path = baseUrl + href; - } else { + + if (itemCount > 0 || subdirs.length !== 1) { break; } + + const baseUrl = path.replace(/\/[^\/]*$/, '/'); + path = baseUrl + subdirs[0] + '/index.html'; } catch (error) { break; } } - - return { - segments: pathSegments, - finalPath: path - }; + + return { segments: pathSegments, finalPath: path }; } /** @@ -165,10 +159,10 @@ export class NavigationManager { async buildCollapsedTreeItem(collapsedPath, depth, currentPath, maxDepth) { const indent = ' '.repeat(depth); const arrow = depth === 0 ? '' : '└─ '; - + const displayName = collapsedPath.segments.map(seg => seg.name).join(' / '); const finalPath = collapsedPath.finalPath; - + let totalItems = 0; let subdirs = []; try { @@ -176,33 +170,25 @@ export class NavigationManager { const htmlContent = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(htmlContent, 'text/html'); - - const subdirElements = doc.querySelectorAll('h2 + ul li a'); - const items = doc.querySelectorAll('.grid-item'); - totalItems = items.length + subdirElements.length; - - subdirs = Array.from(subdirElements); - } catch (error) { - // Handle error case - } - + + const data = this.extractPageData(doc); + totalItems = data.itemCount + data.subdirs.length; + subdirs = data.subdirs; + } catch (error) {} + let html = ''; if (finalPath === currentPath) { html += `
${indent}${arrow}📁 ${displayName} (${totalItems} items)
`; } else { html += `
${indent}${arrow}📁 ${displayName} (${totalItems} items)
`; } - - for (let i = 0; i < subdirs.length; i++) { - const subdir = subdirs[i]; - const href = subdir.getAttribute('href'); - if (href) { - const baseUrl = finalPath.replace(/\/[^\/]*$/, '/'); - const subPath = baseUrl + href; - html += await this.buildTreeRecursive(subPath, depth + 1, currentPath, maxDepth); - } + + const baseUrl = finalPath.replace(/\/[^\/]*$/, '/'); + for (const subdir of subdirs) { + const subPath = baseUrl + subdir + '/index.html'; + html += await this.buildTreeRecursive(subPath, depth + 1, currentPath, maxDepth); } - + return html; } } diff --git a/gallery/api.py b/gallery/api.py index 62079cb..33e9b9e 100644 --- a/gallery/api.py +++ b/gallery/api.py @@ -260,8 +260,11 @@ def generate( print(f"Warning: Could not render gallery root: {e}") # Don't fail, gallery is still usable - if verbose: - print(f"✓ Gallery generated successfully at {gallery_root}") + plot_count = _count_gallery_plots(gallery_root) + if plot_count == 0: + print(f"Warning: Gallery at {gallery_root} appears to be empty — no plot files found!") + else: + print(f"✓ Gallery generated at {gallery_root} — {plot_count} plots total") return True @@ -271,6 +274,15 @@ def generate( return False +def _count_gallery_plots(gallery_root: Path) -> int: + """Recursively count plot files (PDFs and HTMLs, excluding index.html) in the gallery output.""" + count = 0 + for f in gallery_root.rglob('*'): + if f.is_file() and f.suffix.lower() in ('.pdf', '.html') and f.name != 'index.html': + count += 1 + return count + + def _is_writable(path: Path) -> bool: """ Check if a path is writable. diff --git a/gallery/assets b/gallery/assets new file mode 120000 index 0000000..ec2e4be --- /dev/null +++ b/gallery/assets @@ -0,0 +1 @@ +../assets \ No newline at end of file diff --git a/gallery/assets/css/base.css b/gallery/assets/css/base.css deleted file mode 100644 index f007e1e..0000000 --- a/gallery/assets/css/base.css +++ /dev/null @@ -1,71 +0,0 @@ -/* ======================================== - BASE LAYOUT AND TYPOGRAPHY - ======================================== */ -* { - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - margin: 0; - padding: 1rem; - padding-bottom: 400px; /* Further increased bottom padding to prevent overlap with stats box */ - background-color: var(--bg-color); - color: var(--text-color); - transition: background-color 0.3s, color 0.3s; - line-height: 1.5; -} - -h1 { - font-size: 1.8rem; - margin-bottom: 0.5rem; - font-weight: 600; -} - -h2 { - color: var(--text-color); - font-size: 1.3rem; - margin: 1.5rem 0 0.5rem 0; -} - -/* ======================================== - SUBDIRECTORIES LIST - ======================================== */ -ul { - list-style: none; - padding: 0; -} - -ul li { - margin: 0.5rem 0; -} - -ul li a { - color: var(--link-color); - text-decoration: none; - padding: 0.3rem 0; - display: inline-block; - transition: color 0.2s ease; -} - -ul li a:hover { - color: var(--link-hover); - text-decoration: underline; -} - -/* Responsive design adjustments */ -@media (max-width: 768px) { - body { - padding: 0.5rem; - padding-bottom: 300px; /* Further increased mobile bottom padding to match desktop */ - } - - h1 { - font-size: 1.5rem; - } - - h2 { - font-size: 1.2rem; - margin: 1rem 0 0.5rem 0; - } -} diff --git a/gallery/assets/css/comparison.css b/gallery/assets/css/comparison.css deleted file mode 100644 index 7e2dee2..0000000 --- a/gallery/assets/css/comparison.css +++ /dev/null @@ -1,180 +0,0 @@ -/* ======================================== - PLOT COMPARISON OVERLAY - ======================================== */ -.comparison-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.9); - z-index: 2000; - display: none; - backdrop-filter: blur(4px); -} - -.comparison-overlay.open { - display: flex; - align-items: center; - justify-content: center; -} - -.comparison-container { - width: 95%; - height: 90%; - background: var(--bg-color); - border-radius: 12px; - padding: 1.5rem; - display: flex; - flex-direction: column; - position: relative; - box-shadow: 0 10px 30px rgba(0,0,0,0.5); -} - -.comparison-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; - padding-bottom: 1rem; - border-bottom: 1px solid var(--border-color); -} - -.comparison-title { - font-size: 1.5rem; - font-weight: 600; - color: var(--text-color); - margin: 0; -} - -.comparison-close { - background: var(--button-bg); - color: white; - border: none; - border-radius: 50%; - width: 40px; - height: 40px; - font-size: 1.2rem; - cursor: pointer; - transition: all 0.2s ease; -} - -.comparison-close:hover { - background: var(--button-hover); - transform: scale(1.1); -} - -.comparison-content { - flex: 1; - display: flex; - gap: 1rem; - overflow: hidden; -} - -.comparison-panel { - flex: 1; - display: flex; - flex-direction: column; - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - overflow: hidden; -} - -.comparison-panel-header { - background: var(--tree-bg); - padding: 0.8rem 1rem; - border-bottom: 1px solid var(--border-color); - display: flex; - justify-content: space-between; - align-items: center; -} - -.comparison-panel-title { - font-weight: 600; - color: var(--text-color); - font-size: 1rem; -} - -.comparison-replace-btn { - background: var(--success-color); - color: white; - border: none; - padding: 0.4rem 0.8rem; - border-radius: 4px; - font-size: 0.85rem; - cursor: pointer; - transition: all 0.2s ease; -} - -.comparison-replace-btn:hover { - background: var(--success-hover); -} - -.comparison-panel-content { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - padding: 1rem; - overflow: auto; -} - -.comparison-plot-container { - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - position: relative; -} - -.comparison-plot { - max-width: 100%; - max-height: 100%; - border: 1px solid var(--border-color); - border-radius: 4px; - cursor: pointer; - transition: transform 0.2s ease; -} - -.comparison-plot:hover { - transform: scale(1.02); -} - -.comparison-placeholder { - width: 100%; - height: 300px; - border: 2px dashed var(--border-color); - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - color: var(--breadcrumb-color); - font-size: 1.1rem; - cursor: pointer; - transition: all 0.2s ease; -} - -.comparison-placeholder:hover { - border-color: var(--button-bg); - color: var(--button-bg); -} - -.comparison-plot-info { - position: absolute; - bottom: 0.5rem; - left: 0.5rem; - right: 0.5rem; - background: rgba(0, 0, 0, 0.8); - color: white; - padding: 0.5rem; - border-radius: 4px; - font-size: 0.9rem; - opacity: 0; - transition: opacity 0.2s ease; -} - -.comparison-plot-container:hover .comparison-plot-info { - opacity: 1; -} diff --git a/gallery/assets/css/export.css b/gallery/assets/css/export.css deleted file mode 100644 index 84d7b5f..0000000 --- a/gallery/assets/css/export.css +++ /dev/null @@ -1,440 +0,0 @@ -/* ======================================== - EXPORT FUNCTIONALITY STYLES - ======================================== */ - -/* Selection mode styles */ -.selection-mode .grid-item { - cursor: pointer; - transition: all 0.2s ease; -} - -.selection-mode .grid-item:hover { - transform: scale(1.02); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); -} - -/* Selection overlay */ -.selection-overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.3); - display: none; - justify-content: center; - align-items: center; - border-radius: 8px; - z-index: 5; -} - -.selection-mode .selection-overlay { - display: flex; -} - -.selection-checkbox { - background: var(--card-background); - border: 2px solid var(--border-color); - border-radius: 50%; - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - font-size: 18px; - transition: all 0.2s ease; -} - -.grid-item.selected .selection-checkbox { - background: var(--primary-color, #007bff); - border-color: var(--primary-color, #007bff); - color: white; -} - -.checkbox-icon { - line-height: 1; -} - -/* Selection counter */ -.selection-counter { - position: fixed; - bottom: 100px; - right: 20px; - background: var(--card-background); - border: 1px solid var(--border-color); - border-radius: 20px; - padding: 8px 16px; - font-size: 14px; - font-weight: 500; - color: var(--text-color); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - z-index: 999; -} - -/* Export button styles */ -.export-btn { - background: #28a745 !important; -} - -.export-btn:hover { - background: #218838 !important; -} - -.export-btn:disabled { - background: #6c757d !important; - cursor: not-allowed; -} - -/* Export messages */ -.export-message { - position: fixed; - top: 20px; - right: 20px; - padding: 12px 24px; - border-radius: 6px; - font-weight: 500; - z-index: 1001; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - animation: slideIn 0.3s ease; -} - -.export-message-info { - background: #d1ecf1; - color: #0c5460; - border: 1px solid #bee5eb; -} - -.export-message-success { - background: #d4edda; - color: #155724; - border: 1px solid #c3e6cb; -} - -.export-message-warning { - background: #fff3cd; - color: #856404; - border: 1px solid #ffeaa7; -} - -.export-message-error { - background: #f8d7da; - color: #721c24; - border: 1px solid #f5c6cb; -} - -@keyframes slideIn { - from { - transform: translateX(100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -/* Selection mode indicator */ -.selection-mode::before { - content: "Selection Mode - Click plots to select them"; - position: fixed; - top: 0; - left: 0; - right: 0; - background: var(--primary-color, #007bff); - color: white; - text-align: center; - padding: 8px; - font-size: 14px; - font-weight: 500; - z-index: 1000; -} - -/* Adjust main content when in selection mode */ -.selection-mode { - padding-top: 40px; -} - -/* Export instructions overlay */ -.export-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1002; -} - -.export-instructions { - background: var(--card-background); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 24px; - max-width: 600px; - max-height: 80vh; - overflow-y: auto; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); -} - -.export-instructions h3 { - margin: 0 0 16px 0; - color: var(--text-color); -} - -.export-instructions p { - margin: 0 0 16px 0; - color: var(--text-color); -} - -.export-data { - margin: 16px 0; -} - -.export-data textarea { - width: 100%; - height: 200px; - font-family: 'Courier New', monospace; - font-size: 12px; - border: 1px solid var(--border-color); - border-radius: 4px; - padding: 8px; - background: var(--background-color); - color: var(--text-color); - resize: vertical; -} - -.export-commands { - margin: 16px 0; - padding: 12px; - background: var(--header-background); - border-radius: 4px; - border: 1px solid var(--border-color); -} - -.export-command-container { - margin: 20px 0; - border: 1px solid var(--border-color); - border-radius: 8px; - overflow: hidden; -} - -.export-command { - background: var(--header-background); - padding: 16px; - border-bottom: 1px solid var(--border-color); -} - -.export-command code { - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 13px; - line-height: 1.4; - color: var(--text-color); - word-break: break-all; - display: block; - background: none; - border: none; - padding: 0; - margin: 0; -} - -.export-actions { - display: flex; - gap: 8px; - padding: 12px 16px; - background: var(--card-background); -} - -.export-actions button { - padding: 8px 16px; - border: 1px solid var(--border-color); - border-radius: 4px; - background: var(--card-background); - color: var(--text-color); - cursor: pointer; - transition: all 0.2s ease; -} - -.export-actions button:hover { - background: var(--header-background); -} - -.export-actions button:last-child { - background: var(--primary-color, #007bff); - color: white; - border-color: var(--primary-color, #007bff); -} - -.export-actions button:last-child:hover { - background: var(--primary-color-dark, #0056b3); -} - -.copy-btn, .close-btn { - padding: 8px 16px; - border: 1px solid var(--border-color); - border-radius: 6px; - background: var(--card-background); - color: var(--text-color); - cursor: pointer; - transition: all 0.2s ease; - font-size: 14px; - display: flex; - align-items: center; - gap: 4px; -} - -.copy-btn:hover { - background: var(--primary-color, #007bff); - color: white; - border-color: var(--primary-color, #007bff); -} - -.close-btn { - background: #dc3545; - color: white; - border-color: #dc3545; - margin-left: auto; -} - -.close-btn:hover { - background: #c82333; - border-color: #bd2130; -} - -.export-details, .export-tips { - margin: 20px 0; - padding: 16px; - border-radius: 6px; - border: 1px solid var(--border-color); -} - -.export-details { - background: var(--header-background); -} - -.export-tips { - background: var(--card-background); - border-color: var(--primary-color, #007bff); - border-left: 4px solid var(--primary-color, #007bff); -} - -.export-details h4, .export-tips h4 { - margin: 0 0 12px 0; - color: var(--text-color); - font-size: 16px; -} - -.export-details ul, .export-tips ul { - margin: 0; - padding-left: 20px; - color: var(--text-color); -} - -.export-details li, .export-tips li { - margin: 8px 0; - line-height: 1.5; -} - -.export-details code, .export-tips code { - background: var(--background-color); - padding: 2px 6px; - border-radius: 3px; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 12px; - border: 1px solid var(--border-color); -} - -.export-tips kbd { - background: var(--header-background); - border: 1px solid var(--border-color); - border-radius: 3px; - padding: 2px 6px; - font-family: inherit; - font-size: 12px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); -} - -.copy-feedback { - position: absolute; - top: 10px; - right: 10px; - padding: 8px 12px; - border-radius: 4px; - font-size: 12px; - font-weight: 500; - z-index: 1003; - animation: fadeInOut 2s ease-in-out; -} - -.copy-feedback-success { - background: #d4edda; - color: #155724; - border: 1px solid #c3e6cb; -} - -.copy-feedback-error { - background: #f8d7da; - color: #721c24; - border: 1px solid #f5c6cb; -} - -@keyframes fadeInOut { - 0% { opacity: 0; transform: translateY(-10px); } - 20% { opacity: 1; transform: translateY(0); } - 80% { opacity: 1; transform: translateY(0); } - 100% { opacity: 0; transform: translateY(-10px); } -} - -/* Dark theme adjustments */ -[data-theme="dark"] .selection-checkbox { - background: var(--card-background); - border-color: var(--border-color); -} - -[data-theme="dark"] .grid-item.selected .selection-checkbox { - background: var(--primary-color, #0d6efd); - border-color: var(--primary-color, #0d6efd); -} - -[data-theme="dark"] .export-message-info { - background: #0c5460; - color: #d1ecf1; - border-color: #086972; -} - -[data-theme="dark"] .export-message-success { - background: #155724; - color: #d4edda; - border-color: #1e7e34; -} - -[data-theme="dark"] .export-message-warning { - background: #856404; - color: #fff3cd; - border-color: #b58b14; -} - -[data-theme="dark"] .export-message-error { - background: #721c24; - color: #f8d7da; - border-color: #a94442; -} - -[data-theme="dark"] .copy-feedback-success { - background: #155724; - color: #d4edda; - border-color: #1e7e34; -} - -[data-theme="dark"] .copy-feedback-error { - background: #721c24; - color: #f8d7da; - border-color: #a94442; -} - -[data-theme="dark"] .export-tips kbd { - background: var(--background-color); - color: var(--text-color); -} diff --git a/gallery/assets/css/floating-elements.css b/gallery/assets/css/floating-elements.css deleted file mode 100644 index cd8e830..0000000 --- a/gallery/assets/css/floating-elements.css +++ /dev/null @@ -1,112 +0,0 @@ -/* ======================================== - FLOATING ACTION BUTTONS - ======================================== */ -.floating-buttons { - position: fixed; - bottom: 80px; - right: 15px; - display: flex; - flex-direction: column; - gap: 10px; - z-index: 1000; - /* Ensure buttons don't interfere with content */ - pointer-events: none; -} - -.floating-btn { - width: 56px; - height: 56px; - border-radius: 50%; - border: none; - cursor: pointer; - font-size: 1.3rem; - box-shadow: 0 3px 10px rgba(0,0,0,0.3); - transition: all 0.2s ease; - display: flex; - align-items: center; - justify-content: center; - /* Re-enable pointer events for buttons */ - pointer-events: auto; -} - -.floating-btn:hover { - transform: scale(1.1); -} - -.sidebar-toggle { - background: var(--button-bg); - color: white; -} - -.theme-toggle { - background: var(--button-bg); - color: white; -} - -/* Responsive adjustments */ -@media (max-width: 768px) { - .floating-buttons { - bottom: 60px; - right: 10px; - gap: 8px; - } - - .floating-btn { - width: 48px; - height: 48px; - font-size: 1.1rem; - } -} - -/* ======================================== - KEYBOARD SHORTCUTS HELP - ======================================== */ -.shortcuts-help { - position: fixed; - bottom: 220px; - right: 15px; - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 1rem; - display: none; - box-shadow: 0 4px 15px rgba(0,0,0,0.2); - z-index: 1001; - font-size: 0.9rem; - max-width: 280px; - max-height: 400px; - overflow-y: auto; -} - -.shortcuts-help h4 { - margin: 0 0 0.8rem 0; - color: var(--text-color); - font-size: 1rem; -} - -.shortcut-item { - display: flex; - justify-content: space-between; - align-items: center; - margin: 0.4rem 0; -} - -.shortcut-key { - background: var(--border-color); - padding: 0.2rem 0.4rem; - border-radius: 4px; - font-family: 'JetBrains Mono', 'Courier New', monospace; - font-size: 0.8rem; - font-weight: 500; -} - -/* Responsive adjustments */ -@media (max-width: 768px) { - .shortcuts-help { - bottom: 180px; - right: 10px; - left: 10px; - max-width: none; - max-height: 300px; - } -} diff --git a/gallery/assets/css/folder-metadata.css b/gallery/assets/css/folder-metadata.css deleted file mode 100644 index 8508cf3..0000000 --- a/gallery/assets/css/folder-metadata.css +++ /dev/null @@ -1,179 +0,0 @@ -/* ======================================== - FOLDER METADATA STYLES - ======================================== */ - -/* Folder Metadata Container */ -.folder-metadata-container { - margin: 1rem 0; - border-radius: 8px; - background: var(--card-bg); - border: 1px solid var(--border-color); - overflow: hidden; -} - -/* Folder Metadata Toggle Button */ -.folder-metadata-toggle { - width: 100%; - padding: 0.75rem 1rem; - background: var(--card-bg); - border: none; - cursor: pointer; - display: flex; - align-items: center; - justify-content: space-between; - transition: background-color 0.2s ease; - font-size: 0.95rem; - color: var(--text-color); - position: relative; - z-index: 10; - outline: none; -} - -.folder-metadata-toggle:hover { - background: var(--button-hover); - color: white; -} - -.folder-metadata-toggle:focus { - outline: 2px solid var(--link-color); - outline-offset: 2px; -} - -.folder-metadata-icon { - margin-right: 0.5rem; -} - -.folder-metadata-label { - flex: 1; - text-align: left; - font-weight: 500; -} - -.folder-metadata-arrow { - transition: transform 0.2s ease; - font-size: 0.8rem; -} - -.folder-metadata-container.expanded .folder-metadata-arrow { - transform: rotate(180deg); -} - -/* Folder Metadata Content - HIDDEN BY DEFAULT */ -.folder-metadata-content { - max-height: 0; - overflow: hidden; - transition: max-height 0.3s ease, opacity 0.3s ease; - background: var(--bg-color); - opacity: 0; - display: none; /* Force hide initially */ -} - -/* Show content when expanded */ -.folder-metadata-container.expanded .folder-metadata-content { - max-height: 1000px; - border-top: 1px solid var(--border-color); - opacity: 1; - display: block; /* Show when expanded */ -} - -/* Folder Metadata Grid */ -.folder-metadata-grid { - padding: 1rem; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 0.5rem 1rem; -} - -/* Folder Metadata Items */ -.folder-metadata-item { - display: flex; - align-items: flex-start; - gap: 0.5rem; - padding: 0.25rem 0; -} - -.folder-metadata-key { - font-weight: 600; - color: var(--link-color); - white-space: nowrap; - min-width: fit-content; -} - -.folder-metadata-value { - color: var(--text-color); - word-break: break-word; - flex: 1; -} - -/* Tags for list items */ -.folder-metadata-list { - display: flex; - flex-wrap: wrap; - gap: 0.25rem; -} - -.folder-metadata-tag { - background: var(--link-color); - color: white; - padding: 0.2rem 0.5rem; - border-radius: 12px; - font-size: 0.8rem; - white-space: nowrap; -} - -/* Nested metadata */ -.folder-metadata-nested { - background: var(--card-bg); - padding: 0.5rem; - border-radius: 4px; - border-left: 3px solid var(--link-color); -} - -.folder-metadata-nested-item { - margin: 0.25rem 0; - font-size: 0.9rem; -} - -/* Long text handling */ -.folder-metadata-expand { - background: none; - border: none; - color: var(--link-color); - cursor: pointer; - text-decoration: underline; - padding: 0; - margin-left: 0.5rem; - font-size: 0.85rem; -} - -.folder-metadata-expand:hover { - color: var(--link-hover); -} - -/* Links */ -.folder-metadata-value a { - color: var(--link-color); - text-decoration: none; -} - -.folder-metadata-value a:hover { - color: var(--link-hover); - text-decoration: underline; -} - -/* Responsive design */ -@media (max-width: 768px) { - .folder-metadata-grid { - grid-template-columns: 1fr; - gap: 0.5rem; - } - - .folder-metadata-item { - flex-direction: column; - gap: 0.25rem; - } - - .folder-metadata-key { - white-space: normal; - } -} diff --git a/gallery/assets/css/folder-tree.css b/gallery/assets/css/folder-tree.css deleted file mode 100644 index 3c394ad..0000000 --- a/gallery/assets/css/folder-tree.css +++ /dev/null @@ -1,40 +0,0 @@ -/* ======================================== - FOLDER TREE - ======================================== */ -.folder-tree { - background: var(--tree-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 1rem; - margin: 1rem 0; - font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace; - font-size: 0.85rem; - max-height: 350px; - overflow-y: auto; - transition: all 0.3s ease; -} - -.tree-item { - margin: 0.2rem 0; - white-space: pre; - font-family: inherit; -} - -.tree-current { - background: var(--tree-current-bg); - color: white; - padding: 0.2rem 0.4rem; - border-radius: 4px; - font-weight: 500; -} - -.tree-link { - color: var(--link-color); - text-decoration: none; - transition: color 0.2s ease; -} - -.tree-link:hover { - text-decoration: underline; - color: var(--link-hover); -} diff --git a/gallery/assets/css/grid.css b/gallery/assets/css/grid.css deleted file mode 100644 index 8ad9828..0000000 --- a/gallery/assets/css/grid.css +++ /dev/null @@ -1,106 +0,0 @@ -/* ======================================== - PLOT GRID - ======================================== */ -.grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 0.8rem; - padding: 1rem 0; -} - -.grid-item { - text-align: center; - background: var(--card-bg); - border-radius: 8px; - padding: 0.8rem; - transition: all 0.2s ease; - border: 1px solid transparent; -} - -.grid-item:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0,0,0,0.1); - border-color: var(--border-color); -} - -.grid-item img { - max-width: 100%; - border: 1px solid var(--border-color); - border-radius: 6px; - transition: all 0.2s ease; -} - -.grid-item a { - color: var(--link-color); - text-decoration: none; -} - -.grid-item.highlighted { - border-color: var(--button-bg); - box-shadow: 0 0 15px rgba(0, 120, 212, 0.3); - transform: translateY(-2px); - animation: highlightPulse 2s ease-in-out; -} - -@keyframes highlightPulse { - 0%, 100% { transform: translateY(-2px) scale(1); } - 50% { transform: translateY(-2px) scale(1.02); } -} - -.plot-name { - margin-top: 0.8rem; - word-wrap: break-word; - word-break: break-word; - hyphens: auto; - font-size: 0.9rem; - line-height: 1.3; - max-height: 3.9rem; - overflow: hidden; - padding: 0 0.2rem; - font-weight: 500; -} - -/* Plot selection mode */ -.selecting-plots .grid-item { - cursor: pointer !important; - transition: all 0.2s ease; - position: relative; -} - -.selecting-plots .grid-item:hover { - transform: translateY(-4px); - box-shadow: 0 6px 20px rgba(0, 120, 212, 0.3); - border-color: var(--button-bg); -} - -.selecting-plots .grid-item::before { - content: '📊 Click to compare'; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: rgba(0, 120, 212, 0.95); - color: white; - padding: 0.5rem 1rem; - border-radius: 4px; - font-size: 0.9rem; - font-weight: 600; - opacity: 0; - transition: opacity 0.2s ease; - pointer-events: none; - z-index: 10; - white-space: nowrap; -} - -.selecting-plots .grid-item:hover::before { - opacity: 1; -} - -/* Ensure grid items are clickable in selection mode */ -.selecting-plots .grid-item * { - pointer-events: none; -} - -.selecting-plots .grid-item { - pointer-events: auto; -} diff --git a/gallery/assets/css/html-plots.css b/gallery/assets/css/html-plots.css deleted file mode 100644 index 234a2e8..0000000 --- a/gallery/assets/css/html-plots.css +++ /dev/null @@ -1,38 +0,0 @@ -/* Styles for HTML plots */ -.plot-item.html-plot .plot-link { - position: relative; -} - -.plot-item.html-plot .html-thumbnail { - background: #f5f5f5; - border: 1px solid #ddd; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 200px; - text-align: center; - padding: 20px; -} - -.plot-item.html-plot .html-indicator { - position: absolute; - top: 5px; - right: 5px; - background: #4CAF50; - color: white; - padding: 2px 6px; - border-radius: 3px; - font-size: 0.8em; -} - -.plot-item.html-plot .html-preview { - color: #666; - margin-top: 10px; - font-size: 0.9em; -} - -/* Add a hover effect */ -.plot-item.html-plot .plot-link:hover .html-thumbnail { - background: #e8e8e8; -} diff --git a/gallery/assets/css/main.css b/gallery/assets/css/main.css deleted file mode 100644 index c6019d7..0000000 --- a/gallery/assets/css/main.css +++ /dev/null @@ -1,33 +0,0 @@ -/* ======================================== - GALLERY STYLES - MAIN ENTRY POINT - ======================================== */ - -/* Core styles */ -@import url('./variables.css'); -@import url('./base.css'); - -/* Component styles */ -@import url('./navigation.css'); -@import url('./search.css'); -@import url('./folder-tree.css'); -@import url('./grid.css'); -@import url('./sidebar.css'); -@import url('./floating-elements.css'); -@import url('./stats.css'); -@import url('./comparison.css'); -@import url('./metadata.css'); -@import url('./metadata-section.css'); -@import url('./folder-metadata.css'); -@import url('./export.css'); - -/* View controls - must come after grid.css to override */ -@import url('./view-controls.css'); - -/* Sort controls styling */ -@import url('./sort-controls.css'); - -/* View override - force grid layout to work */ -@import url('./view-override.css'); - -/* Responsive design - last to override everything */ -@import url('./responsive.css'); diff --git a/gallery/assets/css/metadata-section.css b/gallery/assets/css/metadata-section.css deleted file mode 100644 index 7988467..0000000 --- a/gallery/assets/css/metadata-section.css +++ /dev/null @@ -1,383 +0,0 @@ -/* ======================================== - METADATA SECTION STYLES - ======================================== */ - -/* Metadata Section Container */ -.metadata-section { - margin: 1rem 0; - border-radius: 8px; - background: var(--card-bg); - border: 1px solid var(--border-color); - overflow: hidden; -} - -/* Toggle Button */ -.metadata-toggle-btn { - width: 100%; - padding: 0.75rem 1rem; - background: var(--card-bg); - border: none; - cursor: pointer; - display: flex; - align-items: center; - justify-content: space-between; - transition: background-color 0.2s ease; - font-size: 0.95rem; - color: var(--text-color); - position: relative; - z-index: 10; -} - -.metadata-toggle-btn:hover { - background: var(--button-hover); - color: white; -} - -.metadata-toggle-btn:focus { - outline: 2px solid var(--link-color); - outline-offset: 2px; -} - -.metadata-icon { - margin-right: 0.5rem; -} - -.metadata-label { - flex: 1; - text-align: left; - font-weight: 500; -} - -.metadata-arrow { - transition: transform 0.2s ease; - font-size: 0.8rem; -} - -/* Content Area - Hidden by default */ -.metadata-content { - background: var(--bg-color); - border-top: 1px solid var(--border-color); - display: none; /* Hidden by default */ -} - -/* Metadata Header with File Path */ -.metadata-header { - padding: 1rem; - background: var(--card-bg); - border-bottom: 1px solid var(--border-color); -} - -.metadata-file-info { - display: flex; - align-items: center; - gap: 0.5rem; - flex-wrap: wrap; -} - -.file-path-label { - font-weight: 600; - color: var(--text-color); - white-space: nowrap; - font-size: 0.9rem; -} - -.file-path { - background: var(--bg-color); - border: 1px solid var(--border-color); - border-radius: 4px; - padding: 0.4rem 0.6rem; - font-family: 'Courier New', monospace; - font-size: 0.8rem; - color: var(--link-color); - flex: 1; - min-width: 200px; - word-break: break-all; - user-select: all; -} - -.copy-path-btn { - background: var(--link-color); - color: white; - border: none; - border-radius: 4px; - padding: 0.4rem 0.8rem; - cursor: pointer; - font-size: 0.8rem; - transition: all 0.2s ease; - white-space: nowrap; - font-weight: 500; -} - -.copy-path-btn:hover { - background: var(--link-hover); - transform: translateY(-1px); -} - -.copy-path-btn:active { - transform: scale(0.95); -} - -.copy-path-btn.copied { - background: #4CAF50; - transform: scale(1.05); -} - -/* Tip Icon with Hover Tooltip */ -.tip-icon { - cursor: help; - font-size: 1.2rem; - position: relative; - display: inline-block; - margin-left: 0.25rem; - opacity: 0.8; - transition: opacity 0.2s ease; -} - -.tip-icon:hover { - opacity: 1; -} - -/* Custom tooltip for tip icon */ -.tip-icon::after { - content: attr(title); - position: absolute; - bottom: 125%; - left: 50%; - transform: translateX(-50%); - background: #333; - color: white; - padding: 0.75rem; - border-radius: 6px; - font-size: 0.85rem; - white-space: normal; - width: 280px; - text-align: left; - z-index: 1000; - opacity: 0; - visibility: hidden; - transition: opacity 0.3s ease, visibility 0.3s ease; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - line-height: 1.4; - font-weight: normal; - font-family: var(--font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif); -} - -/* Tooltip arrow */ -.tip-icon::before { - content: ''; - position: absolute; - bottom: 115%; - left: 50%; - transform: translateX(-50%); - border: 6px solid transparent; - border-top-color: #333; - opacity: 0; - visibility: hidden; - transition: opacity 0.3s ease, visibility 0.3s ease; - z-index: 1001; -} - -.tip-icon:hover::after, -.tip-icon:hover::before { - opacity: 1; - visibility: visible; -} - -/* Grid Layout */ -.metadata-grid { - padding: 1rem; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 1rem 1.5rem; /* Increased gaps to prevent overlapping */ -} - -/* Metadata Items */ -.metadata-item { - display: flex; - flex-direction: column; /* Stack key and value vertically to prevent overlap */ - gap: 0.25rem; - padding: 0.75rem; - background: var(--card-bg); - border-radius: 6px; - border: 1px solid var(--border-color); - word-wrap: break-word; /* Ensure long text wraps */ - overflow-wrap: break-word; /* Additional word wrapping */ -} - -.metadata-key { - font-weight: 600; - color: var(--link-color); - font-size: 0.9rem; - margin-bottom: 0.25rem; -} - -.metadata-value { - color: var(--text-color); - word-break: break-word; - overflow-wrap: break-word; - line-height: 1.4; - font-size: 0.9rem; -} - -/* LaTeX content styling */ -.latex-content { - color: var(--text-color); - line-height: 1.6; - font-family: 'Times New Roman', serif; -} - -/* Simple list items - proper list formatting */ -.metadata-list { - color: var(--text-color); - line-height: 1.4; - margin: 0; - padding-left: 1.2rem; - list-style-type: disc; /* Add bullet points */ -} - -.metadata-list li { - margin: 0.25rem 0; - padding: 0; - color: var(--text-color); -} - -/* YAML-style formatting for metadata */ -.metadata-yaml-list { - margin: 0.5rem 0; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 0.9rem; - line-height: 1.6; - background: var(--card-bg); - padding: 0.8rem; - border-radius: 6px; - border: 1px solid var(--border-color); -} - -.yaml-list-item { - color: var(--text-color); - margin: 0.25rem 0; - padding-left: 0; - text-indent: 0; -} - -.metadata-yaml-object { - margin: 0.5rem 0; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 0.9rem; - line-height: 1.6; - background: var(--card-bg); - padding: 0.8rem; - border-radius: 6px; - border: 1px solid var(--border-color); -} - -.yaml-object-item { - margin: 0.5rem 0; -} - -.yaml-key { - color: var(--link-color); - font-weight: 600; -} - -.yaml-value { - color: var(--text-color); - margin-left: 0.5rem; -} - -.yaml-nested-list { - margin: 0.25rem 0 0 1.5rem; - border-left: 2px solid var(--border-color); - padding-left: 0.8rem; -} - -.yaml-nested-item { - color: var(--text-color); - margin: 0.2rem 0; - padding-left: 0; -} - -.yaml-nested-object { - margin: 0.25rem 0 0 1.5rem; - border-left: 2px solid var(--border-color); - padding-left: 0.8rem; -} - -/* Remove the blue box styling for tags */ -.metadata-tag { - display: inline; - background: none; - color: var(--text-color); - padding: 0; - border-radius: 0; - font-size: inherit; - white-space: normal; -} - -/* Simplified nested metadata */ -.metadata-nested { - background: none; - padding: 0; - border-radius: 0; - border-left: none; - color: var(--text-color); -} - -.metadata-nested-item { - margin: 0.25rem 0; - font-size: 0.9rem; -} - -/* Long text handling */ -.metadata-expand { - background: none; - border: none; - color: var(--link-color); - cursor: pointer; - text-decoration: underline; - padding: 0; - margin-left: 0.5rem; - font-size: 0.85rem; -} - -.metadata-expand:hover { - color: var(--link-hover); -} - -/* Links */ -.metadata-value a { - color: var(--link-color); - text-decoration: none; -} - -.metadata-value a:hover { - color: var(--link-hover); - text-decoration: underline; -} - -/* Responsive design */ -@media (max-width: 768px) { - .metadata-grid { - grid-template-columns: 1fr; - gap: 0.5rem; - } - - .metadata-item { - flex-direction: column; - gap: 0.25rem; - } - - .metadata-key { - white-space: normal; - } - - .metadata-file-info { - flex-direction: column; - align-items: stretch; - gap: 0.5rem; - } - - .file-path { - min-width: auto; - } -} diff --git a/gallery/assets/css/metadata.css b/gallery/assets/css/metadata.css deleted file mode 100644 index 872fef6..0000000 --- a/gallery/assets/css/metadata.css +++ /dev/null @@ -1,180 +0,0 @@ -/* ======================================== - 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/gallery/assets/css/navigation.css b/gallery/assets/css/navigation.css deleted file mode 100644 index 1dc5629..0000000 --- a/gallery/assets/css/navigation.css +++ /dev/null @@ -1,57 +0,0 @@ -/* ======================================== - NAVIGATION COMPONENTS - ======================================== */ -.breadcrumb { - margin: 0.5rem 0 1rem 0; - font-size: 0.9rem; - color: var(--breadcrumb-color); -} - -.breadcrumb a { - color: var(--link-color); - text-decoration: none; -} - -.breadcrumb a:hover { - text-decoration: underline; - color: var(--link-hover); -} - -.breadcrumb .separator { - margin: 0 0.3rem; - color: var(--breadcrumb-color); - opacity: 0.7; -} - -.navigation { - margin: 1rem 0; - display: flex; - gap: 0.5rem; - flex-wrap: wrap; -} - -.nav-btn { - background: var(--button-bg); - color: white; - padding: 0.5rem 1rem; - border: none; - border-radius: 6px; - cursor: pointer; - text-decoration: none; - transition: all 0.2s ease; - font-size: 0.9rem; - display: inline-flex; - align-items: center; - gap: 0.3rem; -} - -.nav-btn:hover { - background: var(--button-hover); - transform: translateY(-1px); -} - -.nav-btn:disabled { - background: var(--disabled-color); - cursor: not-allowed; - transform: none; -} diff --git a/gallery/assets/css/responsive.css b/gallery/assets/css/responsive.css deleted file mode 100644 index 1ca81f5..0000000 --- a/gallery/assets/css/responsive.css +++ /dev/null @@ -1,59 +0,0 @@ -/* ======================================== - RESPONSIVE DESIGN - ======================================== */ -@media (max-width: 768px) { - body { - padding: 0.5rem; - } - - .grid { - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); - gap: 0.8rem; - } - - .sidebar { - width: 100%; - right: -100%; - } - - .navigation { - gap: 0.3rem; - } - - .nav-btn { - padding: 0.4rem 0.8rem; - font-size: 0.8rem; - } - - .search-container { - max-width: 100%; - } - - /* View controls responsive */ - .controls-container { - flex-direction: column; - gap: 1rem; - align-items: stretch; - } - - .sort-controls { - justify-content: center; - } - - .view-controls { - margin-right: 0.5rem !important; - padding: 8px 12px !important; - justify-content: center; - } - - .view-btn { - min-width: 40px !important; - min-height: 40px !important; - padding: 8px 12px !important; - } - - .view-btn svg { - width: 16px !important; - height: 16px !important; - } -} diff --git a/gallery/assets/css/search.css b/gallery/assets/css/search.css deleted file mode 100644 index 7bd6995..0000000 --- a/gallery/assets/css/search.css +++ /dev/null @@ -1,72 +0,0 @@ -/* ======================================== - SEARCH FUNCTIONALITY - ======================================== */ -.search-container { - position: relative; - max-width: 500px; - margin: 1rem 0; -} - -.search-box { - width: 100%; - padding: 0.8rem 3rem 0.8rem 1rem; - border: 1px solid var(--border-color); - border-radius: 8px; - background: var(--card-bg); - color: var(--text-color); - font-size: 1rem; - outline: none; - transition: all 0.3s ease; -} - -.search-box:focus { - border-color: var(--button-bg); - box-shadow: 0 0 0 3px rgba(0, 120, 212, 0.1); -} - -.search-icon { - position: absolute; - right: 1rem; - top: 50%; - transform: translateY(-50%); - color: var(--text-color); - opacity: 0.6; - pointer-events: none; -} - -.search-results { - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - margin-top: 0.5rem; - max-height: 400px; - overflow-y: auto; - display: none; - box-shadow: 0 4px 12px rgba(0,0,0,0.15); - z-index: 100; -} - -.search-result-item { - padding: 0.75rem; - margin: 0; - cursor: pointer; - transition: background-color 0.2s ease; - border-bottom: 1px solid var(--border-color); -} - -.search-result-item:last-child { - border-bottom: none; -} - -.search-result-item:hover { - background: var(--button-bg); - color: white; -} - -.search-highlight { - background: #ffeb3b; - color: #000; - font-weight: bold; - padding: 0.1rem 0.2rem; - border-radius: 2px; -} diff --git a/gallery/assets/css/sidebar.css b/gallery/assets/css/sidebar.css deleted file mode 100644 index 7718fb1..0000000 --- a/gallery/assets/css/sidebar.css +++ /dev/null @@ -1,121 +0,0 @@ -/* ======================================== - SIDEBAR (RECENT PLOTS) - ======================================== */ -.sidebar { - position: fixed; - top: 0; - right: -350px; - width: 330px; - height: 100vh; - background: var(--card-bg); - border-left: 2px solid var(--border-color); - z-index: 2000; - transition: right 0.3s ease; - overflow-y: auto; - box-shadow: -4px 0 15px rgba(0,0,0,0.2); -} - -.sidebar.open { - right: 0; -} - -.sidebar-header { - padding: 1.2rem; - border-bottom: 1px solid var(--border-color); - position: sticky; - top: 0; - background: var(--card-bg); - z-index: 1; -} - -.sidebar-title { - margin: 0; - font-size: 1.2rem; - color: var(--text-color); - font-weight: 600; -} - -.sidebar-close { - position: absolute; - top: 1rem; - right: 1rem; - background: none; - border: none; - font-size: 1.5rem; - cursor: pointer; - color: var(--text-color); - padding: 0.2rem; - border-radius: 4px; - transition: background-color 0.2s ease; -} - -.sidebar-close:hover { - background: var(--border-color); -} - -.sidebar-content { - padding: 1rem; -} - -.recent-plot { - display: flex; - gap: 0.7rem; - padding: 0.8rem; - margin-bottom: 0.8rem; - border-radius: 6px; - cursor: pointer; - transition: all 0.2s ease; - border: 1px solid var(--border-color); -} - -.recent-plot:hover { - background: var(--button-bg); - color: white; - transform: translateX(2px); -} - -.recent-plot-thumb { - width: 60px; - height: 48px; - object-fit: cover; - border-radius: 4px; - flex-shrink: 0; -} - -.recent-plot-info { - flex: 1; - min-width: 0; -} - -.recent-plot-name { - font-size: 0.9rem; - font-weight: 600; - margin-bottom: 0.3rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.recent-plot-path { - font-size: 0.8rem; - color: var(--breadcrumb-color); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.sidebar-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0,0,0,0.5); - z-index: 1500; - display: none; - backdrop-filter: blur(2px); -} - -.sidebar-overlay.open { - display: block; -} diff --git a/gallery/assets/css/sort-controls.css b/gallery/assets/css/sort-controls.css deleted file mode 100644 index 3df2439..0000000 --- a/gallery/assets/css/sort-controls.css +++ /dev/null @@ -1,88 +0,0 @@ -/* ======================================== - SORT CONTROLS STYLING - ======================================== */ - -/* Clean styling for sort controls */ -.sort-controls { - display: flex !important; - align-items: center !important; - gap: 8px !important; - padding: 12px 16px !important; - background: var(--tree-bg) !important; - border: 1px solid var(--border-color) !important; - border-radius: 8px !important; - margin-right: 1rem !important; -} - -.sort-label { - font-size: 0.9rem !important; - color: var(--text-color) !important; - margin-right: 8px !important; - font-weight: 500 !important; -} - -/* Button styling using theme variables */ -.sort-btn { - background: var(--card-bg) !important; - border: 1px solid var(--border-color) !important; - border-radius: 6px !important; - padding: 8px 12px !important; - cursor: pointer !important; - transition: all 0.2s ease !important; - font-size: 0.85rem !important; - color: var(--text-color) !important; - display: flex !important; - align-items: center !important; - gap: 4px !important; - outline: none !important; - text-decoration: none !important; - font-family: inherit !important; -} - -.sort-btn:hover { - background: var(--button-bg) !important; - color: white !important; - transform: translateY(-1px) !important; - box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important; - border-color: var(--button-bg) !important; -} - -.sort-btn.active { - background: var(--button-bg) !important; - color: white !important; - border-color: var(--button-bg) !important; - box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important; -} - -.sort-order-btn { - background: var(--card-bg) !important; - border: 1px solid var(--border-color) !important; - border-radius: 6px !important; - padding: 8px 12px !important; - cursor: pointer !important; - transition: all 0.2s ease !important; - font-size: 1rem !important; - color: var(--text-color) !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - min-width: 36px !important; - outline: none !important; - text-decoration: none !important; - font-family: inherit !important; -} - -.sort-order-btn:hover { - background: var(--button-bg) !important; - color: white !important; - transform: translateY(-1px) !important; - box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important; - border-color: var(--button-bg) !important; -} - -.sort-order-btn.active { - background: var(--button-bg) !important; - color: white !important; - border-color: var(--button-bg) !important; - box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important; -} diff --git a/gallery/assets/css/sort-debug.css b/gallery/assets/css/sort-debug.css deleted file mode 100644 index e69de29..0000000 diff --git a/gallery/assets/css/stats.css b/gallery/assets/css/stats.css deleted file mode 100644 index 10868e5..0000000 --- a/gallery/assets/css/stats.css +++ /dev/null @@ -1,59 +0,0 @@ -/* ======================================== - GALLERY STATISTICS - ======================================== */ -.gallery-stats { - position: fixed; - bottom: 80px; - left: 15px; - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 0.8rem 1rem; - font-size: 0.85rem; - color: var(--breadcrumb-color); - box-shadow: 0 2px 8px rgba(0,0,0,0.1); - z-index: 500; - opacity: 0.8; - transition: opacity 0.2s ease; - max-width: 200px; - /* Ensure stats don't interfere with content */ - pointer-events: none; -} - -.gallery-stats:hover { - opacity: 1; - /* Re-enable pointer events on hover */ - pointer-events: auto; -} - -.stats-item { - display: flex; - justify-content: space-between; - align-items: center; - margin: 0.2rem 0; - white-space: nowrap; -} - -.stats-label { - margin-right: 0.8rem; -} - -.stats-value { - font-weight: 600; - color: var(--text-color); -} - -/* Responsive adjustments */ -@media (max-width: 768px) { - .gallery-stats { - bottom: 60px; - left: 10px; - padding: 0.6rem 0.8rem; - font-size: 0.8rem; - max-width: 180px; - } - - .stats-item { - margin: 0.15rem 0; - } -} diff --git a/gallery/assets/css/variables.css b/gallery/assets/css/variables.css deleted file mode 100644 index 72c531e..0000000 --- a/gallery/assets/css/variables.css +++ /dev/null @@ -1,33 +0,0 @@ -/* ======================================== - CSS VARIABLES AND THEME DEFINITIONS - ======================================== */ -:root { - --bg-color: #1e1e1e; - --text-color: #ffffff; - --card-bg: #2d2d2d; - --border-color: #404040; - --link-color: #569cd6; - --link-hover: #4a9eff; - --button-bg: #0078d4; - --button-hover: #106ebe; - --breadcrumb-color: #cccccc; - --tree-bg: #252526; - --tree-current-bg: #0078d4; - --success-color: #28a745; - --success-hover: #218838; - --disabled-color: #6c757d; -} - -[data-theme="light"] { - --bg-color: #ffffff; - --text-color: #333333; - --card-bg: #f8f8f8; - --border-color: #ddd; - --link-color: #007acc; - --link-hover: #005a9e; - --button-bg: #007acc; - --button-hover: #005a9e; - --breadcrumb-color: #666; - --tree-bg: #f8f8f8; - --tree-current-bg: #007acc; -} diff --git a/gallery/assets/css/view-controls.css b/gallery/assets/css/view-controls.css deleted file mode 100644 index 11580d8..0000000 --- a/gallery/assets/css/view-controls.css +++ /dev/null @@ -1,404 +0,0 @@ -/* ======================================== - VIEW CONTROLS AND LAYOUT MODES - ======================================== */ - -/* Controls Container */ -.controls-container { - display: flex; - justify-content: space-between; - align-items: center; - margin: 1rem 0; - gap: 2rem; - flex-wrap: wrap; -} - -/* Sort Controls */ -.sort-controls { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - background: var(--tree-bg); - border: 1px solid var(--border-color); - border-radius: 8px; -} - -.sort-label { - font-size: 0.9rem; - color: var(--text-color); - margin-right: 4px; - font-weight: 500; -} - -.sort-btn, .sort-order-btn { - background: var(--card-bg) !important; - border: 1px solid var(--border-color) !important; - border-radius: 6px !important; - padding: 6px 12px !important; - cursor: pointer !important; - transition: all 0.2s ease !important; - font-size: 0.85rem !important; - color: var(--text-color) !important; - display: flex !important; - align-items: center !important; - gap: 4px !important; - outline: none !important; - text-decoration: none !important; -} - -.sort-btn:hover, .sort-order-btn:hover { - background: var(--button-bg) !important; - color: white !important; - transform: translateY(-1px) !important; - box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; -} - -.sort-btn.active, .sort-order-btn.active { - background: var(--button-bg) !important; - color: white !important; - border-color: var(--button-bg) !important; - box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; -} - -.sort-order-btn { - min-width: 32px !important; - justify-content: center !important; - font-size: 1rem !important; -} - -/* View Controls */ -.view-controls { - display: flex; - justify-content: flex-end; - align-items: center; - gap: 12px; - padding: 12px 16px; - background: var(--tree-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - position: relative; - z-index: 10; -} - -.view-btn { - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 12px 16px; - cursor: pointer; - transition: all 0.2s ease; - font-size: 1.2rem; - color: var(--text-color); - min-width: 48px; - min-height: 48px; - display: flex; - align-items: center; - justify-content: center; -} - -.view-btn svg { - width: 18px; - height: 18px; -} - -.view-btn:hover { - background: var(--button-bg); - color: white; - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); -} - -.view-btn.active { - background: var(--button-bg); - color: white; - border-color: var(--button-bg); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -/* Plot Container Base Styles */ -.plot-container { - margin: 1rem 0; - margin-bottom: 450px; /* Add extra bottom margin to prevent overlap with stats box */ - transition: all 0.3s ease; - clear: both; -} - -.plot-container .plot-item { - transition: all 0.2s ease; - border-radius: 8px; - overflow: hidden; - background: var(--card-bg); - border: 1px solid transparent; -} - -.plot-container .plot-item:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0,0,0,0.1); - border-color: var(--border-color); -} - -.plot-container .plot-link { - color: var(--link-color); - text-decoration: none; - display: block; -} - -.plot-container .plot-thumbnail { - width: 100%; - height: auto; - border-radius: 6px; - transition: all 0.2s ease; -} - -.plot-container .plot-info { - padding: 0.8rem; -} - -.plot-container .plot-name { - word-wrap: break-word; - word-break: break-word; - hyphens: auto; - font-size: 0.9rem; - line-height: 1.3; - font-weight: 500; - color: var(--text-color); -} - -.plot-container .plot-date { - font-size: 0.8rem; - color: var(--text-secondary); - margin-top: 0.3rem; - opacity: 0.7; /* Slightly lower opacity for differentiation */ -} - -/* Grid View - Override any conflicting styles */ -.plot-container.grid-view { - display: grid !important; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)) !important; - gap: 1.2rem !important; - padding: 1rem 0 !important; -} - -.plot-container.grid-view .plot-item, -.plot-container.grid-view .grid-item { - text-align: center !important; - background: var(--card-bg) !important; - border-radius: 8px !important; - padding: 0.8rem !important; - transition: all 0.2s ease !important; - border: 1px solid transparent !important; - display: block !important; - width: auto !important; - max-width: none !important; -} - -.plot-container.grid-view .plot-item:hover, -.plot-container.grid-view .grid-item:hover { - transform: translateY(-2px) !important; - box-shadow: 0 4px 12px rgba(0,0,0,0.1) !important; - border-color: var(--border-color) !important; -} - -.plot-container.grid-view .plot-link { - display: block !important; - color: var(--link-color) !important; - text-decoration: none !important; -} - -.plot-container.grid-view .plot-thumbnail { - max-width: 100% !important; - height: auto !important; - border: 1px solid var(--border-color) !important; - display: block !important; - width: 100% !important; - object-fit: contain !important; - border-radius: 6px !important; -} - -.plot-container.grid-view .plot-info { - padding: 0.8rem 0 0 0 !important; -} - -.plot-container.grid-view .plot-name { - max-height: 3.9rem !important; - overflow: hidden !important; - margin-top: 0.8rem !important; - display: block !important; - word-wrap: break-word !important; - word-break: break-word !important; - hyphens: auto !important; - font-size: 0.9rem !important; - line-height: 1.3 !important; - font-weight: 500 !important; - color: var(--text-color) !important; - text-align: center !important; -} - -/* Large List View */ -.plot-container.list-large-view { - display: flex !important; - flex-direction: column !important; - gap: 0.8rem !important; -} - -.plot-container.list-large-view .plot-item { - display: flex !important; - align-items: center !important; - padding: 1rem !important; - gap: 1rem !important; -} - -.plot-container.list-large-view .plot-link { - flex-shrink: 0 !important; - width: 120px !important; - height: 90px !important; - overflow: hidden !important; - border-radius: 6px !important; - border: 1px solid var(--border-color) !important; -} - -.plot-container.list-large-view .plot-thumbnail { - width: 100% !important; - height: 100% !important; - object-fit: cover !important; -} - -.plot-container.list-large-view .plot-info { - flex: 1 !important; - padding: 0 !important; - text-align: left !important; - display: flex !important; - justify-content: space-between !important; - align-items: center !important; -} - -.plot-container.list-large-view .plot-name { - font-size: 1rem !important; - line-height: 1.4 !important; - max-height: none !important; - overflow: visible !important; - flex: 1 !important; -} - -.plot-container.list-large-view .plot-date { - flex-shrink: 0 !important; - margin-left: 1rem !important; - margin-top: 0 !important; - font-size: 0.85rem !important; - color: var(--text-secondary) !important; - white-space: nowrap !important; -} - -/* Compact List View */ -.plot-container.list-compact-view { - display: flex !important; - flex-direction: column !important; - gap: 0.4rem !important; -} - -.plot-container.list-compact-view .plot-item { - display: flex !important; - align-items: center !important; - padding: 0.6rem 1rem !important; - gap: 0.8rem !important; - border-radius: 4px !important; -} - -.plot-container.list-compact-view .plot-link { - flex: 1 !important; - display: flex !important; - align-items: center !important; -} - -.plot-container.list-compact-view .plot-thumbnail { - display: none !important; -} - -.plot-container.list-compact-view .plot-info { - padding: 0 !important; - flex: 1 !important; - text-align: left !important; - display: flex !important; - justify-content: space-between !important; - align-items: center !important; -} - -.plot-container.list-compact-view .plot-name { - font-size: 0.95rem !important; - line-height: 1.2 !important; - margin: 0 !important; - white-space: nowrap !important; - overflow: hidden !important; - text-overflow: ellipsis !important; - flex: 1 !important; -} - -.plot-container.list-compact-view .plot-date { - flex-shrink: 0 !important; - margin-left: 1rem !important; - margin-top: 0 !important; - font-size: 0.8rem !important; - color: var(--text-secondary) !important; - white-space: nowrap !important; -} - -/* Highlight effect for all views */ -.plot-item.highlighted { - border-color: var(--button-bg); - box-shadow: 0 0 15px rgba(0, 120, 212, 0.3); - transform: translateY(-2px); - animation: highlightPulse 2s ease-in-out; -} - -@keyframes highlightPulse { - 0%, 100% { transform: translateY(-2px) scale(1); } - 50% { transform: translateY(-2px) scale(1.02); } -} - -/* Responsive adjustments */ -@media (max-width: 768px) { - .view-controls { - width: 100%; - margin-left: 0; - margin-right: 0; - } - - .plot-container.grid-view { - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); - gap: 1rem; - } - - .plot-container.list-large-view .plot-link { - width: 80px; - height: 60px; - } - - .plot-container.list-large-view .plot-item { - padding: 0.8rem; - } - - .plot-container.list-compact-view .plot-item { - padding: 0.5rem 0.8rem; - } -} - -@media (max-width: 480px) { - .view-controls { - gap: 4px; - } - - .view-btn { - padding: 6px 8px; - font-size: 1rem; - min-width: 32px; - } - - .plot-container.grid-view { - grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); - } - - .plot-container.list-large-view .plot-link { - width: 60px; - height: 45px; - } -} diff --git a/gallery/assets/css/view-override.css b/gallery/assets/css/view-override.css deleted file mode 100644 index e9bd47c..0000000 --- a/gallery/assets/css/view-override.css +++ /dev/null @@ -1,54 +0,0 @@ -/* ======================================== - VIEW OVERRIDE - Ensure grid view works - ======================================== */ - -/* Force grid layout when grid-view class is present */ -body .plot-container.grid-view { - display: grid !important; - grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)) !important; - gap: 1.2rem !important; - padding: 1rem 0 !important; -} - -/* Force grid items to be proper tiles */ -body .plot-container.grid-view .plot-item, -body .plot-container.grid-view .grid-item { - display: block !important; - width: auto !important; - max-width: none !important; - text-align: center !important; - background: var(--card-bg) !important; - border-radius: 8px !important; - padding: 0.8rem !important; - border: 1px solid transparent !important; -} - -/* Ensure thumbnails are properly sized */ -body .plot-container.grid-view .plot-thumbnail { - width: 100% !important; - height: auto !important; - max-width: 100% !important; - display: block !important; - border: 1px solid var(--border-color) !important; - border-radius: 6px !important; - object-fit: cover !important; - aspect-ratio: 4/3; -} - -/* Force plot info styling */ -body .plot-container.grid-view .plot-info { - padding: 0.5rem 0 0 0 !important; - text-align: center !important; -} - -body .plot-container.grid-view .plot-name { - font-size: 0.85rem !important; - line-height: 1.2 !important; - max-height: 2.4rem !important; - overflow: hidden !important; - text-overflow: ellipsis !important; - display: -webkit-box !important; - -webkit-line-clamp: 2 !important; - line-clamp: 2 !important; - -webkit-box-orient: vertical !important; -} diff --git a/gallery/assets/js/comparison-manager.js b/gallery/assets/js/comparison-manager.js deleted file mode 100644 index 7056775..0000000 --- a/gallery/assets/js/comparison-manager.js +++ /dev/null @@ -1,180 +0,0 @@ -/** - * Plot comparison functionality - */ -export class ComparisonManager { - constructor() { - this.comparisonMode = false; - this.comparisonSlot = null; // 'left' or 'right' - this.plots = { left: null, right: null }; - } - - /** - * Toggle comparison mode - */ - toggleCompareMode() { - this.comparisonMode = !this.comparisonMode; - const compareBtn = document.getElementById('compareToggle'); - - if (!compareBtn) return; - - if (this.comparisonMode) { - compareBtn.style.background = 'var(--success-color)'; - compareBtn.title = 'Exit Compare Mode (Ctrl+C)'; - this.showComparisonOverlay(); - } else { - compareBtn.style.background = 'var(--button-bg)'; - compareBtn.title = 'Compare Plots (Ctrl+C)'; - this.hideComparisonOverlay(); - } - } - - /** - * Show comparison overlay - */ - showComparisonOverlay() { - const overlay = document.getElementById('comparisonOverlay'); - if (overlay) overlay.classList.add('open'); - } - - /** - * Hide comparison overlay - */ - hideComparisonOverlay() { - const overlay = document.getElementById('comparisonOverlay'); - if (overlay) overlay.classList.remove('open'); - this.comparisonMode = false; - - const compareBtn = document.getElementById('compareToggle'); - if (compareBtn) { - compareBtn.style.background = 'var(--button-bg)'; - compareBtn.title = 'Compare Plots (Ctrl+C)'; - } - } - - /** - * Close comparison overlay (alias for hideComparisonOverlay) - */ - closeComparison() { - this.hideComparisonOverlay(); - } - - /** - * Select plot for comparison - simple approach - */ - selectPlotForComparison(slot) { - this.comparisonSlot = slot; - - // Hide overlay temporarily by removing the 'open' class - const overlay = document.getElementById('comparisonOverlay'); - if (overlay) overlay.classList.remove('open'); - - // Show simple alert with instructions - const instruction = document.createElement('div'); - instruction.id = 'comparisonInstruction'; - instruction.style.cssText = ` - position: fixed; - top: 20px; - left: 50%; - transform: translateX(-50%); - background: var(--button-bg); - color: white; - padding: 1rem 2rem; - border-radius: 8px; - z-index: 3000; - font-size: 1.1rem; - font-weight: 600; - box-shadow: 0 4px 20px rgba(0,0,0,0.3); - `; - instruction.innerHTML = `📊 Click any plot to select for ${slot === 'left' ? 'Plot A' : 'Plot B'} (ESC to cancel)`; - document.body.appendChild(instruction); - - // Add one-time click listener to all grid items - const gridItems = document.querySelectorAll('.grid-item'); - const handleClick = (event) => { - event.preventDefault(); - event.stopPropagation(); - - const gridItem = event.currentTarget; - const img = gridItem.querySelector('img'); - const nameEl = gridItem.querySelector('.plot-name'); - - if (img && nameEl) { - const plotInfo = { - name: nameEl.textContent.trim(), - imgSrc: img.src, - pdfSrc: img.src.replace('.png', '.pdf'), - path: window.location.pathname - }; - - this.addPlotToComparison(plotInfo, slot); - } - - // Clean up - instruction.remove(); - gridItems.forEach(item => item.removeEventListener('click', handleClick)); - this.comparisonSlot = null; - if (overlay) overlay.classList.add('open'); - }; - - gridItems.forEach(item => { - item.style.cursor = 'pointer'; - item.style.border = '2px dashed var(--button-bg)'; - item.addEventListener('click', handleClick); - }); - - // ESC to cancel - const handleEscape = (event) => { - if (event.key === 'Escape') { - instruction.remove(); - gridItems.forEach(item => { - item.removeEventListener('click', handleClick); - item.style.cursor = ''; - item.style.border = ''; - }); - this.comparisonSlot = null; - if (overlay) overlay.classList.add('open'); - document.removeEventListener('keydown', handleEscape); - } - }; - document.addEventListener('keydown', handleEscape); - } - - /** - * Add plot to comparison panel - */ - addPlotToComparison(plotInfo, slot) { - this.plots[slot] = plotInfo; - - const container = document.getElementById(`${slot}PlotContainer`); - const title = document.getElementById(`${slot}PlotTitle`); - const replaceBtn = document.getElementById(`${slot}ReplaceBtn`); - - if (container) { - container.innerHTML = ` - ${plotInfo.name} -
- ${plotInfo.name}
- ${plotInfo.path} -
- `; - } - - if (title) title.textContent = plotInfo.name; - if (replaceBtn) replaceBtn.style.display = 'block'; - - // Reset grid item styles - const gridItems = document.querySelectorAll('.grid-item'); - gridItems.forEach(item => { - item.style.cursor = ''; - item.style.border = ''; - }); - } - - /** - * Replace plot in comparison - */ - replacePlot(slot) { - this.selectPlotForComparison(slot); - } -} diff --git a/gallery/assets/js/export-manager.js b/gallery/assets/js/export-manager.js deleted file mode 100644 index b60967b..0000000 --- a/gallery/assets/js/export-manager.js +++ /dev/null @@ -1,448 +0,0 @@ -/** - * Export Manager for Gallery - * Handles exporting selected plots to merged PDF - */ - -export class ExportManager { - constructor() { - this.selectedPlots = new Set(); - this.maxPlots = 4; - this.init(); - } - - init() { - this.createExportButton(); - this.bindEvents(); - } - - /** - * Create the export button in the floating buttons section - */ - createExportButton() { - const floatingButtons = document.querySelector('.floating-buttons'); - if (!floatingButtons) return; - - const exportBtn = document.createElement('button'); - exportBtn.className = 'floating-btn export-btn'; - exportBtn.id = 'exportBtn'; - exportBtn.title = 'Export Selected Plots (Ctrl+E)'; - exportBtn.innerHTML = '📄'; - exportBtn.style.display = 'none'; // Hidden by default - exportBtn.onclick = () => this.exportSelectedPlots(); - - floatingButtons.appendChild(exportBtn); - - // Add selection counter - const selectionCounter = document.createElement('div'); - selectionCounter.className = 'selection-counter'; - selectionCounter.id = 'selectionCounter'; - selectionCounter.style.display = 'none'; - selectionCounter.innerHTML = '0/4 selected'; - floatingButtons.appendChild(selectionCounter); - } - - /** - * Bind events for plot selection - */ - bindEvents() { - // Add selection mode toggle - document.addEventListener('keydown', (e) => { - if (e.ctrlKey && e.key === 'e') { - e.preventDefault(); - this.toggleSelectionMode(); - } - if (e.key === 'Escape') { - this.exitSelectionMode(); - } - }); - - // Add selection handlers to existing plots - this.addSelectionHandlers(); - } - - /** - * Add selection handlers to all plot items - */ - addSelectionHandlers() { - const plotItems = document.querySelectorAll('.grid-item'); - plotItems.forEach(item => this.addSelectionHandler(item)); - } - - /** - * Add selection handler to a single plot item - */ - addSelectionHandler(item) { - // Create selection overlay - const overlay = document.createElement('div'); - overlay.className = 'selection-overlay'; - overlay.innerHTML = ` -
- -
- `; - item.appendChild(overlay); - - // Add click handler for selection - overlay.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - this.togglePlotSelection(item); - }); - } - - /** - * Toggle selection mode - */ - toggleSelectionMode() { - const body = document.body; - const isSelectionMode = body.classList.contains('selection-mode'); - - if (isSelectionMode) { - this.exitSelectionMode(); - } else { - this.enterSelectionMode(); - } - } - - /** - * Enter selection mode - */ - enterSelectionMode() { - document.body.classList.add('selection-mode'); - document.getElementById('exportBtn').style.display = 'block'; - document.getElementById('selectionCounter').style.display = 'block'; - this.updateSelectionCounter(); - } - - /** - * Exit selection mode - */ - exitSelectionMode() { - const wasInSelectionMode = document.body.classList.contains('selection-mode'); - - document.body.classList.remove('selection-mode'); - document.getElementById('exportBtn').style.display = 'none'; - document.getElementById('selectionCounter').style.display = 'none'; - this.clearSelection(); - - // Show message if user was actually in selection mode - if (wasInSelectionMode) { - this.showMessage('Exited selection mode', 'info'); - } - } - - /** - * Toggle plot selection - */ - togglePlotSelection(item) { - const plotName = this.getPlotName(item); - const plotPath = this.getPlotPath(item); - - if (this.selectedPlots.has(plotName)) { - this.selectedPlots.delete(plotName); - item.classList.remove('selected'); - item.querySelector('.checkbox-icon').textContent = '☐'; - } else { - if (this.selectedPlots.size >= this.maxPlots) { - this.showMessage(`Maximum ${this.maxPlots} plots can be selected`, 'warning'); - return; - } - this.selectedPlots.add(plotName); - item.classList.add('selected'); - item.querySelector('.checkbox-icon').textContent = '☑'; - } - - this.updateSelectionCounter(); - } - - /** - * Get plot name from grid item - */ - getPlotName(item) { - const plotName = item.querySelector('.plot-name'); - return plotName ? plotName.textContent.trim() : ''; - } - - /** - * Get plot PDF path from grid item - */ - getPlotPath(item) { - const link = item.querySelector('a[href$=".pdf"]'); - return link ? link.href : ''; - } - - /** - * Update selection counter - */ - updateSelectionCounter() { - const counter = document.getElementById('selectionCounter'); - if (counter) { - counter.textContent = `${this.selectedPlots.size}/${this.maxPlots} selected`; - } - - const exportBtn = document.getElementById('exportBtn'); - if (exportBtn) { - exportBtn.disabled = this.selectedPlots.size === 0; - exportBtn.style.opacity = this.selectedPlots.size === 0 ? '0.5' : '1'; - } - } - - /** - * Clear all selections - */ - clearSelection() { - this.selectedPlots.clear(); - document.querySelectorAll('.grid-item.selected').forEach(item => { - item.classList.remove('selected'); - const checkbox = item.querySelector('.checkbox-icon'); - if (checkbox) checkbox.textContent = '☐'; - }); - this.updateSelectionCounter(); - } - - /** - * Export selected plots to merged PDF - */ - async exportSelectedPlots() { - if (this.selectedPlots.size === 0) { - this.showMessage('No plots selected', 'warning'); - return; - } - - const plotPaths = Array.from(this.selectedPlots).map(plotName => { - const item = Array.from(document.querySelectorAll('.grid-item')) - .find(item => this.getPlotName(item) === plotName); - return this.getPlotPath(item); - }); - - this.showMessage('Preparing export...', 'info'); - - try { - await this.createMergedPDF(plotPaths); - } catch (error) { - this.showMessage('Export failed: ' + error.message, 'error'); - } - } - - /** - * Create merged PDF using Python script - */ - async createMergedPDF(plotPaths) { - // Convert file:// URLs to actual paths - const actualPaths = plotPaths.map(url => { - if (url.startsWith('file://')) { - return url.substring(7); // Remove 'file://' prefix - } - return url; - }); - - const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0]; - const outputName = `merged_plots_${timestamp}.pdf`; - - const payload = { - plots: actualPaths, - layout: this.calculateLayout(actualPaths.length), - output_name: outputName - }; - - // Generate a unique temporary filename - const tempFileName = `export_request_${Date.now()}.json`; - - // Get work directory from config or fallback - const workDir = window.galleryConfig?.workDir || '/work/kschmidt/web'; - - // Save the request to a JSON file that can be picked up by a Python script - const requestData = JSON.stringify(payload, null, 2); - - // Show improved export instructions with full command - this.showExportInstructions(requestData, tempFileName, workDir); - } - - /** - * Calculate optimal layout for given number of plots - */ - calculateLayout(numPlots) { - switch (numPlots) { - case 1: return { rows: 1, cols: 1 }; - case 2: return { rows: 1, cols: 2 }; - case 3: return { rows: 2, cols: 2 }; // 3 plots in 2x2 grid with one empty - case 4: return { rows: 2, cols: 2 }; - default: return { rows: 2, cols: 2 }; - } - } - - /** - * Show export instructions to user - */ - showExportInstructions(requestData, tempFileName, workDir) { - const tempFilePath = `/tmp/${tempFileName}`; - const fullCommand = `echo '${requestData.replace(/'/g, "'\\''")}' > ${tempFilePath} && cd ${workDir} && python export_plots.py ${tempFilePath}`; - - const instructions = ` -
-

🚀 Export Selected Plots

-

Run the following command in your terminal to export the selected plots:

- -
-
- ${fullCommand} -
-
- - - -
-
- -
-

📋 Command Breakdown:

-
    -
  • Creates temporary file: ${tempFilePath}
  • -
  • Changes to work directory: ${workDir}
  • -
  • Runs export script: python export_plots.py
  • -
  • Output file: Will be saved in the work directory
  • -
-
- -
-

💡 Tips:

-
    -
  • The temporary JSON file will be automatically cleaned up after successful export
  • -
  • Use Esc to exit selection mode
  • -
  • Press Ctrl+E to toggle selection mode
  • -
-
-
- `; - - const overlay = document.createElement('div'); - overlay.className = 'export-overlay'; - overlay.innerHTML = instructions; - - // Add methods to the overlay for button handlers - overlay.copyCommand = function() { - navigator.clipboard.writeText(fullCommand).then(() => { - this.showCopyFeedback('Command copied to clipboard!'); - }).catch(() => { - this.showCopyFeedback('Failed to copy. Please select and copy manually.', 'error'); - }); - }; - - overlay.copyJSON = function() { - navigator.clipboard.writeText(requestData).then(() => { - this.showCopyFeedback('JSON copied to clipboard!'); - }).catch(() => { - this.showCopyFeedback('Failed to copy. Please select and copy manually.', 'error'); - }); - }; - - overlay.close = function() { - this.remove(); - }; - - overlay.showCopyFeedback = function(message, type = 'success') { - const feedback = document.createElement('div'); - feedback.className = `copy-feedback copy-feedback-${type}`; - feedback.textContent = message; - this.appendChild(feedback); - - setTimeout(() => { - if (feedback.parentNode) { - feedback.parentNode.removeChild(feedback); - } - }, 2000); - }; - - document.body.appendChild(overlay); - - // Close on ESC key - const handleEscape = (e) => { - if (e.key === 'Escape') { - overlay.remove(); - document.removeEventListener('keydown', handleEscape); - } - }; - document.addEventListener('keydown', handleEscape); - - // Close on clicking outside - overlay.addEventListener('click', (e) => { - if (e.target === overlay) { - overlay.remove(); - document.removeEventListener('keydown', handleEscape); - } - }); - } - - /** - * Download the PDF blob - */ - downloadPDF(blob) { - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `merged_plots_${new Date().toISOString().split('T')[0]}.pdf`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - } - - /** - * Show temporary message to user - */ - showMessage(text, type = 'info') { - // Remove existing message - const existing = document.querySelector('.export-message'); - if (existing) existing.remove(); - - const message = document.createElement('div'); - message.className = `export-message export-message-${type}`; - message.textContent = text; - - document.body.appendChild(message); - - setTimeout(() => { - if (message.parentNode) { - message.parentNode.removeChild(message); - } - }, 3000); - } -} - -// Add these methods to ExportManager if not present -ExportManager.prototype.isSelectionModeActive = function() { - return document.body.classList.contains('selection-mode'); -}; -ExportManager.prototype.exitSelectionMode = function() { - document.body.classList.remove('selection-mode'); - if (typeof this.clearSelection === 'function') { - this.clearSelection(); - } -}; - -// Ensure a single global instance -window.exportManager = window.exportManager || new ExportManager(); - -// Listen for ESC key globally to exit selection mode -// (This will work even if focus is not on a plot) -document.addEventListener('keydown', function(e) { - if (e.key === 'Escape' && window.exportManager && window.exportManager.isSelectionModeActive()) { - window.exportManager.exitSelectionMode(); - } -}); - -// Attach improved export logic to export button -document.addEventListener('DOMContentLoaded', function() { - const exportBtn = document.getElementById('exportBtn'); - if (exportBtn) { - exportBtn.addEventListener('click', function() { - window.exportManager.exportSelectedPlots(); - }); - } -}); diff --git a/gallery/assets/js/folder-metadata.js b/gallery/assets/js/folder-metadata.js deleted file mode 100644 index 86f9db4..0000000 --- a/gallery/assets/js/folder-metadata.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Folder Metadata functionality for Gallery - * - * Handles folder metadata dropdown display and interaction - */ - -// Define function immediately (not waiting for DOM) -window.toggleFolderMetadata = function() { - console.log('toggleFolderMetadata called'); - - const container = document.querySelector('.folder-metadata-container'); - const content = document.getElementById('folderMetadataContent'); - - if (!container) { - console.log('No metadata container found'); - return; - } - - if (!content) { - console.log('No metadata content found'); - return; - } - - const isExpanded = container.classList.contains('expanded'); - console.log('Current state - expanded:', isExpanded); - - if (isExpanded) { - // Collapse - container.classList.remove('expanded'); - content.style.display = 'none'; - console.log('Collapsed dropdown'); - } else { - // Expand - container.classList.add('expanded'); - content.style.display = 'block'; - console.log('Expanded dropdown'); - } - - // Save state - localStorage.setItem('folderMetadataExpanded', (!isExpanded).toString()); -}; - -// Also define as regular function for alternative access -function toggleFolderMetadata() { - window.toggleFolderMetadata(); -} - -// Toggle long text display -window.toggleMetadataText = function(button) { - const longText = button.previousElementSibling; - const fullText = button.nextElementSibling; - - if (fullText.style.display === 'none') { - longText.style.display = 'none'; - fullText.style.display = 'inline'; - button.textContent = 'Show less'; - } else { - longText.style.display = 'inline'; - fullText.style.display = 'none'; - button.textContent = 'Show more'; - } -}; - -// Initialize folder metadata on page load -document.addEventListener('DOMContentLoaded', function() { - console.log('Folder metadata script loaded'); - - // Make sure all containers start collapsed - const containers = document.querySelectorAll('.folder-metadata-container'); - console.log('Found', containers.length, 'metadata containers'); - - containers.forEach(container => { - const content = container.querySelector('.folder-metadata-content'); - if (content) { - // Force initial hidden state - container.classList.remove('expanded'); - content.style.display = 'none'; - console.log('Initialized container as collapsed'); - } - }); -}); diff --git a/gallery/assets/js/gallery-app.js b/gallery/assets/js/gallery-app.js deleted file mode 100644 index 849286e..0000000 --- a/gallery/assets/js/gallery-app.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Main Gallery Application - * Orchestrates all the different managers and functionality - */ -import { ThemeManager } from './theme-manager.js'; -import { NavigationManager } from './navigation-manager.js'; -import { SearchManager } from './search-manager.js'; -import { RecentPlotsManager } from './recent-plots-manager.js'; -import { ComparisonManager } from './comparison-manager.js'; -import { StatsManager } from './stats-manager.js'; -import { KeyboardManager } from './keyboard-manager.js'; -import { ViewManager } from './view-manager.js'; -import { SortManager } from './sort-manager.js'; -import { Utils } from './utils.js'; - -/** - * Main Gallery Application Class - */ -export class GalleryApp { - constructor(config = {}) { - // Configuration from backend template variables - this.SEARCH_DEBOUNCE_MS = config.searchDebounceMs || 300; - this.MAX_RECENT_PLOTS = config.maxRecentPlots || 20; - this.stats = config.stats || null; - - // Initialize managers - this.themeManager = new ThemeManager(); - this.navigationManager = new NavigationManager(); - this.searchManager = new SearchManager(this.SEARCH_DEBOUNCE_MS); - this.recentPlotsManager = new RecentPlotsManager(this.MAX_RECENT_PLOTS); - this.comparisonManager = new ComparisonManager(); - this.statsManager = new StatsManager(); - this.viewManager = new ViewManager(); - this.sortManager = new SortManager(); - this.keyboardManager = new KeyboardManager(this); - this.utils = Utils; - - // Set global references for backward compatibility - window.themeManager = this.themeManager; - window.searchManager = this.searchManager; - window.recentPlotsManager = this.recentPlotsManager; - window.comparisonManager = this.comparisonManager; - window.viewManager = this.viewManager; - window.sortManager = this.sortManager; - window.utils = this.utils; - - this.init(); - } - - /** - * Initialize the gallery application - */ - init() { - this.navigationManager.buildBreadcrumb(); - this.navigationManager.buildFolderTree(); - - // Update stats with backend data if available - if (this.stats) { - this.statsManager.updateWithBackendStats(this.stats); - } - - // Handle URL-based thumbnail highlighting - Utils.handleThumbnailHighlight(); - } - - // Backward compatibility methods - toggleTheme() { this.themeManager.toggle(); } - toggleSidebar() { this.recentPlotsManager.toggleSidebar(); } - toggleCompareMode() { this.comparisonManager.toggleCompareMode(); } - closeComparison() { this.comparisonManager.closeComparison(); } - selectPlotForComparison(slot) { this.comparisonManager.selectPlotForComparison(slot); } - replacePlot(slot) { this.comparisonManager.replacePlot(slot); } -} diff --git a/gallery/assets/js/keyboard-manager.js b/gallery/assets/js/keyboard-manager.js deleted file mode 100644 index c535a9c..0000000 --- a/gallery/assets/js/keyboard-manager.js +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Keyboard shortcuts management - */ -export class KeyboardManager { - constructor(galleryApp) { - this.app = galleryApp; - this.init(); - } - - /** - * Initialize keyboard shortcuts - */ - init() { - document.addEventListener('keydown', (e) => { - if (e.ctrlKey && e.key === 'k') { - e.preventDefault(); - const searchBox = document.getElementById('searchBox'); - if (searchBox) searchBox.focus(); - } - - if (e.ctrlKey && e.key === 'r') { - e.preventDefault(); - if (this.app.recentPlotsManager) { - this.app.recentPlotsManager.toggleSidebar(); - } - } - - if (e.ctrlKey && e.key === 'c') { - e.preventDefault(); - if (this.app.comparisonManager) { - this.app.comparisonManager.toggleCompareMode(); - } - } - - if (e.ctrlKey && e.key === 't') { - e.preventDefault(); - if (this.app.themeManager) { - this.app.themeManager.toggle(); - } - } - - if (e.ctrlKey && e.key === 'v') { - e.preventDefault(); - if (this.app.viewManager) { - this.app.viewManager.cycleView(); - } - } - - if (e.ctrlKey && e.key === 'n') { - e.preventDefault(); - if (this.app.sortManager) { - this.app.sortManager.setSortType('name'); - } - } - - if (e.ctrlKey && e.key === 'm') { - e.preventDefault(); - if (this.app.sortManager) { - this.app.sortManager.setSortType('time'); - } - } - - if (e.ctrlKey && e.key === 'o') { - e.preventDefault(); - if (this.app.sortManager) { - this.app.sortManager.toggleSortOrder(); - } - } - - if (e.key === '?' && !e.ctrlKey && !e.altKey && !e.metaKey) { - e.preventDefault(); - if (this.app.utils && this.app.utils.toggleShortcutsHelp) { - this.app.utils.toggleShortcutsHelp(); - } - } - - if (e.key === 'Escape') { - this.handleEscape(); - } - }); - } - - /** - * Handle escape key actions - */ - handleEscape() { - // If in plot selection mode, cancel it - if (this.app.comparisonManager && this.app.comparisonManager.comparisonSlot) { - // Find and remove instruction element - const instruction = document.getElementById('comparisonInstruction'); - if (instruction) instruction.remove(); - - // Reset grid item styles - const gridItems = document.querySelectorAll('.grid-item'); - gridItems.forEach(item => { - item.style.cursor = ''; - item.style.border = ''; - }); - - // Show overlay again - const comparisonOverlay = document.getElementById('comparisonOverlay'); - if (comparisonOverlay) comparisonOverlay.classList.add('open'); - this.app.comparisonManager.comparisonSlot = null; - return; - } - - // Close comparison overlay if open - const comparisonOverlay = document.getElementById('comparisonOverlay'); - if (comparisonOverlay && comparisonOverlay.classList.contains('open')) { - if (this.app.comparisonManager) { - this.app.comparisonManager.hideComparisonOverlay(); - } - return; - } - - // Other ESC behaviors - const searchResults = document.getElementById('searchResults'); - if (searchResults) searchResults.style.display = 'none'; - - const sidebar = document.getElementById('sidebar'); - if (sidebar && sidebar.classList.contains('open')) { - if (this.app.recentPlotsManager) { - this.app.recentPlotsManager.toggleSidebar(); - } - } - - const shortcutsHelp = document.getElementById('shortcutsHelp'); - if (shortcutsHelp) shortcutsHelp.style.display = 'none'; - } -} diff --git a/gallery/assets/js/main.js b/gallery/assets/js/main.js deleted file mode 100644 index 8f924e6..0000000 --- a/gallery/assets/js/main.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Main entry point for the Gallery application - * This file initializes the app when the DOM is ready - */ -import { GalleryApp } from './gallery-app.js'; - -// Global app instance for backward compatibility -let app; - -// Global functions for onclick handlers (backward compatibility) -function toggleTheme() { app.toggleTheme(); } -function toggleSidebar() { app.toggleSidebar(); } - -// Make functions globally available -window.toggleTheme = toggleTheme; -window.toggleSidebar = toggleSidebar; - -// Initialize application when DOM is ready -document.addEventListener('DOMContentLoaded', function() { - // Configuration will be injected by the template - const config = window.galleryConfig || {}; - - app = new GalleryApp(config); - - // Make app globally available - window.app = app; -}); diff --git a/gallery/assets/js/metadata-popup.js b/gallery/assets/js/metadata-popup.js deleted file mode 100644 index 085c3a9..0000000 --- a/gallery/assets/js/metadata-popup.js +++ /dev/null @@ -1,212 +0,0 @@ -/** - * 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 ` - - - `; - } - - let 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 => ``).join(' '); - } else { - formattedValue = `${value.slice(0, 3).map(item => ``).join(' ')} `; - } - } 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 ` - - `; - } -} - -// 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/gallery/assets/js/metadata-section.js b/gallery/assets/js/metadata-section.js deleted file mode 100644 index b8f2485..0000000 --- a/gallery/assets/js/metadata-section.js +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Simple Metadata Section Toggle - * - * Handles showing/hiding the metadata grid with a simple button - */ - -// Global function to toggle metadata section visibility -function toggleMetadataSection() { - console.log('toggleMetadataSection called'); - - const content = document.getElementById('metadataContent'); - const arrow = document.getElementById('metadataArrow'); - - if (!content) { - console.log('No metadata content found'); - return; - } - - const isVisible = content.style.display !== 'none'; - - if (isVisible) { - // Hide the content - content.style.display = 'none'; - if (arrow) arrow.textContent = '▼'; - console.log('Metadata hidden'); - } else { - // Show the content - content.style.display = 'block'; - if (arrow) arrow.textContent = '▲'; - console.log('Metadata shown'); - - // Trigger MathJax rendering for LaTeX content - if (typeof MathJax !== 'undefined') { - MathJax.typesetPromise([content]).catch(function (err) { - console.log('MathJax typeset failed: ' + err.message); - }); - } - } - - // Save state to localStorage - localStorage.setItem('metadataVisible', (!isVisible).toString()); -} - -// Function to expand long text -function expandText(button) { - const longText = button.previousElementSibling; - const fullText = button.nextElementSibling; - - if (fullText.style.display === 'none') { - longText.style.display = 'none'; - fullText.style.display = 'inline'; - button.textContent = 'Show less'; - } else { - longText.style.display = 'inline'; - fullText.style.display = 'none'; - button.textContent = 'Show more'; - } -} - -// Copy metadata file path to clipboard -async function copyMetadataPath() { - const pathElement = document.getElementById('metadata-file-path'); - const copyBtn = document.querySelector('.copy-path-btn'); - - if (!pathElement || !copyBtn) { - console.log('Path element or copy button not found'); - return; - } - - const path = pathElement.textContent; - console.log('Attempting to copy path:', path); - - try { - // Try using the modern clipboard API - if (navigator.clipboard && window.isSecureContext) { - await navigator.clipboard.writeText(path); - } else { - // Fallback for older browsers - const textArea = document.createElement('textarea'); - textArea.value = path; - textArea.style.position = 'fixed'; - textArea.style.left = '-999999px'; - textArea.style.top = '-999999px'; - document.body.appendChild(textArea); - textArea.focus(); - textArea.select(); - document.execCommand('copy'); - textArea.remove(); - } - - // Visual feedback - const originalText = copyBtn.innerHTML; - copyBtn.innerHTML = '✅ Copied!'; - copyBtn.classList.add('copied'); - - setTimeout(() => { - copyBtn.innerHTML = originalText; - copyBtn.classList.remove('copied'); - }, 2000); - - console.log('Path copied successfully'); - - } catch (err) { - console.error('Failed to copy path: ', err); - - // Show error feedback - const originalText = copyBtn.innerHTML; - copyBtn.innerHTML = '❌ Failed'; - - setTimeout(() => { - copyBtn.innerHTML = originalText; - }, 2000); - } -} - -// Make functions globally available -window.toggleMetadataSection = toggleMetadataSection; -window.expandText = expandText; -window.copyMetadataPath = copyMetadataPath; - -// Initialize on page load -document.addEventListener('DOMContentLoaded', function() { - console.log('Metadata section script loaded'); - - const content = document.getElementById('metadataContent'); - if (content) { - // Check if user previously had it expanded - const wasVisible = localStorage.getItem('metadataVisible') === 'true'; - - if (wasVisible) { - content.style.display = 'block'; - const arrow = document.getElementById('metadataArrow'); - if (arrow) arrow.textContent = '▲'; - } else { - content.style.display = 'none'; - const arrow = document.getElementById('metadataArrow'); - if (arrow) arrow.textContent = '▼'; - } - - console.log('Metadata section initialized, visible:', wasVisible); - } -}); diff --git a/gallery/assets/js/navigation-manager.js b/gallery/assets/js/navigation-manager.js deleted file mode 100644 index b9a69d6..0000000 --- a/gallery/assets/js/navigation-manager.js +++ /dev/null @@ -1,208 +0,0 @@ -/** - * Navigation functionality - breadcrumbs and folder tree - */ -export class NavigationManager { - /** - * Build breadcrumb navigation based on current path - */ - buildBreadcrumb() { - const currentPath = window.location.pathname; - const pathParts = currentPath.split('/').filter(part => part !== '' && part !== 'index.html'); - const breadcrumb = document.getElementById('breadcrumb'); - - if (!breadcrumb) return; - - if (pathParts.length === 0) { - breadcrumb.innerHTML = '🏠 Root'; - return; - } - - let html = '🏠 Root'; - - for (let i = 0; i < pathParts.length; i++) { - const part = pathParts[i]; - html += '/'; - - if (i === pathParts.length - 1) { - html += `${decodeURIComponent(part)}`; - } else { - const levelsUp = pathParts.length - 1 - i; - const relativePath = '../'.repeat(levelsUp) + 'index.html'; - html += `${decodeURIComponent(part)}`; - } - } - - breadcrumb.innerHTML = html; - } - - /** - * Build and display the folder tree structure - */ - async buildFolderTree() { - const treeContainer = document.getElementById('folderTree'); - const currentPath = window.location.pathname; - - if (!treeContainer) return; - - try { - const tree = await this.buildTreeRecursive(currentPath, 0, currentPath); - treeContainer.innerHTML = tree; - } catch (error) { - console.error('Error building folder tree:', error); - treeContainer.innerHTML = '
❌ Error loading folder tree
'; - } - } - - /** - * Recursively build tree structure for folders with collapsed empty directories - */ - async buildTreeRecursive(path, depth, currentPath, maxDepth = 5) { - if (depth > maxDepth) { - const indent = ' '.repeat(depth); - return `
${indent}└─ ...
`; - } - - try { - const response = await fetch(path); - const htmlContent = await response.text(); - const parser = new DOMParser(); - const doc = parser.parseFromString(htmlContent, 'text/html'); - - const subdirs = doc.querySelectorAll('h2 + ul li a'); - const items = doc.querySelectorAll('.grid-item'); - - // Check if this is an empty directory (only one subdirectory, no items) - if (items.length === 0 && subdirs.length === 1) { - const subdir = subdirs[0]; - const href = subdir.getAttribute('href'); - if (href) { - const baseUrl = path.replace(/\/[^\/]*$/, '/'); - const subPath = baseUrl + href; - - const collapsedPath = await this.getCollapsedPath(path, subPath); - return await this.buildCollapsedTreeItem(collapsedPath, depth, currentPath, maxDepth); - } - } - - // Normal directory processing - const indent = ' '.repeat(depth); - const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery'; - const totalItems = items.length + subdirs.length; - const arrow = depth === 0 ? '' : '└─ '; - - let html = ''; - if (path === currentPath) { - html += `
${indent}${arrow}📁 ${folderName} (${totalItems} items)
`; - } else { - html += `
${indent}${arrow}📁 ${folderName} (${totalItems} items)
`; - } - - for (let i = 0; i < subdirs.length; i++) { - const subdir = subdirs[i]; - const href = subdir.getAttribute('href'); - if (href) { - const baseUrl = path.replace(/\/[^\/]*$/, '/'); - const subPath = baseUrl + href; - html += await this.buildTreeRecursive(subPath, depth + 1, currentPath, maxDepth); - } - } - - return html; - } catch (error) { - const indent = ' '.repeat(depth); - const arrow = depth === 0 ? '' : '└─ '; - const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery'; - return `
${indent}${arrow}📁 ${folderName} (error loading)
`; - } - } - - /** - * Get the collapsed path by following empty directories - */ - async getCollapsedPath(startPath, currentPath) { - const pathSegments = []; - let path = startPath; - - while (true) { - try { - const response = await fetch(path); - const htmlContent = await response.text(); - const parser = new DOMParser(); - const doc = parser.parseFromString(htmlContent, 'text/html'); - - const subdirs = doc.querySelectorAll('h2 + ul li a'); - const items = doc.querySelectorAll('.grid-item'); - - const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery'; - pathSegments.push({ name: folderName, path: path }); - - if (items.length > 0 || subdirs.length !== 1) { - break; - } - - const subdir = subdirs[0]; - const href = subdir.getAttribute('href'); - if (href) { - const baseUrl = path.replace(/\/[^\/]*$/, '/'); - path = baseUrl + href; - } else { - break; - } - } catch (error) { - break; - } - } - - return { - segments: pathSegments, - finalPath: path - }; - } - - /** - * Build a collapsed tree item for empty directory chains - */ - async buildCollapsedTreeItem(collapsedPath, depth, currentPath, maxDepth) { - const indent = ' '.repeat(depth); - const arrow = depth === 0 ? '' : '└─ '; - - const displayName = collapsedPath.segments.map(seg => seg.name).join(' / '); - const finalPath = collapsedPath.finalPath; - - let totalItems = 0; - let subdirs = []; - try { - const response = await fetch(finalPath); - const htmlContent = await response.text(); - const parser = new DOMParser(); - const doc = parser.parseFromString(htmlContent, 'text/html'); - - const subdirElements = doc.querySelectorAll('h2 + ul li a'); - const items = doc.querySelectorAll('.grid-item'); - totalItems = items.length + subdirElements.length; - - subdirs = Array.from(subdirElements); - } catch (error) { - // Handle error case - } - - let html = ''; - if (finalPath === currentPath) { - html += `
${indent}${arrow}📁 ${displayName} (${totalItems} items)
`; - } else { - html += `
${indent}${arrow}📁 ${displayName} (${totalItems} items)
`; - } - - for (let i = 0; i < subdirs.length; i++) { - const subdir = subdirs[i]; - const href = subdir.getAttribute('href'); - if (href) { - const baseUrl = finalPath.replace(/\/[^\/]*$/, '/'); - const subPath = baseUrl + href; - html += await this.buildTreeRecursive(subPath, depth + 1, currentPath, maxDepth); - } - } - - return html; - } -} diff --git a/gallery/assets/js/recent-plots-manager.js b/gallery/assets/js/recent-plots-manager.js deleted file mode 100644 index 29fd3bc..0000000 --- a/gallery/assets/js/recent-plots-manager.js +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Recent plots sidebar management - */ -export class RecentPlotsManager { - constructor(maxRecentPlots = 20) { - this.MAX_RECENT_PLOTS = maxRecentPlots; - this.init(); - } - - init() { - this.updateRecentPlotsDisplay(); - this.trackPlotClicks(); - } - - /** - * Add plot to recent plots list - */ - addToRecentPlots(plotHref) { - const plotName = plotHref.split('/').pop().replace('.pdf', ''); - const pathParts = plotHref.split('/').filter(p => p !== '' && p !== plotName + '.pdf'); - const plotPath = pathParts.join(' / '); - const thumbUrl = plotHref.replace('.pdf', '.png'); - - // Determine the gallery page URL (directory containing the plot) - const plotDir = plotHref.substring(0, plotHref.lastIndexOf('/')); - const galleryUrl = plotDir + '/index.html'; - - const plotInfo = { - name: plotName, - path: plotPath, - href: plotHref, - thumbUrl: thumbUrl, - galleryUrl: galleryUrl, - timestamp: Date.now() - }; - - let recentPlots = JSON.parse(localStorage.getItem('recentPlots') || '[]'); - recentPlots = recentPlots.filter(p => p.href !== plotHref); - recentPlots.unshift(plotInfo); - recentPlots = recentPlots.slice(0, this.MAX_RECENT_PLOTS); - - localStorage.setItem('recentPlots', JSON.stringify(recentPlots)); - this.updateRecentPlotsDisplay(); - } - - /** - * Update recent plots sidebar display - */ - updateRecentPlotsDisplay() { - const sidebarContent = document.getElementById('sidebarContent'); - if (!sidebarContent) return; - - const recentPlots = JSON.parse(localStorage.getItem('recentPlots') || '[]'); - - if (recentPlots.length === 0) { - sidebarContent.innerHTML = ` -
- 📭 No recent plots yet
- Open some plots to see them here -
- `; - return; - } - - let html = ''; - recentPlots.forEach(plot => { - html += ` -
- ${plot.name} -
-
${plot.name}
-
📍 ${plot.path}
-
-
- `; - }); - - sidebarContent.innerHTML = html; - } - - /** - * Open recent plot gallery page and highlight thumbnail - */ - openRecentPlot(galleryUrl, plotName) { - // Navigate to gallery page with plot highlight parameter - const url = new URL(galleryUrl, window.location.origin); - url.searchParams.set('highlight', plotName); - window.location.href = url.toString(); - this.toggleSidebar(); - } - - /** - * Toggle recent plots sidebar - */ - toggleSidebar() { - const sidebar = document.getElementById('sidebar'); - const overlay = document.getElementById('sidebarOverlay'); - - if (sidebar) sidebar.classList.toggle('open'); - if (overlay) overlay.classList.toggle('open'); - } - - /** - * Track clicks on plot links - */ - trackPlotClicks() { - document.addEventListener('click', (e) => { - const link = e.target.closest('a[href$=".pdf"]'); - if (link) { - this.addToRecentPlots(link.href); - } - }); - } -} diff --git a/gallery/assets/js/search-manager.js b/gallery/assets/js/search-manager.js deleted file mode 100644 index 20eff08..0000000 --- a/gallery/assets/js/search-manager.js +++ /dev/null @@ -1,217 +0,0 @@ -/** - * Search functionality - */ -export class SearchManager { - constructor(debounceMs = 300) { - this.searchTimeout = null; - this.SEARCH_DEBOUNCE_MS = debounceMs; - this.init(); - } - - /** - * Initialize search functionality with debouncing - */ - init() { - const searchBox = document.getElementById('searchBox'); - const searchResults = document.getElementById('searchResults'); - - if (!searchBox || !searchResults) return; - - searchBox.addEventListener('input', (e) => { - clearTimeout(this.searchTimeout); - const query = e.target.value.trim(); - - if (query.length === 0) { - searchResults.style.display = 'none'; - return; - } - - this.searchTimeout = setTimeout(() => { - this.performSearch(query); - }, this.SEARCH_DEBOUNCE_MS); - }); - - document.addEventListener('click', (e) => { - if (!searchBox.contains(e.target) && !searchResults.contains(e.target)) { - searchResults.style.display = 'none'; - } - }); - } - - /** - * Perform search across plot names - */ - async performSearch(query) { - const searchResults = document.getElementById('searchResults'); - if (!searchResults) return; - - searchResults.innerHTML = '
🔍 Searching...
'; - searchResults.style.display = 'block'; - - try { - const results = await this.searchPlots(query); - this.displaySearchResults(results, query); - } catch (error) { - console.error('Search error:', error); - searchResults.innerHTML = '
❌ Search failed
'; - } - } - - /** - * Search for plots matching the query - */ - async searchPlots(query) { - const results = []; - const visited = new Set(); - const lowerQuery = query.toLowerCase(); - - await this.searchInPage(window.location.pathname, lowerQuery, results, visited); - await this.searchRecursive(window.location.pathname, lowerQuery, results, visited, 0, 5); - - return results.slice(0, 20); - } - - /** - * Search for plots in a specific page - */ - async searchInPage(path, query, results, visited, maxResults = 50) { - if (visited.has(path) || results.length >= maxResults) return; - visited.add(path); - - try { - const response = await fetch(path); - const html = await response.text(); - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); - - const items = doc.querySelectorAll('.grid-item'); - items.forEach(item => { - const nameElement = item.querySelector('.plot-name'); - const imgElement = item.querySelector('img'); - const linkElement = item.querySelector('a'); - - if (nameElement && imgElement && linkElement) { - const name = nameElement.textContent.toLowerCase(); - if (name.includes(query)) { - results.push({ - name: nameElement.textContent, - path: path, - href: linkElement.href, - imgSrc: imgElement.src, - relevance: this.calculateRelevance(name, query) - }); - } - } - }); - } catch (error) { - console.error('Error searching in', path, error); - } - } - - /** - * Recursively search in subdirectories - */ - async searchRecursive(path, query, results, visited, depth, maxDepth) { - if (depth >= maxDepth || results.length >= 50) return; - - try { - const response = await fetch(path); - const html = await response.text(); - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); - - const subdirs = doc.querySelectorAll('h2 + ul li a'); - for (const subdir of subdirs) { - const href = subdir.getAttribute('href'); - if (href) { - const baseUrl = path.replace(/\/[^\/]*$/, '/'); - const subPath = baseUrl + href; - await this.searchInPage(subPath, query, results, visited); - await this.searchRecursive(subPath, query, results, visited, depth + 1, maxDepth); - } - } - } catch (error) { - console.error('Error in recursive search:', error); - } - } - - /** - * Calculate search relevance score - */ - calculateRelevance(text, query) { - const exactMatch = text === query; - const startsWith = text.startsWith(query); - const wordMatch = text.split(/\s+/).some(word => word.startsWith(query)); - - if (exactMatch) return 100; - if (startsWith) return 80; - if (wordMatch) return 60; - return 40; - } - - /** - * Display search results with highlighting - */ - displaySearchResults(results, query) { - const searchResults = document.getElementById('searchResults'); - if (!searchResults) return; - - if (results.length === 0) { - searchResults.innerHTML = '
📭 No plots found
'; - return; - } - - results.sort((a, b) => b.relevance - a.relevance); - - let html = ''; - results.forEach(result => { - const highlightedName = this.highlightText(result.name, query); - const relativePath = this.getRelativePath(result.path); - - html += ` -
-
- -
-
- ${highlightedName} -
-
- 📍 ${relativePath} -
-
-
-
- `; - }); - - searchResults.innerHTML = html; - } - - /** - * Highlight search query in text - */ - highlightText(text, query) { - const regex = new RegExp(`(${query})`, 'gi'); - return text.replace(regex, '$1'); - } - - /** - * Get relative path for display - */ - getRelativePath(fullPath) { - const parts = fullPath.split('/').filter(p => p !== '' && p !== 'index.html'); - return parts.length > 0 ? parts.join(' / ') : 'Root'; - } - - /** - * Open search result and track it - */ - openSearchResult(href) { - if (window.recentPlotsManager) { - window.recentPlotsManager.addToRecentPlots(href); - } - window.open(href, '_blank'); - document.getElementById('searchResults').style.display = 'none'; - } -} diff --git a/gallery/assets/js/sort-manager.js b/gallery/assets/js/sort-manager.js deleted file mode 100644 index 8ec9a85..0000000 --- a/gallery/assets/js/sort-manager.js +++ /dev/null @@ -1,197 +0,0 @@ -/** - * Sort Manager - handles sorting of plot items by name and creation time - */ -export class SortManager { - constructor() { - this.currentSort = 'name'; - this.currentOrder = 'asc'; - this.init(); - } - - /** - * Initialize sort controls - */ - init() { - // Use setTimeout to ensure DOM is ready - setTimeout(() => { - this.setupSortButtons(); - this.loadSavedSort(); - }, 100); - } - - /** - * Setup sort button event listeners - */ - setupSortButtons() { - const sortButtons = document.querySelectorAll('.sort-btn'); - const orderButton = document.querySelector('.sort-order-btn'); - - if (sortButtons.length === 0) { - setTimeout(() => this.setupSortButtons(), 500); - return; - } - - sortButtons.forEach((button) => { - const sortType = button.getAttribute('data-sort'); - - button.addEventListener('click', (e) => { - e.preventDefault(); - this.setSortType(sortType); - }); - }); - - if (orderButton) { - orderButton.addEventListener('click', (e) => { - e.preventDefault(); - this.toggleSortOrder(); - }); - } - - // Initialize button states - this.updateSortButtons(); - this.updateOrderButton(); - } - - /** - * Set the sort type (name or time) - */ - setSortType(sortType) { - if (sortType === this.currentSort) return; - - this.currentSort = sortType; - this.updateSortButtons(); - this.sortPlots(); - this.saveSortPreference(); - } - - /** - * Toggle sort order between ascending and descending - */ - toggleSortOrder() { - this.currentOrder = this.currentOrder === 'asc' ? 'desc' : 'asc'; - this.updateOrderButton(); - this.sortPlots(); - this.saveSortPreference(); - } - - /** - * Update visual state of sort buttons - */ - updateSortButtons() { - const sortButtons = document.querySelectorAll('.sort-btn'); - - sortButtons.forEach(btn => { - if (btn.getAttribute('data-sort') === this.currentSort) { - btn.classList.add('active'); - } else { - btn.classList.remove('active'); - } - }); - } - - /** - * Update visual state of order button - */ - updateOrderButton() { - const orderButton = document.querySelector('.sort-order-btn'); - if (orderButton) { - orderButton.textContent = this.currentOrder === 'asc' ? '↑' : '↓'; - orderButton.setAttribute('data-order', this.currentOrder); - orderButton.title = `Sort Order: ${this.currentOrder === 'asc' ? 'Ascending' : 'Descending'}`; - } - } - - /** - * Sort the plot items - */ - sortPlots() { - const plotContainer = document.getElementById('plotContainer'); - if (!plotContainer) return; - - const plotItems = Array.from(plotContainer.children); - - plotItems.sort((a, b) => { - let valueA, valueB; - - if (this.currentSort === 'name') { - valueA = a.getAttribute('data-name') || ''; - valueB = b.getAttribute('data-name') || ''; - - // Natural sort for better number handling - const result = valueA.localeCompare(valueB, undefined, { - numeric: true, - sensitivity: 'base' - }); - return this.currentOrder === 'asc' ? result : -result; - } else if (this.currentSort === 'time') { - valueA = parseInt(a.getAttribute('data-time') || '0'); - valueB = parseInt(b.getAttribute('data-time') || '0'); - - const result = valueA - valueB; - return this.currentOrder === 'asc' ? result : -result; - } - - return 0; - }); - - // Re-append sorted items - plotItems.forEach(item => { - plotContainer.appendChild(item); - }); - } - - /** - * Save sort preferences to localStorage - */ - saveSortPreference() { - try { - localStorage.setItem('gallery-sort-type', this.currentSort); - localStorage.setItem('gallery-sort-order', this.currentOrder); - } catch (e) { - // Ignore localStorage errors - } - } - - /** - * Load saved sort preferences - */ - loadSavedSort() { - try { - const savedSort = localStorage.getItem('gallery-sort-type'); - const savedOrder = localStorage.getItem('gallery-sort-order'); - - if (savedSort && ['name', 'time'].includes(savedSort)) { - this.currentSort = savedSort; - } - - if (savedOrder && ['asc', 'desc'].includes(savedOrder)) { - this.currentOrder = savedOrder; - } - - this.updateSortButtons(); - this.updateOrderButton(); - - // Sort immediately if there are plots - setTimeout(() => this.sortPlots(), 100); - } catch (e) { - // Ignore localStorage errors, use defaults - } - } - - /** - * Get current sort settings - */ - getCurrentSort() { - return { - type: this.currentSort, - order: this.currentOrder - }; - } - - /** - * Refresh sorting (call this when plot content changes) - */ - refresh() { - this.sortPlots(); - } -} diff --git a/gallery/assets/js/stats-manager.js b/gallery/assets/js/stats-manager.js deleted file mode 100644 index 9ad8df5..0000000 --- a/gallery/assets/js/stats-manager.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Statistics manager for gallery display - */ -export class StatsManager { - constructor() { - this.updateGalleryStats(); - } - - /** - * Update gallery statistics display - */ - updateGalleryStats() { - // Use stats passed from Python backend if available - // Otherwise fallback to DOM counting - const gridItems = document.querySelectorAll('.grid-item'); - const subdirLinks = document.querySelectorAll('a[href$="/index.html"]'); - - const fileCountEl = document.getElementById('fileCount'); - const folderCountEl = document.getElementById('folderCount'); - const totalSizeEl = document.getElementById('totalSize'); - const lastUpdatedEl = document.getElementById('lastUpdated'); - - if (fileCountEl) fileCountEl.textContent = gridItems.length; - if (folderCountEl) folderCountEl.textContent = subdirLinks.length; - if (totalSizeEl) totalSizeEl.textContent = 'Unknown'; - - // Set last updated time - const now = new Date(); - const timeStr = now.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit' - }); - if (lastUpdatedEl) lastUpdatedEl.textContent = timeStr; - } - - /** - * Update stats with backend data - */ - updateWithBackendStats(stats) { - const fileCountEl = document.getElementById('fileCount'); - const folderCountEl = document.getElementById('folderCount'); - const totalSizeEl = document.getElementById('totalSize'); - - if (fileCountEl && stats.file_count !== undefined) { - fileCountEl.textContent = stats.file_count; - } - if (folderCountEl && stats.folder_count !== undefined) { - folderCountEl.textContent = stats.folder_count; - } - if (totalSizeEl && stats.total_size !== undefined) { - totalSizeEl.textContent = stats.total_size; - } - } -} diff --git a/gallery/assets/js/theme-manager.js b/gallery/assets/js/theme-manager.js deleted file mode 100644 index f23affc..0000000 --- a/gallery/assets/js/theme-manager.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Theme management functionality - */ -export class ThemeManager { - constructor() { - this.init(); - } - - /** - * Initialize theme system and load saved preference - */ - init() { - const savedTheme = localStorage.getItem('theme'); - const html = document.documentElement; - const themeToggle = document.getElementById('themeToggle'); - - if (savedTheme === 'light') { - html.setAttribute('data-theme', 'light'); - if (themeToggle) themeToggle.textContent = '🌙'; - } else { - html.removeAttribute('data-theme'); - if (themeToggle) themeToggle.textContent = '☀️'; - } - } - - /** - * Toggle between light and dark themes - */ - toggle() { - const html = document.documentElement; - const themeToggle = document.getElementById('themeToggle'); - - if (html.getAttribute('data-theme') === 'light') { - html.removeAttribute('data-theme'); - if (themeToggle) themeToggle.textContent = '☀️'; - localStorage.setItem('theme', 'dark'); - } else { - html.setAttribute('data-theme', 'light'); - if (themeToggle) themeToggle.textContent = '🌙'; - localStorage.setItem('theme', 'light'); - } - } -} diff --git a/gallery/assets/js/utils.js b/gallery/assets/js/utils.js deleted file mode 100644 index 3c5b432..0000000 --- a/gallery/assets/js/utils.js +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Utility functions and helpers - */ -export class Utils { - /** - * Format file size in human readable format - */ - static formatFileSize(bytes) { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; - } - - /** - * Handle thumbnail highlighting from URL parameters - */ - static handleThumbnailHighlight() { - const urlParams = new URLSearchParams(window.location.search); - const highlightPlot = urlParams.get('highlight'); - - if (highlightPlot) { - // Find and highlight the thumbnail - const gridItems = document.querySelectorAll('.grid-item'); - gridItems.forEach(item => { - const plotName = item.querySelector('.plot-name'); - if (plotName && plotName.textContent.trim() === highlightPlot) { - item.classList.add('highlighted'); - // Scroll to the highlighted item - setTimeout(() => { - item.scrollIntoView({ - behavior: 'smooth', - block: 'center' - }); - }, 100); - // Remove highlight after animation - setTimeout(() => { - item.classList.remove('highlighted'); - }, 3000); - } - }); - - // Clean up URL - const newUrl = new URL(window.location); - newUrl.searchParams.delete('highlight'); - window.history.replaceState({}, document.title, newUrl.toString()); - } - } - - /** - * Calculate approximate total size of displayed files - */ - static async calculateApproximateSize() { - const images = document.querySelectorAll('.grid-item img'); - let totalSize = 0; - let loadedCount = 0; - - const sizeElement = document.getElementById('totalSize'); - if (!sizeElement) return; - - sizeElement.textContent = 'Loading...'; - - // Estimate size based on a sample of images - const sampleSize = Math.min(images.length, 5); - const sampleImages = Array.from(images).slice(0, sampleSize); - - if (sampleImages.length === 0) { - sizeElement.textContent = '0 KB'; - return; - } - - // Calculate average size from sample - for (const img of sampleImages) { - try { - const response = await fetch(img.src, { method: 'HEAD' }); - const size = parseInt(response.headers.get('content-length') || '0'); - if (size > 0) { - totalSize += size; - loadedCount++; - } - } catch (e) { - // Fallback: estimate 100KB per image - totalSize += 102400; - loadedCount++; - } - } - - if (loadedCount > 0) { - const averageSize = totalSize / loadedCount; - const estimatedTotal = averageSize * images.length; - sizeElement.textContent = Utils.formatFileSize(estimatedTotal); - } else { - sizeElement.textContent = 'Unknown'; - } - } - - /** - * Toggle keyboard shortcuts help display - */ - static toggleShortcutsHelp() { - const help = document.getElementById('shortcutsHelp'); - if (help) { - help.style.display = help.style.display === 'block' ? 'none' : 'block'; - } - } -} diff --git a/gallery/assets/js/view-manager.js b/gallery/assets/js/view-manager.js deleted file mode 100644 index 0d51208..0000000 --- a/gallery/assets/js/view-manager.js +++ /dev/null @@ -1,192 +0,0 @@ -/** - * View Controls Manager - handles switching between grid, list-large, and list-compact views - */ -export class ViewManager { - constructor() { - this.currentView = 'grid'; - this.init(); - } - - /** - * Initialize view controls - */ - init() { - this.setupViewButtons(); - this.loadSavedView(); - this.updateControlsVisibility(); - } - - /** - * Update visibility of view controls based on plot content - */ - updateControlsVisibility() { - const plotContainer = document.getElementById('plotContainer'); - const controlsContainer = document.querySelector('.controls-container'); - - if (!plotContainer || !controlsContainer) return; - - const hasPlots = plotContainer.children.length > 0; - controlsContainer.style.display = hasPlots ? 'flex' : 'none'; - } - - /** - * Setup view toggle buttons - */ - setupViewButtons() { - const viewButtons = document.querySelectorAll('.view-btn'); - - viewButtons.forEach(button => { - button.addEventListener('click', (e) => { - const newView = button.getAttribute('data-view'); - this.switchView(newView); - }); - }); - } - - /** - * Switch to a different view mode - */ - switchView(viewMode) { - if (viewMode === this.currentView) return; - - const plotContainer = document.getElementById('plotContainer'); - const viewButtons = document.querySelectorAll('.view-btn'); - - if (!plotContainer) return; - - // Remove current view class - plotContainer.classList.remove( - 'grid-view', - 'list-large-view', - 'list-compact-view' - ); - - // Add new view class - switch (viewMode) { - case 'grid': - plotContainer.classList.add('grid-view'); - break; - case 'list-large': - plotContainer.classList.add('list-large-view'); - break; - case 'list-compact': - plotContainer.classList.add('list-compact-view'); - break; - default: - plotContainer.classList.add('grid-view'); - viewMode = 'grid'; - } - - // Update button states - viewButtons.forEach(btn => { - if (btn.getAttribute('data-view') === viewMode) { - btn.classList.add('active'); - } else { - btn.classList.remove('active'); - } - }); - - // Save the preference - this.currentView = viewMode; - this.saveViewPreference(viewMode); - - // Trigger any necessary layout updates - this.onViewChanged(viewMode); - } - - /** - * Save view preference to localStorage - */ - saveViewPreference(viewMode) { - try { - localStorage.setItem('gallery-view-mode', viewMode); - } catch (e) { - // Ignore localStorage errors - } - } - - /** - * Load saved view preference - */ - loadSavedView() { - try { - const savedView = localStorage.getItem('gallery-view-mode'); - if (savedView && ['grid', 'list-large', 'list-compact'].includes(savedView)) { - this.switchView(savedView); - } - } catch (e) { - // Ignore localStorage errors, use default - } - } - - /** - * Handle view change events - can be extended for additional functionality - */ - onViewChanged(viewMode) { - // Dispatch custom event for other components that might need to know - const event = new CustomEvent('viewChanged', { - detail: { viewMode } - }); - document.dispatchEvent(event); - - // Update any other UI elements that depend on view mode - this.updateUIForView(viewMode); - } - - /** - * Update UI elements based on current view - */ - updateUIForView(viewMode) { - // You can add view-specific UI updates here - // For example, adjusting search result highlighting, etc. - - // Update any tooltips or help text - const viewButtons = document.querySelectorAll('.view-btn'); - viewButtons.forEach(btn => { - const btnView = btn.getAttribute('data-view'); - if (btnView === viewMode) { - btn.style.transform = 'scale(1.05)'; - } else { - btn.style.transform = 'scale(1)'; - } - }); - } - - /** - * Get current view mode - */ - getCurrentView() { - return this.currentView; - } - - /** - * Refresh controls visibility (call this when gallery content changes) - */ - refresh() { - this.updateControlsVisibility(); - } - - /** - * Check if current view is grid mode - */ - isGridView() { - return this.currentView === 'grid'; - } - - /** - * Check if current view is list mode (either variant) - */ - isListView() { - return this.currentView === 'list-large' || this.currentView === 'list-compact'; - } - - /** - * Cycle through view modes (useful for keyboard shortcuts) - */ - cycleView() { - const views = ['grid', 'list-large', 'list-compact']; - const currentIndex = views.indexOf(this.currentView); - const nextIndex = (currentIndex + 1) % views.length; - this.switchView(views[nextIndex]); - } -} diff --git a/gallery/builder.py b/gallery/builder.py index eec847b..73d2cfb 100644 --- a/gallery/builder.py +++ b/gallery/builder.py @@ -2,7 +2,7 @@ import shutil from pathlib import Path -from typing import Dict, Any, Optional +from typing import Dict, Any, Optional, Union from jinja2 import Environment, FileSystemLoader, Template from gallery.config import GalleryConfig @@ -22,7 +22,7 @@ from gallery.utils.processing import ( ) -def get_template(template_dir: Optional[Path | str] = None): +def get_template(template_dir: Optional[Union[Path, str]] = None): """ Get the Jinja2 template for gallery rendering. @@ -167,11 +167,16 @@ def copy_assets( gallery_root = Path(config.web_folder) / config.plot_root assets_dst = gallery_root.parent / "assets" - main_css_src = assets_src / "css" / "main.css" - main_css_dst = assets_dst / "css" / "main.css" + # Use the newest mtime across all source asset files as the staleness check, + # so any change to any JS/CSS file triggers a redeploy. + sentinel_dst = assets_dst / "css" / "main.css" + newest_src_mtime = max( + (f.stat().st_mtime for f in assets_src.rglob('*') if f.is_file()), + default=0, + ) + dst_mtime = sentinel_dst.stat().st_mtime if sentinel_dst.exists() else 0 - if (not assets_dst.exists() or - needs_update(main_css_src, main_css_dst)): + if not assets_dst.exists() or newest_src_mtime > (dst_mtime + 30): if assets_dst.exists(): shutil.rmtree(assets_dst) shutil.copytree(assets_src, assets_dst) diff --git a/gallery/templates/gallery.html b/gallery/templates/gallery.html index 635852f..8b82b68 100644 --- a/gallery/templates/gallery.html +++ b/gallery/templates/gallery.html @@ -203,16 +203,6 @@ {% endfor %} - - {% if subdirs %} -

Subdirectories

- - {% endif %} -