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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
49
generator.js
49
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 += `<line x1="${x}" y1="${gridTop}" x2="${x}" y2="${gridBottom}" stroke="#4a9b6b" 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 += `<text x="${x + 4}" y="${monthLabelY}" fill="#2d8659" font-size="13" font-weight="600">${label}</text>`;
|
||||
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="${primaryColor}" opacity="0.1" rx="4" />`;
|
||||
monthGraphics += `<text x="${x + 4}" y="${monthLabelY}" fill="${primaryColor}" font-size="13" font-weight="600">${label}</text>`;
|
||||
// Add month separator
|
||||
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 {
|
||||
// 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 += `<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
|
||||
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 {
|
||||
// Default lane styling
|
||||
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
|
||||
if (template && template.includes('Enhanced')) {
|
||||
laneBlocks += `<circle cx="${cx}" cy="${cy}" r="6" fill="#2d8659" stroke="#4a9b6b" stroke-width="2" />`;
|
||||
laneBlocks += `<text x="${cx + 12}" y="${cy + 4}" font-size="12" fill="#2d8659" font-weight="500">
|
||||
// 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 += `<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-title" fill="#1e5a3d">${this.escapeXml(it.title || "")}</tspan>
|
||||
<tspan class="item-title" fill="${darkColor}">${this.escapeXml(it.title || "")}</tspan>
|
||||
</text>`;
|
||||
} 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(
|
||||
/<svg([^>]*?)>/,
|
||||
`<svg$1 width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`
|
||||
);
|
||||
|
||||
return processedTemplate;
|
||||
}
|
||||
|
||||
// Fallback: embed directly in simple SVG
|
||||
|
||||
126
index.html
126
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;
|
||||
}
|
||||
</style>
|
||||
<script src="generator.js"></script>
|
||||
<script src="engine.js"></script>
|
||||
@@ -155,7 +231,8 @@
|
||||
|
||||
<h1 id="projectName" style="color:#495057; margin-bottom:8px;">Timeline Generator</h1>
|
||||
<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>
|
||||
|
||||
<!-- Integrated File Management -->
|
||||
@@ -178,14 +255,14 @@
|
||||
|
||||
<div class="file-item">
|
||||
<div class="file-header">
|
||||
<span class="file-label">CSV Data</span>
|
||||
<span class="file-label">SVG Template</span>
|
||||
<label class="upload-btn">
|
||||
<input type="file" id="csvInput" accept=".csv" style="display:none;" />
|
||||
📊 Load
|
||||
<input type="file" id="svgInput" accept=".svg" style="display:none;" />
|
||||
🖼️ Load
|
||||
</label>
|
||||
</div>
|
||||
<div class="file-status">
|
||||
<span id="csvFile" class="file-name">Not loaded</span>
|
||||
<span id="svgFile" class="file-name">Not loaded</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -204,14 +281,14 @@
|
||||
|
||||
<div class="file-item">
|
||||
<div class="file-header">
|
||||
<span class="file-label">SVG Template</span>
|
||||
<span class="file-label">CSV Data</span>
|
||||
<label class="upload-btn">
|
||||
<input type="file" id="svgInput" accept=".svg" style="display:none;" />
|
||||
🖼️ Load
|
||||
<input type="file" id="csvInput" accept=".csv" style="display:none;" />
|
||||
📊 Load
|
||||
</label>
|
||||
</div>
|
||||
<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>
|
||||
@@ -227,13 +304,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="viewer" style="border:1px solid #ccd3db; background:white; padding:12px; border-radius:8px; overflow-x:auto; min-height:200px;">
|
||||
<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>
|
||||
<!-- SVG Viewer with zoom and scroll capabilities -->
|
||||
<div id="viewerContainer">
|
||||
<!-- Zoom controls -->
|
||||
<div id="zoomControls">
|
||||
<button id="zoomIn">🔍+</button>
|
||||
<button id="zoomOut">🔍-</button>
|
||||
<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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"fieldMapping": {
|
||||
"id": "Schlüssel",
|
||||
"title": "Zusammenfassung",
|
||||
"lane": "Übergeordnet",
|
||||
"lane": "Status",
|
||||
"due": [
|
||||
"Abgeleitetes 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">
|
||||
<rect width="100%" height="100%" fill="#FFFFFF"/>
|
||||
{{MONTHS}}
|
||||
{{LANES}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);">
|
||||
<defs>
|
||||
<!-- Enhanced month indicator styling -->
|
||||
<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>
|
||||
|
||||
|
Before Width: | Height: | Size: 125 B After Width: | Height: | Size: 2.1 KiB |
Reference in New Issue
Block a user