From bc756fa0cd30712e847f4246cf52d2a8b09d377d Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 19 Nov 2025 00:46:58 +0100 Subject: [PATCH] feat: comprehensive SVG viewer enhancements with zoom and template preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add interactive zoom functionality (25%-300% with Ctrl+scroll wheel) - Fix SVG width constraints by injecting calculated dimensions into templates - Implement template preview with theme-aware sample data - Enhance UI layout with file reordering (Project → Template → CSS → Data) - Add blue theme support for my-project alongside existing green theme - Fix my-project field mapping from empty 'Übergeordnet' to 'Status' - Improve SVG viewport handling with proper scrolling and container management - Add visual zoom controls with percentage display and smart visibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- engine.js | 262 +++++++++++++++++++++++++++++++++++++++- generator.js | 49 ++++++-- index.html | 126 ++++++++++++++++--- my-project/project.json | 2 +- my-project/style.css | 65 +++++++++- my-project/template.svg | 53 +++++++- 6 files changed, 524 insertions(+), 33 deletions(-) diff --git a/engine.js b/engine.js index f409c74..c006117 100644 --- a/engine.js +++ b/engine.js @@ -6,6 +6,117 @@ window.timelineEngine = { projectBasePath: '', + showTemplatePreview() { + if (!this.template) return; + + console.log("Showing template preview"); + + // Detect template theme for appropriate colors + const isEnhanced = this.template.includes('Enhanced'); + const isBlueTheme = this.template.includes('My Project'); + const isGreenTheme = this.template.includes('Example'); + + // Color scheme selection + let primaryColor = '#0A4D8C'; // default + let secondaryColor = '#5C6B7A'; // default + + if (isEnhanced) { + if (isBlueTheme) { + primaryColor = '#3b82f6'; + secondaryColor = '#60a5fa'; + } else if (isGreenTheme) { + primaryColor = '#2d8659'; + secondaryColor = '#4a9b6b'; + } + } + + // Create sample month graphics matching the template style + const sampleMonths = isEnhanced ? ` + + + Jan 25 + + + + Feb 25 + + + + Mar 25 + + + + Apr 25 + ` : ` + + Jan 25 + + Feb 25 + + Mar 25 + + Apr 25 + `; + + // Create sample lane content matching the template style + const sampleLanes = isEnhanced ? ` + + Example Epic + + + EX-1: + Sample Task Preview + + + + Another Epic + + + EX-2: + Template Preview + + ` : ` + + Example Epic + + + EX-1: + Sample Task Preview + + + + Another Epic + + + EX-2: + Template Preview + + `; + + // Replace placeholders with sample data + let previewSvg = this.template + .replace("{{MONTHS}}", sampleMonths) + .replace("{{LANES}}", sampleLanes); + + // Add dimensions for proper preview display (using reasonable default size) + const previewWidth = 720; + const previewHeight = 360; + previewSvg = previewSvg.replace( + /]*?)>/, + `` + ); + + document.getElementById("viewer").innerHTML = previewSvg; + + // Initialize zoom functionality for the preview + if (window.svgViewer) { + window.svgViewer.resetZoom(); + window.svgViewer.updateZoomControls(); + } + + console.log("Template preview displayed with theme:", { isEnhanced, isBlueTheme, isGreenTheme }); + }, + updateFileStatus(fileType, filename, status = 'loaded') { const statusElement = document.getElementById(`${fileType}File`); @@ -47,9 +158,10 @@ window.timelineEngine = { async autoLoadDefaultProject() { console.log("Starting autoLoadDefaultProject"); - // Versuche zuerst Binect-Projekt, sonst Example + // Try projects in order: binect, my-project, example const candidates = [ { path: "binect/project.json", basePath: "binect/" }, + { path: "my-project/project.json", basePath: "my-project/" }, { path: "example/project.json", basePath: "example/" } ]; for (const candidate of candidates) { @@ -122,6 +234,11 @@ window.timelineEngine = { this.template = await response.text(); console.log("SVG template loaded, length:", this.template.length); this.updateFileStatus('svg', cfg.svgTemplate, 'loaded'); + + // Show template preview if no CSV data is loaded yet + if (!this.csvOverride && document.querySelector("#viewer").innerHTML.includes("Keine Timeline verfügbar")) { + this.showTemplatePreview(); + } } catch (e) { console.error("SVG template could not be loaded:", e); console.error("Failed SVG template path was:", this.resolveProjectPath(cfg.svgTemplate)); @@ -238,6 +355,13 @@ window.timelineEngine = { const dlBtn = document.getElementById("downloadSvg"); dlBtn.disabled = false; dlBtn.style.opacity = 1; + + // Initialize zoom functionality for the new SVG + if (window.svgViewer) { + window.svgViewer.resetZoom(); + window.svgViewer.updateZoomControls(); + } + console.log("Timeline generated successfully"); } catch (error) { console.error("Error generating timeline:", error); @@ -275,6 +399,136 @@ window.timelineEngine = { } }; +// --------- SVG Viewer with Zoom functionality --------- + +window.svgViewer = { + currentZoom: 1, + minZoom: 0.25, + maxZoom: 3, + zoomStep: 0.25, + + initializeZoom() { + const viewer = document.getElementById("viewer"); + const zoomControls = document.getElementById("zoomControls"); + const zoomIn = document.getElementById("zoomIn"); + const zoomOut = document.getElementById("zoomOut"); + const zoomReset = document.getElementById("zoomReset"); + + if (!viewer || !zoomControls || !zoomIn || !zoomOut || !zoomReset) { + console.warn("Zoom controls not found in DOM"); + return; + } + + // Show zoom controls when SVG is present + this.updateZoomControls(); + + // Zoom in + zoomIn.addEventListener("click", () => { + this.zoomIn(); + }); + + // Zoom out + zoomOut.addEventListener("click", () => { + this.zoomOut(); + }); + + // Reset zoom + zoomReset.addEventListener("click", () => { + this.resetZoom(); + }); + + // Mouse wheel zoom (with Ctrl key) + viewer.addEventListener("wheel", (e) => { + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + if (e.deltaY < 0) { + this.zoomIn(); + } else { + this.zoomOut(); + } + } + }); + + console.log("SVG zoom functionality initialized"); + }, + + showZoomControls() { + const zoomControls = document.getElementById("zoomControls"); + if (zoomControls) { + zoomControls.style.display = "block"; + } + }, + + hideZoomControls() { + const zoomControls = document.getElementById("zoomControls"); + if (zoomControls) { + zoomControls.style.display = "none"; + } + }, + + zoomIn() { + if (this.currentZoom < this.maxZoom) { + this.currentZoom = Math.min(this.maxZoom, this.currentZoom + this.zoomStep); + this.applyZoom(); + } + }, + + zoomOut() { + if (this.currentZoom > this.minZoom) { + this.currentZoom = Math.max(this.minZoom, this.currentZoom - this.zoomStep); + this.applyZoom(); + } + }, + + resetZoom() { + this.currentZoom = 1; + this.applyZoom(); + }, + + applyZoom() { + const viewer = document.getElementById("viewer"); + const container = document.getElementById("viewerContainer"); + if (viewer && container) { + viewer.style.transform = `scale(${this.currentZoom})`; + + // Adjust container overflow behavior based on zoom level + if (this.currentZoom !== 1) { + viewer.classList.add("zoomed"); + container.style.overflow = "auto"; + } else { + viewer.classList.remove("zoomed"); + container.style.overflow = "hidden"; + } + + this.updateZoomControls(); + } + }, + + updateZoomControls() { + const zoomIn = document.getElementById("zoomIn"); + const zoomOut = document.getElementById("zoomOut"); + const zoomReset = document.getElementById("zoomReset"); + + if (zoomIn && zoomOut && zoomReset) { + // Update button states + zoomIn.disabled = this.currentZoom >= this.maxZoom; + zoomOut.disabled = this.currentZoom <= this.minZoom; + + // Update zoom percentage display + const percentage = Math.round(this.currentZoom * 100); + zoomReset.textContent = `${percentage}%`; + + // Show/hide controls based on whether SVG is present + const hasSvg = document.querySelector("#viewer svg") !== null; + if (hasSvg) { + this.showZoomControls(); + } else { + this.hideZoomControls(); + } + } + } +}; + // --------- UI event handlers --------- function setupEventHandlers() { @@ -295,6 +549,9 @@ function setupEventHandlers() { if (cfg.name && cfg.name.includes('Example')) { window.timelineEngine.projectBasePath = 'example/'; console.log("Detected example project, setting base path to example/"); + } else if (cfg.name && (cfg.name.includes('My Project') || cfg.name.includes('Roadmap'))) { + window.timelineEngine.projectBasePath = 'my-project/'; + console.log("Detected my-project, setting base path to my-project/"); } else { window.timelineEngine.projectBasePath = ''; console.log("Unknown project, clearing base path"); @@ -352,6 +609,9 @@ function setupEventHandlers() { if (!file) return; window.timelineEngine.template = await file.text(); window.timelineEngine.updateFileStatus('svg', file.name, 'loaded'); + + // Show template preview immediately when manually loaded + window.timelineEngine.showTemplatePreview(); }); } diff --git a/generator.js b/generator.js index 6bae671..08dd92e 100644 --- a/generator.js +++ b/generator.js @@ -48,13 +48,18 @@ window.timelineGenerator = { // Enhanced styling when using external template if (template && template.includes('Enhanced')) { + // Determine color scheme based on template content + const isBlueTheme = template.includes('My Project'); + const primaryColor = isBlueTheme ? '#3b82f6' : '#2d8659'; + const secondaryColor = isBlueTheme ? '#60a5fa' : '#4a9b6b'; + // More prominent month indicators for external template - monthGraphics += ``; - monthGraphics += ``; - monthGraphics += `${label}`; + monthGraphics += ``; + monthGraphics += ``; + monthGraphics += `${label}`; // Add month separator if (i > 0) { - monthGraphics += ``; + monthGraphics += ``; } } else { // Default styling @@ -76,10 +81,15 @@ window.timelineGenerator = { // Enhanced styling when using external template if (template && template.includes('Enhanced')) { + // Determine color scheme based on template content + const isBlueTheme = template.includes('My Project'); + const primaryColor = isBlueTheme ? '#3b82f6' : '#2d8659'; + const secondaryColor = isBlueTheme ? '#60a5fa' : '#4a9b6b'; + // More subtle lane borders for enhanced template - laneBlocks += ``; + laneBlocks += ``; // Enhanced lane label - laneBlocks += `${this.escapeXml(laneName)}`; + laneBlocks += `${this.escapeXml(laneName)}`; } else { // Default lane styling laneBlocks += ``; @@ -96,10 +106,16 @@ window.timelineGenerator = { // Enhanced task item styling for external template if (template && template.includes('Enhanced')) { - laneBlocks += ``; - laneBlocks += ` + // Determine color scheme based on template content + const isBlueTheme = template.includes('My Project'); + const primaryColor = isBlueTheme ? '#3b82f6' : '#2d8659'; + const secondaryColor = isBlueTheme ? '#60a5fa' : '#4a9b6b'; + const darkColor = isBlueTheme ? '#1e40af' : '#1e5a3d'; + + laneBlocks += ``; + laneBlocks += ` ${this.escapeXml(it.id || "")}: - ${this.escapeXml(it.title || "")} + ${this.escapeXml(it.title || "")} `; } else { // Default task item styling @@ -113,9 +129,22 @@ window.timelineGenerator = { }); if (template && template.includes("{{MONTHS}}") && template.includes("{{LANES}}")) { - return template + // Calculate dimensions for template + const height = top + laneNames.length * (laneHeight + laneGap) + 80; + const width = left + months.length * monthWidth + 100; + + // Replace placeholders and inject calculated dimensions + let processedTemplate = template .replace("{{MONTHS}}", monthGraphics) .replace("{{LANES}}", laneBlocks); + + // Add width and height attributes to the SVG element + processedTemplate = processedTemplate.replace( + /]*?)>/, + `` + ); + + return processedTemplate; } // Fallback: embed directly in simple SVG diff --git a/index.html b/index.html index 273ab92..6a58eb9 100644 --- a/index.html +++ b/index.html @@ -147,6 +147,82 @@ cursor: not-allowed; opacity: 0.6; } + + /* SVG Viewer enhancements */ + #viewerContainer { + position: relative; + border: 1px solid #ccd3db; + background: white; + border-radius: 8px; + min-height: 400px; + width: 100%; + overflow: hidden; + } + + #viewer { + overflow: auto; + padding: 12px; + height: 100%; + max-height: 80vh; + transform-origin: top left; + transition: transform 0.2s ease; + width: 100%; + box-sizing: border-box; + min-width: 100%; + } + + #viewer svg { + max-width: none !important; + height: auto !important; + width: auto !important; + display: block; + margin: 0; + min-width: 100%; + box-sizing: content-box; + } + + /* Ensure zoom doesn't get clipped */ + #viewer.zoomed { + overflow: visible; + width: fit-content; + height: fit-content; + max-height: none; + } + + #zoomControls { + position: absolute; + top: 8px; + right: 8px; + z-index: 10; + background: rgba(255,255,255,0.95); + border-radius: 4px; + padding: 4px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + display: none; + } + + #zoomControls button { + padding: 4px 8px; + margin: 2px; + border: 1px solid #ccc; + border-radius: 3px; + background: white; + cursor: pointer; + font-size: 12px; + transition: all 0.2s ease; + } + + #zoomControls button:hover:not(:disabled) { + background: #f0f0f0; + border-color: #999; + transform: none; /* Override global button transform */ + } + + #zoomControls button:disabled { + background: #f8f8f8; + color: #ccc; + cursor: not-allowed; + } @@ -155,7 +231,8 @@

Timeline Generator

- Lade Projektdateien um eine Timeline zu erstellen oder verwende den lokalen Server für automatisches Laden. + Lade Projektdateien um eine Timeline zu erstellen oder verwende den lokalen Server für automatisches Laden.
+ 💡 Zoom: Verwende die Zoom-Buttons oder Strg+Scrollrad für große Timelines.

@@ -178,14 +255,14 @@
- CSV Data + SVG Template
- Not loaded + Not loaded
@@ -204,14 +281,14 @@
- SVG Template + CSV Data
- Not loaded + Not loaded
@@ -227,13 +304,24 @@ -
-
-
📊
-

Keine Timeline verfügbar

-

- Lade eine Projektkonfiguration oder CSV-Datei um zu beginnen. -

+ +
+ +
+ + + +
+ + +
+
+
📊
+

Keine Timeline verfügbar

+

+ Lade eine Projektkonfiguration oder CSV-Datei um zu beginnen. +

+
@@ -245,6 +333,12 @@ console.log("Event handlers set up"); } + // Initialize zoom functionality + if (window.svgViewer && typeof window.svgViewer.initializeZoom === 'function') { + window.svgViewer.initializeZoom(); + console.log("SVG zoom initialized"); + } + // Ensure engines are loaded before auto-loading function tryAutoLoad() { if (window.timelineEngine && window.timelineGenerator) { diff --git a/my-project/project.json b/my-project/project.json index a38055f..3423cd0 100644 --- a/my-project/project.json +++ b/my-project/project.json @@ -10,7 +10,7 @@ "fieldMapping": { "id": "Schlüssel", "title": "Zusammenfassung", - "lane": "Übergeordnet", + "lane": "Status", "due": [ "Abgeleitetes Fälligkeitsdatum", "Fälligkeitsdatum" diff --git a/my-project/style.css b/my-project/style.css index ac96614..ba9fcf7 100644 --- a/my-project/style.css +++ b/my-project/style.css @@ -1 +1,64 @@ -body { background:#f5f7fa; } +/* My Project Blue Theme */ +/* This CSS demonstrates successful external stylesheet loading */ + +body { + background: #1a2332 !important; +} + +#projectName { + color: #3b82f6 !important; + border-bottom: 2px solid #3b82f6; + padding-bottom: 8px; +} + +#projectSubtitle { + color: #60a5fa !important; +} + +/* File Manager Override */ +#fileManager { + background: #1e293b !important; + border-color: #3b82f6 !important; +} + +#fileManager h3 { + color: #60a5fa !important; +} + +.file-item { + background: #334155 !important; + border-color: #3b82f6 !important; +} + +.file-item:hover { + border-color: #60a5fa !important; + box-shadow: 0 2px 8px rgba(59, 130, 246, 0.2) !important; +} + +.file-label { + color: #60a5fa !important; +} + +.upload-btn { + background: #3b82f6 !important; +} + +.upload-btn:hover { + background: #2563eb !important; +} + +/* Buttons */ +button { + background: #3b82f6 !important; +} + +button:hover:not(:disabled) { + background: #60a5fa !important; +} + +/* Viewer */ +#viewer { + background: #334155 !important; + border-color: #3b82f6 !important; + color: #f1f5f9 !important; +} diff --git a/my-project/template.svg b/my-project/template.svg index 91cc643..aa99d8f 100644 --- a/my-project/template.svg +++ b/my-project/template.svg @@ -1,5 +1,50 @@ - - - {{MONTHS}} - {{LANES}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 📅 My Project Timeline + + + Enhanced blue styling with prominent months ✨ + + + + + + {{MONTHS}} + + + + + {{LANES}} + + + +