generated from coulomb/repo-seed
feat: comprehensive SVG viewer enhancements with zoom and template preview
- 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 <noreply@anthropic.com>
This commit is contained in:
262
engine.js
262
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 ? `
|
||||
<line x1="240" y1="75" x2="240" y2="300" stroke="${secondaryColor}" stroke-width="2" opacity="0.6" />
|
||||
<rect x="210" y="70" width="60" height="25" fill="${primaryColor}" opacity="0.1" rx="4" />
|
||||
<text x="244" y="90" fill="${primaryColor}" font-size="13" font-weight="600">Jan 25</text>
|
||||
|
||||
<line x1="360" y1="75" x2="360" y2="300" stroke="${secondaryColor}" stroke-width="2" opacity="0.6" />
|
||||
<rect x="330" y="70" width="60" height="25" fill="${primaryColor}" opacity="0.1" rx="4" />
|
||||
<text x="364" y="90" fill="${primaryColor}" font-size="13" font-weight="600">Feb 25</text>
|
||||
|
||||
<line x1="480" y1="75" x2="480" y2="300" stroke="${secondaryColor}" stroke-width="2" opacity="0.6" />
|
||||
<rect x="450" y="70" width="60" height="25" fill="${primaryColor}" opacity="0.1" rx="4" />
|
||||
<text x="484" y="90" fill="${primaryColor}" font-size="13" font-weight="600">Mar 25</text>
|
||||
|
||||
<line x1="600" y1="75" x2="600" y2="300" stroke="${secondaryColor}" stroke-width="2" opacity="0.6" />
|
||||
<rect x="570" y="70" width="60" height="25" fill="${primaryColor}" opacity="0.1" rx="4" />
|
||||
<text x="604" y="90" fill="${primaryColor}" font-size="13" font-weight="600">Apr 25</text>
|
||||
` : `
|
||||
<line x1="240" y1="75" x2="240" y2="300" stroke="#E3E8EF" />
|
||||
<text x="244" y="90" fill="#5C6B7A" font-size="12">Jan 25</text>
|
||||
<line x1="360" y1="75" x2="360" y2="300" stroke="#E3E8EF" />
|
||||
<text x="364" y="90" fill="#5C6B7A" font-size="12">Feb 25</text>
|
||||
<line x1="480" y1="75" x2="480" y2="300" stroke="#E3E8EF" />
|
||||
<text x="484" y="90" fill="#5C6B7A" font-size="12">Mar 25</text>
|
||||
<line x1="600" y1="75" x2="600" y2="300" stroke="#E3E8EF" />
|
||||
<text x="604" y="90" fill="#5C6B7A" font-size="12">Apr 25</text>
|
||||
`;
|
||||
|
||||
// Create sample lane content matching the template style
|
||||
const sampleLanes = isEnhanced ? `
|
||||
<rect x="40" y="116" width="640" height="80" fill="rgba(255,255,255,0.7)" stroke="${secondaryColor}" stroke-width="1" opacity="0.5" rx="8" />
|
||||
<text x="56" y="136" fill="${primaryColor}" font-size="14" font-weight="700">Example Epic</text>
|
||||
<circle cx="270" cy="156" r="6" fill="${primaryColor}" stroke="${secondaryColor}" stroke-width="2" />
|
||||
<text x="282" y="160" font-size="12" fill="${primaryColor}" font-weight="500">
|
||||
<tspan class="item-id">EX-1: </tspan>
|
||||
<tspan class="item-title" fill="${primaryColor === '#3b82f6' ? '#1e40af' : '#1e5a3d'}">Sample Task Preview</tspan>
|
||||
</text>
|
||||
|
||||
<rect x="40" y="212" width="640" height="80" fill="rgba(255,255,255,0.7)" stroke="${secondaryColor}" stroke-width="1" opacity="0.5" rx="8" />
|
||||
<text x="56" y="232" fill="${primaryColor}" font-size="14" font-weight="700">Another Epic</text>
|
||||
<circle cx="390" cy="252" r="6" fill="${primaryColor}" stroke="${secondaryColor}" stroke-width="2" />
|
||||
<text x="402" y="256" font-size="12" fill="${primaryColor}" font-weight="500">
|
||||
<tspan class="item-id">EX-2: </tspan>
|
||||
<tspan class="item-title" fill="${primaryColor === '#3b82f6' ? '#1e40af' : '#1e5a3d'}">Template Preview</tspan>
|
||||
</text>
|
||||
` : `
|
||||
<rect x="40" y="116" width="640" height="80" fill="#FFFFFF" stroke="#E3E8EF" rx="10" />
|
||||
<text x="56" y="136" fill="#0B1F3B" font-size="14" font-weight="600">Example Epic</text>
|
||||
<circle cx="270" cy="156" r="5" fill="#0A4D8C" />
|
||||
<text x="282" y="160" font-size="12" fill="#0B1F3B">
|
||||
<tspan class="item-id">EX-1: </tspan>
|
||||
<tspan class="item-title">Sample Task Preview</tspan>
|
||||
</text>
|
||||
|
||||
<rect x="40" y="212" width="640" height="80" fill="#FFFFFF" stroke="#E3E8EF" rx="10" />
|
||||
<text x="56" y="232" fill="#0B1F3B" font-size="14" font-weight="600">Another Epic</text>
|
||||
<circle cx="390" cy="252" r="5" fill="#0A4D8C" />
|
||||
<text x="402" y="256" font-size="12" fill="#0B1F3B">
|
||||
<tspan class="item-id">EX-2: </tspan>
|
||||
<tspan class="item-title">Template Preview</tspan>
|
||||
</text>
|
||||
`;
|
||||
|
||||
// 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(
|
||||
/<svg([^>]*?)>/,
|
||||
`<svg$1 width="${previewWidth}" height="${previewHeight}" viewBox="0 0 ${previewWidth} ${previewHeight}">`
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user