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: '',
|
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') {
|
updateFileStatus(fileType, filename, status = 'loaded') {
|
||||||
const statusElement = document.getElementById(`${fileType}File`);
|
const statusElement = document.getElementById(`${fileType}File`);
|
||||||
|
|
||||||
@@ -47,9 +158,10 @@ window.timelineEngine = {
|
|||||||
|
|
||||||
async autoLoadDefaultProject() {
|
async autoLoadDefaultProject() {
|
||||||
console.log("Starting autoLoadDefaultProject");
|
console.log("Starting autoLoadDefaultProject");
|
||||||
// Versuche zuerst Binect-Projekt, sonst Example
|
// Try projects in order: binect, my-project, example
|
||||||
const candidates = [
|
const candidates = [
|
||||||
{ path: "binect/project.json", basePath: "binect/" },
|
{ path: "binect/project.json", basePath: "binect/" },
|
||||||
|
{ path: "my-project/project.json", basePath: "my-project/" },
|
||||||
{ path: "example/project.json", basePath: "example/" }
|
{ path: "example/project.json", basePath: "example/" }
|
||||||
];
|
];
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
@@ -122,6 +234,11 @@ window.timelineEngine = {
|
|||||||
this.template = await response.text();
|
this.template = await response.text();
|
||||||
console.log("SVG template loaded, length:", this.template.length);
|
console.log("SVG template loaded, length:", this.template.length);
|
||||||
this.updateFileStatus('svg', cfg.svgTemplate, 'loaded');
|
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) {
|
} catch (e) {
|
||||||
console.error("SVG template could not be loaded:", e);
|
console.error("SVG template could not be loaded:", e);
|
||||||
console.error("Failed SVG template path was:", this.resolveProjectPath(cfg.svgTemplate));
|
console.error("Failed SVG template path was:", this.resolveProjectPath(cfg.svgTemplate));
|
||||||
@@ -238,6 +355,13 @@ window.timelineEngine = {
|
|||||||
const dlBtn = document.getElementById("downloadSvg");
|
const dlBtn = document.getElementById("downloadSvg");
|
||||||
dlBtn.disabled = false;
|
dlBtn.disabled = false;
|
||||||
dlBtn.style.opacity = 1;
|
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");
|
console.log("Timeline generated successfully");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error generating timeline:", 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 ---------
|
// --------- UI event handlers ---------
|
||||||
|
|
||||||
function setupEventHandlers() {
|
function setupEventHandlers() {
|
||||||
@@ -295,6 +549,9 @@ function setupEventHandlers() {
|
|||||||
if (cfg.name && cfg.name.includes('Example')) {
|
if (cfg.name && cfg.name.includes('Example')) {
|
||||||
window.timelineEngine.projectBasePath = 'example/';
|
window.timelineEngine.projectBasePath = 'example/';
|
||||||
console.log("Detected example project, setting base path to 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 {
|
} else {
|
||||||
window.timelineEngine.projectBasePath = '';
|
window.timelineEngine.projectBasePath = '';
|
||||||
console.log("Unknown project, clearing base path");
|
console.log("Unknown project, clearing base path");
|
||||||
@@ -352,6 +609,9 @@ function setupEventHandlers() {
|
|||||||
if (!file) return;
|
if (!file) return;
|
||||||
window.timelineEngine.template = await file.text();
|
window.timelineEngine.template = await file.text();
|
||||||
window.timelineEngine.updateFileStatus('svg', file.name, 'loaded');
|
window.timelineEngine.updateFileStatus('svg', file.name, 'loaded');
|
||||||
|
|
||||||
|
// Show template preview immediately when manually loaded
|
||||||
|
window.timelineEngine.showTemplatePreview();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
49
generator.js
49
generator.js
@@ -48,13 +48,18 @@ window.timelineGenerator = {
|
|||||||
|
|
||||||
// Enhanced styling when using external template
|
// Enhanced styling when using external template
|
||||||
if (template && template.includes('Enhanced')) {
|
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
|
// More prominent month indicators for external template
|
||||||
monthGraphics += `<line x1="${x}" y1="${gridTop}" x2="${x}" y2="${gridBottom}" stroke="#4a9b6b" stroke-width="2" opacity="0.6" />`;
|
monthGraphics += `<line x1="${x}" y1="${gridTop}" x2="${x}" y2="${gridBottom}" stroke="${secondaryColor}" stroke-width="2" opacity="0.6" />`;
|
||||||
monthGraphics += `<rect x="${x-30}" y="${monthLabelY-20}" width="60" height="25" fill="#2d8659" opacity="0.1" rx="4" />`;
|
monthGraphics += `<rect x="${x-30}" y="${monthLabelY-20}" width="60" height="25" fill="${primaryColor}" opacity="0.1" rx="4" />`;
|
||||||
monthGraphics += `<text x="${x + 4}" y="${monthLabelY}" fill="#2d8659" font-size="13" font-weight="600">${label}</text>`;
|
monthGraphics += `<text x="${x + 4}" y="${monthLabelY}" fill="${primaryColor}" font-size="13" font-weight="600">${label}</text>`;
|
||||||
// Add month separator
|
// Add month separator
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
monthGraphics += `<rect x="${x-1}" y="${gridTop}" width="2" height="${gridBottom-gridTop}" fill="#4a9b6b" opacity="0.3" />`;
|
monthGraphics += `<rect x="${x-1}" y="${gridTop}" width="2" height="${gridBottom-gridTop}" fill="${secondaryColor}" opacity="0.3" />`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Default styling
|
// Default styling
|
||||||
@@ -76,10 +81,15 @@ window.timelineGenerator = {
|
|||||||
|
|
||||||
// Enhanced styling when using external template
|
// Enhanced styling when using external template
|
||||||
if (template && template.includes('Enhanced')) {
|
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
|
// More subtle lane borders for enhanced template
|
||||||
laneBlocks += `<rect x="40" y="${laneY - 24}" width="${left + months.length * monthWidth}" height="${laneHeight}" fill="rgba(255,255,255,0.7)" stroke="#4a9b6b" stroke-width="1" opacity="0.5" rx="8" />`;
|
laneBlocks += `<rect x="40" y="${laneY - 24}" width="${left + months.length * monthWidth}" height="${laneHeight}" fill="rgba(255,255,255,0.7)" stroke="${secondaryColor}" stroke-width="1" opacity="0.5" rx="8" />`;
|
||||||
// Enhanced lane label
|
// Enhanced lane label
|
||||||
laneBlocks += `<text x="56" y="${laneY - 4}" fill="#2d8659" font-size="14" font-weight="700">${this.escapeXml(laneName)}</text>`;
|
laneBlocks += `<text x="56" y="${laneY - 4}" fill="${primaryColor}" font-size="14" font-weight="700">${this.escapeXml(laneName)}</text>`;
|
||||||
} else {
|
} else {
|
||||||
// Default lane styling
|
// Default lane styling
|
||||||
laneBlocks += `<rect x="40" y="${laneY - 24}" width="${left + months.length * monthWidth}" height="${laneHeight}" fill="#FFFFFF" stroke="#E3E8EF" rx="10" />`;
|
laneBlocks += `<rect x="40" y="${laneY - 24}" width="${left + months.length * monthWidth}" height="${laneHeight}" fill="#FFFFFF" stroke="#E3E8EF" rx="10" />`;
|
||||||
@@ -96,10 +106,16 @@ window.timelineGenerator = {
|
|||||||
|
|
||||||
// Enhanced task item styling for external template
|
// Enhanced task item styling for external template
|
||||||
if (template && template.includes('Enhanced')) {
|
if (template && template.includes('Enhanced')) {
|
||||||
laneBlocks += `<circle cx="${cx}" cy="${cy}" r="6" fill="#2d8659" stroke="#4a9b6b" stroke-width="2" />`;
|
// Determine color scheme based on template content
|
||||||
laneBlocks += `<text x="${cx + 12}" y="${cy + 4}" font-size="12" fill="#2d8659" font-weight="500">
|
const isBlueTheme = template.includes('My Project');
|
||||||
|
const primaryColor = isBlueTheme ? '#3b82f6' : '#2d8659';
|
||||||
|
const secondaryColor = isBlueTheme ? '#60a5fa' : '#4a9b6b';
|
||||||
|
const darkColor = isBlueTheme ? '#1e40af' : '#1e5a3d';
|
||||||
|
|
||||||
|
laneBlocks += `<circle cx="${cx}" cy="${cy}" r="6" fill="${primaryColor}" stroke="${secondaryColor}" stroke-width="2" />`;
|
||||||
|
laneBlocks += `<text x="${cx + 12}" y="${cy + 4}" font-size="12" fill="${primaryColor}" font-weight="500">
|
||||||
<tspan class="item-id">${this.escapeXml(it.id || "")}: </tspan>
|
<tspan class="item-id">${this.escapeXml(it.id || "")}: </tspan>
|
||||||
<tspan class="item-title" fill="#1e5a3d">${this.escapeXml(it.title || "")}</tspan>
|
<tspan class="item-title" fill="${darkColor}">${this.escapeXml(it.title || "")}</tspan>
|
||||||
</text>`;
|
</text>`;
|
||||||
} else {
|
} else {
|
||||||
// Default task item styling
|
// Default task item styling
|
||||||
@@ -113,9 +129,22 @@ window.timelineGenerator = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (template && template.includes("{{MONTHS}}") && template.includes("{{LANES}}")) {
|
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("{{MONTHS}}", monthGraphics)
|
||||||
.replace("{{LANES}}", laneBlocks);
|
.replace("{{LANES}}", laneBlocks);
|
||||||
|
|
||||||
|
// Add width and height attributes to the SVG element
|
||||||
|
processedTemplate = processedTemplate.replace(
|
||||||
|
/<svg([^>]*?)>/,
|
||||||
|
`<svg$1 width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`
|
||||||
|
);
|
||||||
|
|
||||||
|
return processedTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: embed directly in simple SVG
|
// Fallback: embed directly in simple SVG
|
||||||
|
|||||||
126
index.html
126
index.html
@@ -147,6 +147,82 @@
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.6;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script src="generator.js"></script>
|
<script src="generator.js"></script>
|
||||||
<script src="engine.js"></script>
|
<script src="engine.js"></script>
|
||||||
@@ -155,7 +231,8 @@
|
|||||||
|
|
||||||
<h1 id="projectName" style="color:#495057; margin-bottom:8px;">Timeline Generator</h1>
|
<h1 id="projectName" style="color:#495057; margin-bottom:8px;">Timeline Generator</h1>
|
||||||
<p id="projectSubtitle" style="color:#5C6B7A; margin-top:0; margin-bottom:16px;">
|
<p id="projectSubtitle" style="color:#5C6B7A; margin-top:0; margin-bottom:16px;">
|
||||||
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.<br>
|
||||||
|
<small style="color:#6c757d;">💡 Zoom: Verwende die Zoom-Buttons oder Strg+Scrollrad für große Timelines.</small>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Integrated File Management -->
|
<!-- Integrated File Management -->
|
||||||
@@ -178,14 +255,14 @@
|
|||||||
|
|
||||||
<div class="file-item">
|
<div class="file-item">
|
||||||
<div class="file-header">
|
<div class="file-header">
|
||||||
<span class="file-label">CSV Data</span>
|
<span class="file-label">SVG Template</span>
|
||||||
<label class="upload-btn">
|
<label class="upload-btn">
|
||||||
<input type="file" id="csvInput" accept=".csv" style="display:none;" />
|
<input type="file" id="svgInput" accept=".svg" style="display:none;" />
|
||||||
📊 Load
|
🖼️ Load
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-status">
|
<div class="file-status">
|
||||||
<span id="csvFile" class="file-name">Not loaded</span>
|
<span id="svgFile" class="file-name">Not loaded</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -204,14 +281,14 @@
|
|||||||
|
|
||||||
<div class="file-item">
|
<div class="file-item">
|
||||||
<div class="file-header">
|
<div class="file-header">
|
||||||
<span class="file-label">SVG Template</span>
|
<span class="file-label">CSV Data</span>
|
||||||
<label class="upload-btn">
|
<label class="upload-btn">
|
||||||
<input type="file" id="svgInput" accept=".svg" style="display:none;" />
|
<input type="file" id="csvInput" accept=".csv" style="display:none;" />
|
||||||
🖼️ Load
|
📊 Load
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-status">
|
<div class="file-status">
|
||||||
<span id="svgFile" class="file-name">Not loaded</span>
|
<span id="csvFile" class="file-name">Not loaded</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -227,13 +304,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="viewer" style="border:1px solid #ccd3db; background:white; padding:12px; border-radius:8px; overflow-x:auto; min-height:200px;">
|
<!-- SVG Viewer with zoom and scroll capabilities -->
|
||||||
<div style="text-align:center; padding:40px 20px; color:#6c757d;">
|
<div id="viewerContainer">
|
||||||
<div style="font-size:48px; margin-bottom:16px;">📊</div>
|
<!-- Zoom controls -->
|
||||||
<h4 style="margin:0 0 8px 0; color:#495057;">Keine Timeline verfügbar</h4>
|
<div id="zoomControls">
|
||||||
<p style="margin:0; font-size:14px;">
|
<button id="zoomIn">🔍+</button>
|
||||||
Lade eine <strong>Projektkonfiguration</strong> oder <strong>CSV-Datei</strong> um zu beginnen.
|
<button id="zoomOut">🔍-</button>
|
||||||
</p>
|
<button id="zoomReset">100%</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scrollable viewer -->
|
||||||
|
<div id="viewer">
|
||||||
|
<div style="text-align:center; padding:40px 20px; color:#6c757d;">
|
||||||
|
<div style="font-size:48px; margin-bottom:16px;">📊</div>
|
||||||
|
<h4 style="margin:0 0 8px 0; color:#495057;">Keine Timeline verfügbar</h4>
|
||||||
|
<p style="margin:0; font-size:14px;">
|
||||||
|
Lade eine <strong>Projektkonfiguration</strong> oder <strong>CSV-Datei</strong> um zu beginnen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -245,6 +333,12 @@
|
|||||||
console.log("Event handlers set up");
|
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
|
// Ensure engines are loaded before auto-loading
|
||||||
function tryAutoLoad() {
|
function tryAutoLoad() {
|
||||||
if (window.timelineEngine && window.timelineGenerator) {
|
if (window.timelineEngine && window.timelineGenerator) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"fieldMapping": {
|
"fieldMapping": {
|
||||||
"id": "Schlüssel",
|
"id": "Schlüssel",
|
||||||
"title": "Zusammenfassung",
|
"title": "Zusammenfassung",
|
||||||
"lane": "Übergeordnet",
|
"lane": "Status",
|
||||||
"due": [
|
"due": [
|
||||||
"Abgeleitetes Fälligkeitsdatum",
|
"Abgeleitetes Fälligkeitsdatum",
|
||||||
"Fälligkeitsdatum"
|
"Fälligkeitsdatum"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,50 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg">
|
<svg xmlns="http://www.w3.org/2000/svg" style="background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);">
|
||||||
<rect width="100%" height="100%" fill="#FFFFFF"/>
|
<defs>
|
||||||
{{MONTHS}}
|
<!-- Enhanced month indicator styling -->
|
||||||
{{LANES}}
|
<linearGradient id="monthHeaderGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#3b82f6;stop-opacity:0.15"/>
|
||||||
|
<stop offset="100%" style="stop-color:#60a5fa;stop-opacity:0.08"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Drop shadow for month labels -->
|
||||||
|
<filter id="textShadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feDropShadow dx="1" dy="1" stdDeviation="1.5" flood-color="#3b82f6" flood-opacity="0.4"/>
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
<!-- Subtle background grid pattern -->
|
||||||
|
<pattern id="bgGrid" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||||
|
<rect width="40" height="40" fill="transparent"/>
|
||||||
|
<circle cx="20" cy="20" r="1" fill="#60a5fa" opacity="0.1"/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background with subtle pattern -->
|
||||||
|
<rect width="100%" height="100%" fill="url(#bgGrid)"/>
|
||||||
|
|
||||||
|
<!-- Enhanced month header background -->
|
||||||
|
<rect x="0" y="0" width="100%" height="130" fill="url(#monthHeaderGrad)" stroke="#3b82f6" stroke-width="1" opacity="0.6"/>
|
||||||
|
|
||||||
|
<!-- Title area with visual indicator -->
|
||||||
|
<rect x="10" y="10" width="300" height="60" fill="#ffffff" stroke="#3b82f6" stroke-width="2" rx="8" opacity="0.9"/>
|
||||||
|
<text x="20" y="35" fill="#3b82f6" font-size="16" font-weight="bold" filter="url(#textShadow)">
|
||||||
|
📅 My Project Timeline
|
||||||
|
</text>
|
||||||
|
<text x="20" y="55" fill="#60a5fa" font-size="11" font-weight="500">
|
||||||
|
Enhanced blue styling with prominent months ✨
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- Month indicators with enhanced styling -->
|
||||||
|
<g class="enhanced-months" transform="translate(0,0)">
|
||||||
|
<rect x="0" y="75" width="100%" height="55" fill="rgba(59, 130, 246, 0.05)" stroke="#60a5fa" stroke-width="1"/>
|
||||||
|
{{MONTHS}}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Lane content -->
|
||||||
|
<g class="enhanced-lanes">
|
||||||
|
{{LANES}}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Decorative border -->
|
||||||
|
<rect x="1" y="1" width="calc(100% - 2)" height="calc(100% - 2)"
|
||||||
|
fill="none" stroke="#3b82f6" stroke-width="2" rx="4" opacity="0.7"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 125 B After Width: | Height: | Size: 2.1 KiB |
Reference in New Issue
Block a user