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:
2025-11-19 00:46:58 +01:00
parent 02bcf69096
commit bc756fa0cd
6 changed files with 524 additions and 33 deletions

262
engine.js
View File

@@ -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();
});
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -10,7 +10,7 @@
"fieldMapping": {
"id": "Schlüssel",
"title": "Zusammenfassung",
"lane": "Übergeordnet",
"lane": "Status",
"due": [
"Abgeleitetes Fälligkeitsdatum",
"Fälligkeitsdatum"

View File

@@ -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;
}

View File

@@ -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