generated from coulomb/repo-seed
- 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>
170 lines
7.3 KiB
JavaScript
170 lines
7.3 KiB
JavaScript
window.timelineGenerator = {
|
|
generate(items, cfg, template) {
|
|
const monthsRange = (cfg.settings && cfg.settings.timelineMonths) || 18;
|
|
|
|
// Determine time window from earliest due date
|
|
const sorted = [...items].sort((a, b) => a.due - b.due);
|
|
const minDue = sorted[0].due;
|
|
const start = new Date(minDue.getFullYear(), minDue.getMonth(), 1);
|
|
const end = new Date(start);
|
|
end.setMonth(end.getMonth() + monthsRange);
|
|
|
|
// Build months array
|
|
const months = [];
|
|
const cursor = new Date(start);
|
|
while (cursor <= end) {
|
|
months.push(new Date(cursor));
|
|
cursor.setMonth(cursor.getMonth() + 1);
|
|
}
|
|
|
|
// Group items by lane
|
|
const laneMap = new Map();
|
|
for (const it of items) {
|
|
const laneName = it.lane || "Ohne Epic";
|
|
if (!laneMap.has(laneName)) laneMap.set(laneName, []);
|
|
laneMap.get(laneName).push(it);
|
|
}
|
|
|
|
const laneNames = Array.from(laneMap.keys()).sort((a, b) =>
|
|
a.localeCompare(b, "de")
|
|
);
|
|
|
|
// Layout constants
|
|
const left = 220;
|
|
const top = 140;
|
|
const monthWidth = 120;
|
|
const laneHeight = 80;
|
|
const laneGap = 16;
|
|
|
|
// Month grid (labels + vertical lines)
|
|
let monthGraphics = "";
|
|
const monthLabelY = 90;
|
|
const gridTop = top - 20;
|
|
const gridBottom = top + laneNames.length * (laneHeight + laneGap) + 40;
|
|
|
|
months.forEach((m, i) => {
|
|
const x = left + i * monthWidth;
|
|
const label = m.toLocaleString("de-DE", { month: "short", year: "2-digit" });
|
|
|
|
// 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="${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="${secondaryColor}" opacity="0.3" />`;
|
|
}
|
|
} else {
|
|
// Default styling
|
|
monthGraphics += `<line x1="${x}" y1="${gridTop}" x2="${x}" y2="${gridBottom}" stroke="#E3E8EF" />`;
|
|
monthGraphics += `<text x="${x + 4}" y="${monthLabelY}" fill="#5C6B7A" font-size="12">${label}</text>`;
|
|
}
|
|
});
|
|
|
|
// Helper to compute month index
|
|
function monthIndexForDate(d) {
|
|
return (d.getFullYear() - start.getFullYear()) * 12
|
|
+ (d.getMonth() - start.getMonth());
|
|
}
|
|
|
|
// Lane blocks
|
|
let laneBlocks = "";
|
|
laneNames.forEach((laneName, laneIdx) => {
|
|
const laneY = top + laneIdx * (laneHeight + laneGap);
|
|
|
|
// 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="${secondaryColor}" stroke-width="1" opacity="0.5" rx="8" />`;
|
|
// Enhanced lane label
|
|
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" />`;
|
|
// Default lane label
|
|
laneBlocks += `<text x="56" y="${laneY - 4}" fill="#0B1F3B" font-size="14" font-weight="600">${this.escapeXml(laneName)}</text>`;
|
|
}
|
|
|
|
const laneItems = laneMap.get(laneName).sort((a, b) => a.due - b.due);
|
|
laneItems.forEach((it, idx) => {
|
|
const mi = monthIndexForDate(it.due);
|
|
const clampedMi = Math.max(0, Math.min(months.length - 1, mi));
|
|
const cx = left + clampedMi * monthWidth + monthWidth * 0.5;
|
|
const cy = laneY + 10 + idx * 18;
|
|
|
|
// Enhanced task item styling for 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';
|
|
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="${darkColor}">${this.escapeXml(it.title || "")}</tspan>
|
|
</text>`;
|
|
} else {
|
|
// Default task item styling
|
|
laneBlocks += `<circle cx="${cx}" cy="${cy}" r="5" fill="#0A4D8C" />`;
|
|
laneBlocks += `<text x="${cx + 10}" y="${cy + 4}" font-size="12" fill="#0B1F3B">
|
|
<tspan class="item-id">${this.escapeXml(it.id || "")}: </tspan>
|
|
<tspan class="item-title">${this.escapeXml(it.title || "")}</tspan>
|
|
</text>`;
|
|
}
|
|
});
|
|
});
|
|
|
|
if (template && template.includes("{{MONTHS}}") && template.includes("{{LANES}}")) {
|
|
// 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
|
|
const height = top + laneNames.length * (laneHeight + laneGap) + 80;
|
|
const width = left + months.length * monthWidth + 100;
|
|
|
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
<rect width="100%" height="100%" fill="#FFFFFF" />
|
|
${monthGraphics}
|
|
${laneBlocks}
|
|
</svg>`;
|
|
},
|
|
|
|
escapeXml(str) {
|
|
return String(str)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
};
|