Compare commits
12 Commits
b5c7c613de
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5bec61740b | |||
| a5811040e7 | |||
| 710794de88 | |||
| 4576d066b3 | |||
| cefbf96a82 | |||
| 39037587ba | |||
| 4e92665358 | |||
| d5c1ca239d | |||
| b5e4550efd | |||
| 2bab447fa8 | |||
| dd3ba4df58 | |||
| c22a47f1ea |
4
.gitignore
vendored
@@ -180,3 +180,7 @@ node_modules/
|
||||
|
||||
# npm package lock file
|
||||
package-lock.json
|
||||
|
||||
# Distribution archives
|
||||
timeline-svg-dist.zip
|
||||
timeline-svg-dist.tar.gz
|
||||
|
||||
241
CLAUDE.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Overview
|
||||
|
||||
TimelineSvg is a browser-based system for generating multi-lane timelines as SVG graphics from CSV data. It processes everything client-side using vanilla JavaScript, with no build pipeline or backend required. The system uses a template-based architecture where SVG templates define the visual appearance and data is mapped through a project configuration file.
|
||||
|
||||
**Troubleshooting**: If you encounter issues during development, check `TROUBLESHOOTING.md` first. It contains solutions for common problems including npm installation failures, CORS errors, template validation issues, and more.
|
||||
|
||||
## Commands
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
make install # Install development dependencies
|
||||
```
|
||||
|
||||
**Important - Node.js v24 on WSL**: If you encounter `ERR_SSL_CIPHER_OPERATION_FAILED` errors during `npm install`, this is a known issue with Node.js v24 and OpenSSL 3.x in WSL environments. The Makefile automatically applies the workaround using `NODE_OPTIONS="--openssl-legacy-provider"`. If installing manually without make:
|
||||
```bash
|
||||
export NODE_OPTIONS="--openssl-legacy-provider" && npm install
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
npm test # Run all tests once
|
||||
npm run test:watch # Run tests in watch mode
|
||||
npm run test:coverage # Generate coverage report
|
||||
npm run test:ui # Run Vitest UI
|
||||
```
|
||||
|
||||
The project uses Vitest with jsdom for testing. Tests are located in `/test/` and cover the generator, engine, and integration scenarios.
|
||||
|
||||
### Development
|
||||
```bash
|
||||
make serve # Start local development server (auto-detects Python/npx)
|
||||
# or manually:
|
||||
python3 -m http.server 8000
|
||||
|
||||
# Then open http://localhost:8000
|
||||
```
|
||||
|
||||
The application must be served via HTTP (not opened as file://) due to CORS restrictions when loading CSV, CSS, and SVG files.
|
||||
|
||||
### Distribution
|
||||
```bash
|
||||
make dist # Build distribution package in dist/
|
||||
make dist-zip # Build distribution and create archive
|
||||
```
|
||||
|
||||
The distribution package includes:
|
||||
- All runtime files (HTML, JS, CSS)
|
||||
- Example projects (example/, example-1/, my-project/)
|
||||
- Documentation (README.md, TEMPLATE_V2_GUIDE.md, TROUBLESHOOTING.md)
|
||||
- Startup scripts for Windows (start-server.bat) and Linux/Mac (start-server.sh)
|
||||
|
||||
Use this to deploy the application to Windows or other environments. The distribution is self-contained and requires only a web browser and Python (for the local server).
|
||||
|
||||
**For Windows deployment from WSL**: See `WINDOWS_USAGE.md` for detailed instructions on building the distribution in WSL, transferring to Windows, and running the application on Windows OS.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
**1. engine.js** - Application controller and data loading
|
||||
- Manages project configuration loading (auto-loads from `binect/`, `my-project/`, or `example/` folders)
|
||||
- Handles CSV parsing using PapaParse library
|
||||
- Manages file uploads and overrides
|
||||
- Provides debug information panel
|
||||
- Controls view modes (internal vs external)
|
||||
- Implements zoom functionality for SVG viewer
|
||||
|
||||
**2. generator.js** - Timeline rendering engine
|
||||
- Implements template-v2 architecture
|
||||
- Extracts template elements from `<defs>` section: `month-template`, `lane-template`, `item-template`
|
||||
- Replaces placeholders with calculated values and CSV data
|
||||
- Handles layout calculations (positioning months, lanes, items)
|
||||
- Generates final SVG by cloning templates and replacing `{{MONTHS}}` and `{{LANES}}` macros
|
||||
|
||||
**3. index.html** - UI and browser entry point
|
||||
- Loads PapaParse from CDN for CSV parsing
|
||||
- Provides file upload controls
|
||||
- Contains viewer with zoom controls
|
||||
- Shows debug information panel with field mappings and data preview
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. **Project Configuration** (`project.json`) defines:
|
||||
- Data source (CSV file path)
|
||||
- Stylesheet (CSS file path)
|
||||
- SVG template (template-v2.svg file path)
|
||||
- Field mappings (CSV columns → timeline fields)
|
||||
- Optional placeholder mappings (override default `ITEM_*` convention)
|
||||
- Settings (timeline duration, etc.)
|
||||
|
||||
2. **CSV Data** is parsed and mapped to timeline items:
|
||||
- Required fields: `id`, `title`, `due`, `lane`
|
||||
- Date parsing supports multiple formats: `YYYY-MM-DD`, `YYYY/MM/DD`, `DD.MM.YYYY`
|
||||
- Items without valid title or due date are filtered out
|
||||
|
||||
3. **Template System** (template-v2 architecture):
|
||||
- Templates are standard SVG files editable in Inkscape, Illustrator, or Figma
|
||||
- Three required template elements in `<defs>` section:
|
||||
- `<g id="month-template">` - Defines month column rendering
|
||||
- `<g id="lane-template">` - Defines lane/epic row rendering
|
||||
- `<g id="item-template">` - Defines individual task rendering
|
||||
- Templates contain placeholders like `{{MONTH_X}}`, `{{LANE_NAME}}`, `{{ITEM_TITLE}}`
|
||||
- Generator extracts templates, replaces placeholders, and injects into main SVG
|
||||
|
||||
4. **Placeholder System**:
|
||||
- Layout placeholders: `{{MONTH_X}}`, `{{LANE_Y}}`, `{{ITEM_X}}`, etc. (calculated by generator)
|
||||
- Data placeholders: Automatically created from CSV columns using `ITEM_{PROPERTY}` convention
|
||||
- Custom mappings: Use `placeholderMapping` in project.json to override default names (e.g., `TASK_ID` instead of `ITEM_ID`)
|
||||
- All CSV properties (except `due`) are available as placeholders in templates
|
||||
|
||||
### Key Concepts
|
||||
|
||||
**Template-v2 Architecture**: The current system (migrated from older macro-based approach) uses proper SVG template elements that are cloned and customized. This makes templates more maintainable and editable in visual SVG editors.
|
||||
|
||||
**Field Mapping**: CSV columns don't need to match exact names. The `fieldMapping` object in project.json maps CSV column names to the internal timeline model:
|
||||
```json
|
||||
{
|
||||
"fieldMapping": {
|
||||
"id": "ID", // CSV column "ID" maps to item.id
|
||||
"title": "Title", // CSV column "Title" maps to item.title
|
||||
"lane": "Lane", // CSV column "Lane" maps to item.lane
|
||||
"due": ["Due"] // CSV column "Due" maps to item.due (array for fallbacks)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Placeholder Mapping**: When templates use non-standard placeholder names (common when migrating from other tools), use `placeholderMapping` to override the default `ITEM_*` convention:
|
||||
```json
|
||||
{
|
||||
"placeholderMapping": {
|
||||
"id": "TASK_ID", // Use {{TASK_ID}} instead of {{ITEM_ID}}
|
||||
"title": "TASK_NAME" // Use {{TASK_NAME}} instead of {{ITEM_TITLE}}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Layout Constants** (generator.js:41-45): Control overall positioning
|
||||
- `left: 220` - Left margin
|
||||
- `top: 140` - Top margin
|
||||
- `monthWidth: 120` - Width of each month column
|
||||
- `laneHeight: 80` - Height of each lane/epic row
|
||||
- `laneGap: 16` - Vertical gap between lanes
|
||||
|
||||
**View Modes**:
|
||||
- Internal view: Shows item IDs (for development/review)
|
||||
- External view: Hides item IDs (for presentations/exports)
|
||||
|
||||
**Project Loading Methods**:
|
||||
- Auto-load: When served via HTTP, attempts to load from binect/, my-project/, or example/ folders
|
||||
- Folder picker: User selects entire project folder - all files load automatically (uses webkitdirectory attribute)
|
||||
- Individual files: User manually uploads project.json, then CSV/SVG/CSS separately
|
||||
|
||||
The folder picker was added to solve the browser security limitation where uploading a single project.json doesn't allow automatic access to other files in the same directory.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
/
|
||||
├── index.html # Main application entry point
|
||||
├── engine.js # Application controller & data loading
|
||||
├── generator.js # SVG generation & template processing
|
||||
├── vitest.config.js # Test configuration
|
||||
├── test/
|
||||
│ ├── setup.js # Test environment setup
|
||||
│ ├── generator.test.js # Generator unit tests
|
||||
│ ├── engine.test.js # Engine unit tests
|
||||
│ └── integration.test.js # End-to-end tests
|
||||
├── example/ # Example project
|
||||
│ ├── project.json # Project configuration
|
||||
│ ├── sample.csv # Sample data
|
||||
│ ├── style.css # Custom styling
|
||||
│ └── template-v2.svg # SVG template
|
||||
└── TEMPLATE_V2_GUIDE.md # Comprehensive template documentation
|
||||
```
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
When writing tests:
|
||||
- Use Vitest's `describe()` and `it()` structure
|
||||
- Mock browser APIs (localStorage, fetch) using jsdom
|
||||
- Test template validation separately from rendering
|
||||
- Test date parsing for all supported formats
|
||||
- Test field mapping edge cases (missing fields, null values)
|
||||
- Integration tests should cover full project loading → CSV parsing → SVG generation flow
|
||||
|
||||
## Important Implementation Details
|
||||
|
||||
**Template Validation** (generator.js:54-70): Always validate templates before attempting to generate SVG. Check for all three required template IDs.
|
||||
|
||||
**Template Extraction** (generator.js:73-82): Use regex to extract template elements from `<defs>`. Throws error if extraction fails (indicates malformed template).
|
||||
|
||||
**XML Escaping** (generator.js:216-223): Always escape user data (titles, lane names) when injecting into SVG to prevent malformed XML.
|
||||
|
||||
**Date Parsing** (engine.js:385-408): Supports multiple formats with fallback to `new Date()`. Returns null for invalid dates, which causes items to be filtered out.
|
||||
|
||||
**Project Base Path Resolution** (engine.js:148-157): Relative paths in project.json are resolved relative to the project folder (e.g., `example/`). This enables projects to reference their own resources.
|
||||
|
||||
**Debug Information Panel**: Shows field mappings, template placeholders, and CSV preview to help diagnose configuration issues. Automatically displayed when project loads.
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
When adding new placeholder types:
|
||||
1. Add to placeholder documentation in TEMPLATE_V2_GUIDE.md
|
||||
2. Update generator.js placeholder replacement logic if needed
|
||||
3. Update example templates to demonstrate usage
|
||||
4. Add tests for the new placeholder type
|
||||
|
||||
When modifying layout:
|
||||
1. Update layout constants in generator.js:41-45
|
||||
2. Update placeholder value calculations in generateFromTemplates()
|
||||
3. Update TEMPLATE_V2_GUIDE.md with new dimensions
|
||||
4. Regenerate example SVGs to verify
|
||||
|
||||
When adding CSV field support:
|
||||
1. Update fieldMapping documentation in README.md
|
||||
2. Update example project.json files
|
||||
3. Add to processCsv() in engine.js
|
||||
4. Update generator to expose as placeholder
|
||||
5. Add tests for field mapping
|
||||
|
||||
## Template Development
|
||||
|
||||
See TEMPLATE_V2_GUIDE.md for comprehensive template creation and customization documentation. Key points:
|
||||
|
||||
- Templates must have all three required `<g id="*-template">` elements in `<defs>`
|
||||
- Use `style="display:none"` on template elements to hide them
|
||||
- All placeholders use `{{UPPERCASE_NAME}}` format
|
||||
- Templates are standard SVG - edit in any SVG editor
|
||||
- Test templates by loading them in the UI and checking debug panel
|
||||
|
||||
## Migration Notes
|
||||
|
||||
The codebase uses template-v2 architecture. Legacy template-v1 (macro-based) is deprecated. When working with old projects:
|
||||
- Convert macro-based templates to template-v2 format
|
||||
- Move template definitions into `<defs>` section
|
||||
- Update placeholder names to match conventions
|
||||
- Use `placeholderMapping` if preserving legacy placeholder names
|
||||
147
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: help install test test-watch test-coverage test-ui clean serve lint format deps-check deps-update
|
||||
.PHONY: help install test test-watch test-coverage test-ui clean serve lint format deps-check deps-update dist dist-zip
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@@ -25,11 +25,16 @@ help:
|
||||
@echo " deps-update Update dependencies"
|
||||
@echo " deps-audit Security audit of dependencies"
|
||||
@echo ""
|
||||
@echo "Distribution:"
|
||||
@echo " dist Build distribution package in dist/"
|
||||
@echo " dist-zip Build distribution and create ZIP archive"
|
||||
@echo ""
|
||||
|
||||
# Development setup
|
||||
install:
|
||||
@echo "Installing development dependencies..."
|
||||
npm install
|
||||
@# Workaround for Node.js v24 + OpenSSL 3.x in WSL (ERR_SSL_CIPHER_OPERATION_FAILED)
|
||||
@export NODE_OPTIONS="--openssl-legacy-provider" && npm install
|
||||
@echo "✅ Dependencies installed"
|
||||
|
||||
# Testing targets
|
||||
@@ -136,4 +141,140 @@ setup: clean install test
|
||||
@echo " • Run 'make test' to run tests"
|
||||
@echo " • Run 'make test-watch' for TDD"
|
||||
@echo " • Run 'make serve' to start development server"
|
||||
@echo " • Open index.html in browser to use the app"
|
||||
@echo " • Open index.html in browser to use the app"
|
||||
|
||||
# Distribution build for deployment/Windows
|
||||
dist:
|
||||
@echo "Building distribution package..."
|
||||
@# Create dist directory
|
||||
@rm -rf dist
|
||||
@mkdir -p dist
|
||||
@echo "📦 Created dist/ directory"
|
||||
|
||||
@# Copy core application files
|
||||
@cp index.html engine.js generator.js generator-dom.js file-editor.js dist/
|
||||
@echo "✅ Copied core files (index.html, engine.js, generator.js, generator-dom.js, file-editor.js)"
|
||||
|
||||
@# Copy project directories
|
||||
@cp -r example example-1 example-proto my-project dist/
|
||||
@echo "✅ Copied project directories (example, example-1, example-proto, my-project)"
|
||||
|
||||
@# Copy documentation
|
||||
@cp README.md LICENSE dist/
|
||||
@[ -f TEMPLATE_V2_GUIDE.md ] && cp TEMPLATE_V2_GUIDE.md dist/ || true
|
||||
@[ -f TROUBLESHOOTING.md ] && cp TROUBLESHOOTING.md dist/ || true
|
||||
@echo "✅ Copied documentation"
|
||||
|
||||
@# Create Windows batch file for serving
|
||||
@echo '@echo off' > dist/start-server.bat
|
||||
@echo 'echo Starting Timeline SVG Generator...' >> dist/start-server.bat
|
||||
@echo 'echo.' >> dist/start-server.bat
|
||||
@echo 'echo Opening browser at http://localhost:8000' >> dist/start-server.bat
|
||||
@echo 'echo Press Ctrl+C to stop the server' >> dist/start-server.bat
|
||||
@echo 'echo.' >> dist/start-server.bat
|
||||
@echo 'start http://localhost:8000' >> dist/start-server.bat
|
||||
@echo 'python -m http.server 8000 2^>nul || python3 -m http.server 8000 2^>nul || (echo Python not found. Please install Python or open index.html in a browser. ^& pause)' >> dist/start-server.bat
|
||||
@echo "✅ Created start-server.bat for Windows"
|
||||
|
||||
@# Create Linux/Mac shell script for serving
|
||||
@echo '#!/bin/bash' > dist/start-server.sh
|
||||
@echo 'echo "Starting Timeline SVG Generator..."' >> dist/start-server.sh
|
||||
@echo 'echo ""' >> dist/start-server.sh
|
||||
@echo 'echo "Opening browser at http://localhost:8000"' >> dist/start-server.sh
|
||||
@echo 'echo "Press Ctrl+C to stop the server"' >> dist/start-server.sh
|
||||
@echo 'echo ""' >> dist/start-server.sh
|
||||
@echo 'if command -v xdg-open >/dev/null 2>&1; then' >> dist/start-server.sh
|
||||
@echo ' xdg-open http://localhost:8000 &' >> dist/start-server.sh
|
||||
@echo 'elif command -v open >/dev/null 2>&1; then' >> dist/start-server.sh
|
||||
@echo ' open http://localhost:8000 &' >> dist/start-server.sh
|
||||
@echo 'fi' >> dist/start-server.sh
|
||||
@echo 'python3 -m http.server 8000 2>/dev/null || python -m SimpleHTTPServer 8000 2>/dev/null || { echo "Python not found. Please install Python or open index.html in a browser."; read -p "Press Enter to exit..."; }' >> dist/start-server.sh
|
||||
@chmod +x dist/start-server.sh
|
||||
@echo "✅ Created start-server.sh for Linux/Mac"
|
||||
|
||||
@# Create distribution README
|
||||
@echo "# Timeline SVG Generator - Distribution Package" > dist/DIST_README.md
|
||||
@echo "" >> dist/DIST_README.md
|
||||
@echo "This is a standalone distribution of Timeline SVG Generator." >> dist/DIST_README.md
|
||||
@echo "" >> dist/DIST_README.md
|
||||
@echo "## Quick Start" >> dist/DIST_README.md
|
||||
@echo "" >> dist/DIST_README.md
|
||||
@echo "### Windows" >> dist/DIST_README.md
|
||||
@echo "" >> dist/DIST_README.md
|
||||
@echo "1. Double-click \`start-server.bat\`" >> dist/DIST_README.md
|
||||
@echo "2. Your browser will open automatically at http://localhost:8000" >> dist/DIST_README.md
|
||||
@echo "3. The application will load the example project automatically" >> dist/DIST_README.md
|
||||
@echo "4. To load your own project, click **📂 Load Project Folder** and select your project directory" >> dist/DIST_README.md
|
||||
@echo "" >> dist/DIST_README.md
|
||||
@echo "### Linux / Mac" >> dist/DIST_README.md
|
||||
@echo "" >> dist/DIST_README.md
|
||||
@echo "1. Run \`./start-server.sh\` (or \`bash start-server.sh\`)" >> dist/DIST_README.md
|
||||
@echo "2. Your browser will open automatically at http://localhost:8000" >> dist/DIST_README.md
|
||||
@echo "3. The application will load the example project automatically" >> dist/DIST_README.md
|
||||
@echo "4. To load your own project, click **📂 Load Project Folder** and select your project directory" >> dist/DIST_README.md
|
||||
@echo "" >> dist/DIST_README.md
|
||||
@echo "### Alternative: Open Directly" >> dist/DIST_README.md
|
||||
@echo "" >> dist/DIST_README.md
|
||||
@echo "**Note:** Due to browser security restrictions (CORS), opening index.html directly may prevent loading of project files. Using a local server is recommended." >> dist/DIST_README.md
|
||||
@echo "" >> dist/DIST_README.md
|
||||
@echo "## Project Structure" >> dist/DIST_README.md
|
||||
@echo "" >> dist/DIST_README.md
|
||||
@echo "- \`index.html\` - Main application" >> dist/DIST_README.md
|
||||
@echo "- \`engine.js\` - Application logic" >> dist/DIST_README.md
|
||||
@echo "- \`generator.js\` - SVG generation engine" >> dist/DIST_README.md
|
||||
@echo "- \`example/\` - Simple example project" >> dist/DIST_README.md
|
||||
@echo "- \`example-1/\` - Enhanced example with styling" >> dist/DIST_README.md
|
||||
@echo "- \`my-project/\` - Template for your own projects" >> dist/DIST_README.md
|
||||
@echo "- \`README.md\` - Full documentation" >> dist/DIST_README.md
|
||||
@echo "- \`TEMPLATE_V2_GUIDE.md\` - Template creation guide" >> dist/DIST_README.md
|
||||
@echo "" >> dist/DIST_README.md
|
||||
@echo "## Creating Your Own Timeline" >> dist/DIST_README.md
|
||||
@echo "" >> dist/DIST_README.md
|
||||
@echo "1. Copy one of the example folders (e.g., \`example/\`)" >> dist/DIST_README.md
|
||||
@echo "2. Rename it to your project name" >> dist/DIST_README.md
|
||||
@echo "3. Edit \`project.json\` to configure your timeline" >> dist/DIST_README.md
|
||||
@echo "4. Replace \`sample.csv\` with your data" >> dist/DIST_README.md
|
||||
@echo "5. Customize \`style.css\` and \`template-v2.svg\` as needed" >> dist/DIST_README.md
|
||||
@echo "6. Click **📂 Load Project Folder** and select your project folder - all files load automatically!" >> dist/DIST_README.md
|
||||
@echo "" >> dist/DIST_README.md
|
||||
@echo "**Tip:** The folder picker loads all files (JSON, CSV, SVG, CSS) in one click. You can also load files individually using the separate upload buttons if needed." >> dist/DIST_README.md
|
||||
@echo "" >> dist/DIST_README.md
|
||||
@echo "See README.md and TEMPLATE_V2_GUIDE.md for detailed instructions." >> dist/DIST_README.md
|
||||
@echo "" >> dist/DIST_README.md
|
||||
@echo "## Troubleshooting" >> dist/DIST_README.md
|
||||
@echo "" >> dist/DIST_README.md
|
||||
@echo "If you encounter issues, see TROUBLESHOOTING.md for solutions to common problems." >> dist/DIST_README.md
|
||||
@echo "✅ Created DIST_README.md"
|
||||
|
||||
@echo ""
|
||||
@echo "🎉 Distribution package created in dist/"
|
||||
@echo ""
|
||||
@echo "Contents:"
|
||||
@echo " • Core application files (HTML, JS)"
|
||||
@echo " • Example projects (example, example-1, my-project)"
|
||||
@echo " • Documentation (README.md, TEMPLATE_V2_GUIDE.md, etc.)"
|
||||
@echo " • Server startup scripts (start-server.bat, start-server.sh)"
|
||||
@echo ""
|
||||
@echo "To use on Windows:"
|
||||
@echo " 1. Copy the dist/ folder to your Windows machine"
|
||||
@echo " 2. Double-click start-server.bat to run"
|
||||
@echo ""
|
||||
@echo "Or create a ZIP archive with: make dist-zip"
|
||||
|
||||
# Create ZIP archive of distribution
|
||||
dist-zip: dist
|
||||
@echo "Creating distribution archive..."
|
||||
@if command -v zip >/dev/null 2>&1; then \
|
||||
cd dist && zip -r ../timeline-svg-dist.zip . -x "*.git*" -x "*.DS_Store" && cd ..; \
|
||||
echo "✅ Created timeline-svg-dist.zip"; \
|
||||
echo ""; \
|
||||
echo "📦 Distribution archive ready: timeline-svg-dist.zip"; \
|
||||
echo " Transfer this file to Windows and extract to use the application."; \
|
||||
else \
|
||||
tar --exclude=".git*" --exclude=".DS_Store" -czf timeline-svg-dist.tar.gz -C dist .; \
|
||||
echo "✅ Created timeline-svg-dist.tar.gz (zip not available)"; \
|
||||
echo ""; \
|
||||
echo "📦 Distribution archive ready: timeline-svg-dist.tar.gz"; \
|
||||
echo " Transfer this file to Windows, extract with 7-Zip or similar tool."; \
|
||||
echo " Or install zip: sudo apt-get install zip"; \
|
||||
fi
|
||||
339
PROTOTYPE_TEMPLATES.md
Normal file
@@ -0,0 +1,339 @@
|
||||
# Prototype-Based Templates Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The timeline generator now supports **prototype-based templates** using DOM cloning. This is the recommended approach for creating custom timeline designs because:
|
||||
|
||||
- ✅ **100% Valid SVG** - Edit directly in Inkscape with full visual control
|
||||
- ✅ **WYSIWYG** - See exactly how your timeline will look
|
||||
- ✅ **No String Placeholders** - No need for `{{PLACEHOLDER}}` syntax
|
||||
- ✅ **Better Performance** - Uses native DOM manipulation
|
||||
- ✅ **Easier Maintenance** - Visual editing instead of code
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Create Prototypes in Inkscape**
|
||||
- Design visual elements (month headers, lanes, items) with specific IDs
|
||||
- Style them exactly as you want them to appear
|
||||
- The generator will clone these elements for each data item
|
||||
|
||||
2. **Generator Clones Prototypes**
|
||||
- Each prototype is deep-cloned using DOM
|
||||
- Positioned using SVG transforms
|
||||
- Text content updated from your data
|
||||
- Appended to target containers
|
||||
|
||||
3. **Prototypes Hidden**
|
||||
- Original prototypes are hidden (`display:none`) after cloning
|
||||
- Only the cloned, data-filled elements remain visible
|
||||
|
||||
## Template Structure
|
||||
|
||||
### Required Prototype Elements
|
||||
|
||||
Your SVG must contain these three prototype groups:
|
||||
|
||||
```xml
|
||||
<g id="month-proto">
|
||||
<!-- Month header design -->
|
||||
<line x1="0" y1="120" x2="0" y2="600" class="month-grid" />
|
||||
<text x="4" y="90" class="month-label">Jan 25</text>
|
||||
</g>
|
||||
|
||||
<g id="lane-proto">
|
||||
<!-- Lane/swimlane design -->
|
||||
<rect x="40" y="-24" width="1000" height="80" class="lane-bg" />
|
||||
<text x="56" y="-4" class="lane-label">Lane Name</text>
|
||||
</g>
|
||||
|
||||
<g id="item-proto">
|
||||
<!-- Task/item marker design -->
|
||||
<circle cx="0" cy="0" r="5" class="item-marker" />
|
||||
<text x="12" y="4" class="item-title">Task Title</text>
|
||||
<text x="12" y="-8" class="item-id">T-123</text>
|
||||
</g>
|
||||
```
|
||||
|
||||
### Required Container Elements
|
||||
|
||||
The generator will place clones in these containers (created automatically if missing):
|
||||
|
||||
```xml
|
||||
<g id="months-container"></g>
|
||||
<g id="lanes-container"></g>
|
||||
<g id="items-container"></g>
|
||||
```
|
||||
|
||||
### Text Element Mapping
|
||||
|
||||
Text elements in the item prototype are matched to your data fields:
|
||||
|
||||
- Elements with `id="item-{fieldname}"` (e.g., `id="item-title"`, `id="item-id"`)
|
||||
- Or elements with IDs containing the field name
|
||||
- Or the first `<text>` element if no specific match found
|
||||
|
||||
Example:
|
||||
```xml
|
||||
<g id="item-proto">
|
||||
<text id="item-title">Placeholder Title</text>
|
||||
<text id="item-id">T-000</text>
|
||||
<text id="item-status">TODO</text>
|
||||
</g>
|
||||
```
|
||||
|
||||
Will map to CSV columns based on your `fieldMapping` in project.json:
|
||||
```json
|
||||
{
|
||||
"fieldMapping": {
|
||||
"title": "Title",
|
||||
"id": "ID",
|
||||
"status": "Status"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Creating a Prototype Template in Inkscape
|
||||
|
||||
### Step 1: Start with a Base SVG
|
||||
|
||||
1. Open Inkscape
|
||||
2. Create new document (File → New)
|
||||
3. Set document size (recommended: 1200x800px)
|
||||
|
||||
### Step 2: Design Your Month Prototype
|
||||
|
||||
1. Create a group (Ctrl+G) and name it `month-proto` in the Object Properties panel
|
||||
2. Add elements:
|
||||
- Vertical line for grid separator
|
||||
- Text label for month name
|
||||
3. Style using Inkscape's fill, stroke, font tools
|
||||
4. Position at x=220 (will be translated for each month)
|
||||
|
||||
### Step 3: Design Your Lane Prototype
|
||||
|
||||
1. Create group named `lane-proto`
|
||||
2. Add elements:
|
||||
- Rectangle for background
|
||||
- Text label for lane name
|
||||
3. Style as desired (gradients, rounded corners, etc.)
|
||||
4. Position at y=140 (will be translated for each lane)
|
||||
|
||||
### Step 4: Design Your Item Prototype
|
||||
|
||||
1. Create group named `item-proto`
|
||||
2. Add elements:
|
||||
- Shape (circle, rect, path) for marker
|
||||
- Text elements for title, ID, or other fields
|
||||
3. Give text elements IDs like `item-title`, `item-id`
|
||||
4. Position at x=280, y=150
|
||||
|
||||
### Step 5: Add Containers
|
||||
|
||||
1. Create three empty groups:
|
||||
- `months-container`
|
||||
- `lanes-container`
|
||||
- `items-container`
|
||||
2. These can be anywhere - they're just containers
|
||||
|
||||
### Step 6: Add Styles
|
||||
|
||||
Use Inkscape's built-in styles or add a `<style>` section:
|
||||
|
||||
```xml
|
||||
<defs>
|
||||
<style>
|
||||
.month-label { font-family: Arial; font-size: 11px; fill: #666; }
|
||||
.lane-bg { fill: #f8f9fa; stroke: #e9ecef; }
|
||||
.item-marker { fill: #007bff; }
|
||||
</style>
|
||||
</defs>
|
||||
```
|
||||
|
||||
### Step 7: Save and Use
|
||||
|
||||
1. Save as Plain SVG (not Inkscape SVG)
|
||||
2. Reference in your project.json:
|
||||
```json
|
||||
{
|
||||
"svgTemplate": "template-proto.svg"
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Layout Settings
|
||||
|
||||
Control spacing and positioning in your project.json:
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": {
|
||||
"timelineMonths": 18,
|
||||
"marginLeft": 220,
|
||||
"marginTop": 140,
|
||||
"monthWidth": 120,
|
||||
"laneHeight": 80,
|
||||
"laneGap": 16
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `marginLeft`: Left margin before first month
|
||||
- `marginTop`: Top margin before first lane
|
||||
- `monthWidth`: Horizontal space for each month
|
||||
- `laneHeight`: Vertical space for each lane
|
||||
- `laneGap`: Space between lanes
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Minimal Prototype Template
|
||||
|
||||
See `example-proto/template-proto.svg` for a complete working example.
|
||||
|
||||
### Example 2: Custom Styling
|
||||
|
||||
```xml
|
||||
<g id="item-proto">
|
||||
<rect x="-8" y="-8" width="16" height="16"
|
||||
fill="#007bff" rx="3" opacity="0.8"/>
|
||||
<text x="12" y="4"
|
||||
font-family="Arial" font-size="10px" font-weight="bold">
|
||||
Task Title
|
||||
</text>
|
||||
</g>
|
||||
```
|
||||
|
||||
### Example 3: With Gradients
|
||||
|
||||
```xml
|
||||
<defs>
|
||||
<linearGradient id="laneGradient">
|
||||
<stop offset="0%" stop-color="#ffffff"/>
|
||||
<stop offset="100%" stop-color="#f0f0f0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<g id="lane-proto">
|
||||
<rect fill="url(#laneGradient)" width="1000" height="80"/>
|
||||
<text>Lane Name</text>
|
||||
</g>
|
||||
```
|
||||
|
||||
## Migration from Template-v2
|
||||
|
||||
If you have existing template-v2.svg files, you can continue using them. The generator automatically detects which template type you're using:
|
||||
|
||||
- **Prototype-based** (new): Detected by `id="*-proto"` elements → uses DOM generator
|
||||
- **Template-v2** (old): Detected by `id="*-template"` elements → uses string generator
|
||||
|
||||
To migrate:
|
||||
|
||||
1. Open your template-v2.svg in Inkscape
|
||||
2. Rename template elements:
|
||||
- `month-template` → `month-proto`
|
||||
- `lane-template` → `lane-proto`
|
||||
- `item-template` → `item-proto`
|
||||
3. Replace `{{PLACEHOLDERS}}` with actual sample text
|
||||
4. Add IDs to text elements (e.g., `id="item-title"`)
|
||||
5. Create container groups
|
||||
6. Save and test
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Prototypes Not Found
|
||||
|
||||
**Error:** `Template is missing required prototype elements`
|
||||
|
||||
**Solution:** Ensure your SVG has groups with exact IDs:
|
||||
- `month-proto`
|
||||
- `lane-proto`
|
||||
- `item-proto`
|
||||
|
||||
Check Object Properties panel in Inkscape (Ctrl+Shift+O).
|
||||
|
||||
### Text Not Updating
|
||||
|
||||
**Problem:** Cloned items show placeholder text instead of data
|
||||
|
||||
**Solution:** Add IDs to text elements matching your field names:
|
||||
```xml
|
||||
<text id="item-title">Placeholder</text>
|
||||
<text id="item-id">T-000</text>
|
||||
```
|
||||
|
||||
### Wrong Positioning
|
||||
|
||||
**Problem:** Elements appear in wrong locations
|
||||
|
||||
**Solution:** Prototypes should be positioned at a base location (e.g., x=220 for months). The generator adds transforms for each clone. Check that your `marginLeft`, `marginTop`, `monthWidth`, etc. settings match your prototype positions.
|
||||
|
||||
### Styling Not Applied
|
||||
|
||||
**Problem:** Generated timeline doesn't have styles from Inkscape
|
||||
|
||||
**Solution:**
|
||||
1. Use CSS classes in your prototypes, define in `<style>` section
|
||||
2. Or use inline SVG attributes (fill, stroke, etc.)
|
||||
3. Avoid Inkscape-specific attributes
|
||||
|
||||
### Parse Errors
|
||||
|
||||
**Error:** `Failed to parse SVG template`
|
||||
|
||||
**Solution:**
|
||||
1. Save as "Plain SVG" not "Inkscape SVG"
|
||||
2. Check SVG is valid XML (balanced tags, proper nesting)
|
||||
3. Remove Inkscape-specific elements if present
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep Prototypes Simple** - Complex nested structures may be harder to position
|
||||
2. **Use CSS Classes** - Easier to style consistently than inline attributes
|
||||
3. **Test Incrementally** - Start with basic shapes, add detail gradually
|
||||
4. **Use Inkscape Layers** - Keep prototypes on separate layer for organization
|
||||
5. **Set Prototype Opacity** - Makes them visible but distinguishable from final output
|
||||
6. **Document Your Template** - Add comments or text notes explaining custom elements
|
||||
|
||||
## Advanced: Custom Field Mapping
|
||||
|
||||
You can map CSV columns to any text element IDs:
|
||||
|
||||
```json
|
||||
{
|
||||
"fieldMapping": {
|
||||
"id": "ID",
|
||||
"title": "Task Name",
|
||||
"assignee": "Owner",
|
||||
"status": "State"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then in your prototype:
|
||||
```xml
|
||||
<g id="item-proto">
|
||||
<text id="item-id">T-000</text>
|
||||
<text id="item-title">Task placeholder</text>
|
||||
<text id="item-assignee">Person</text>
|
||||
<text id="item-status">Open</text>
|
||||
</g>
|
||||
```
|
||||
|
||||
## Further Resources
|
||||
|
||||
- See `example-proto/` for a complete working example
|
||||
- Read `CLAUDE.md` for generator implementation details
|
||||
- Check `README.md` for general usage instructions
|
||||
|
||||
## Comparison: Prototype vs Template-v2
|
||||
|
||||
| Feature | Prototype (New) | Template-v2 (Old) |
|
||||
|---------|----------------|-------------------|
|
||||
| Inkscape Editing | ✅ Full WYSIWYG | ⚠️ Limited (placeholders) |
|
||||
| Valid SVG | ✅ Yes | ✅ Yes |
|
||||
| Visual Preview | ✅ See actual design | ⚠️ See placeholders |
|
||||
| Syntax | Simple IDs | `{{PLACEHOLDER}}` |
|
||||
| Performance | ✅ DOM cloning | String manipulation |
|
||||
| Styling | Full Inkscape tools | CSS + inline |
|
||||
| Learning Curve | Lower (visual) | Higher (syntax) |
|
||||
| Recommended | ✅ Yes | For legacy only |
|
||||
@@ -102,8 +102,9 @@ Just open `index.html` in any modern browser (Chrome, Firefox, Safari, Edge).
|
||||
|
||||
You can choose between:
|
||||
|
||||
* automatic loading from `project.json` in the project folder
|
||||
* manual upload of a `project.json` file via the UI
|
||||
* **Automatic loading** - When served via HTTP, the app auto-loads from `project.json` in project folders (binect/, my-project/, or example/)
|
||||
* **Load Project Folder** - Click "📂 Load Project Folder" and select an entire project directory. All files (project.json, CSV, CSS, SVG) will be loaded automatically
|
||||
* **Load files individually** - Upload project.json first, then manually upload CSV, CSS, and SVG files using the individual file buttons
|
||||
|
||||
### **3. Preview the timeline**
|
||||
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
# SVG Template Refactoring Plan
|
||||
|
||||
## Current Architecture Problems
|
||||
## ✅ STATUS: COMPLETED
|
||||
|
||||
**Completion Date**: 2025-11-27
|
||||
**Final Architecture**: Template-only SVG generation with no fallback mechanisms
|
||||
|
||||
All phases have been successfully completed:
|
||||
- ✅ Phase 1: Template-v2.svg files created and working
|
||||
- ✅ Phase 2: Template-based generation fully implemented
|
||||
- ✅ Phase 3: Old templates removed, hardcoded generation eliminated
|
||||
- ✅ Comprehensive test coverage: 40+ tests (unit + integration)
|
||||
- ✅ Documentation complete (see TEMPLATE_V2_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
## Original Architecture Problems (Resolved)
|
||||
|
||||
### The Issue
|
||||
While `template.svg` files exist in `example/` and `my-project/`, they only contain:
|
||||
@@ -142,11 +156,54 @@ The `generator.js` should:
|
||||
- **Compatibility**: Need to support both old and new templates
|
||||
- **Testing**: More edge cases to test
|
||||
|
||||
## Next Steps
|
||||
## Final Implementation Summary
|
||||
|
||||
1. Review and approve this plan
|
||||
2. Create proof-of-concept with one template
|
||||
3. Refactor generator.js gradually
|
||||
4. Update tests
|
||||
5. Document new template format
|
||||
6. Migrate existing templates
|
||||
### Code Changes
|
||||
1. **generator.js**: Reduced from 326 to 210 lines (-36%)
|
||||
- Removed `generateHardcoded()` method entirely (~125 lines)
|
||||
- Added `validateTemplate()` for strict validation
|
||||
- Simplified `generate()` to call `generateFromTemplates()` directly
|
||||
- Updated `extractTemplate()` to throw errors instead of returning null
|
||||
|
||||
2. **Templates**: Removed old v1 templates
|
||||
- Deleted `example/template.svg`
|
||||
- Deleted `my-project/template.svg`
|
||||
- Updated project.json files to reference template-v2.svg exclusively
|
||||
|
||||
3. **Tests**: Refactored for template-only architecture
|
||||
- **engine.test.js**: 16 tests kept (unchanged)
|
||||
- **generator.test.js**: 23 tests (removed 11 hardcoded tests, kept template tests)
|
||||
- **integration.test.js**: 15 comprehensive e2e tests (added 8 new scenarios)
|
||||
- **testHelpers.js**: Updated with template-v2 helpers, malformed template generator, large dataset generator
|
||||
|
||||
### Test Coverage
|
||||
**Unit Tests (39 total)**:
|
||||
- 16 engine business logic tests
|
||||
- 23 generator tests (3 escapeXml, 5 validation, 3 extraction, 4 placeholder, 8 generation)
|
||||
|
||||
**Integration Tests (15 total)**:
|
||||
- Basic e2e workflow
|
||||
- CSV override handling
|
||||
- Large dataset (60+ items)
|
||||
- Date range edge cases (24+ months)
|
||||
- Special character handling
|
||||
- Empty CSV handling
|
||||
- Malformed template error handling (3 tests)
|
||||
- Template styling preservation
|
||||
- File upload handling (2 tests)
|
||||
- Export functionality (2 tests)
|
||||
|
||||
### Architecture Benefits Achieved
|
||||
1. **Single clear path**: Template-v2.svg format is required, no fallbacks
|
||||
2. **Clear error messages**: Immediate feedback when templates are malformed
|
||||
3. **Reduced complexity**: No branching logic, no backward compatibility code
|
||||
4. **Visual editing**: Users can edit templates in SVG tools
|
||||
5. **Maintainability**: ~235 lines of code removed, simpler architecture
|
||||
|
||||
### Breaking Changes
|
||||
- Old template.svg format no longer supported
|
||||
- Must use template-v2.svg with proper `<g id="*-template">` elements
|
||||
- No silent fallbacks - errors thrown immediately
|
||||
|
||||
### Migration Notes
|
||||
All existing projects already had template-v2.svg files created during Phase 1, so migration was seamless with only project.json updates needed.
|
||||
|
||||
354
TEMPLATE_V2_GUIDE.md
Normal file
@@ -0,0 +1,354 @@
|
||||
# Template-v2.svg Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Template-v2.svg is the required format for creating custom timeline visualizations. Templates are standard SVG files that can be edited in any SVG editor (Inkscape, Adobe Illustrator, Figma, etc.) and contain special template elements that define how months, lanes, and tasks are rendered.
|
||||
|
||||
## Template Structure
|
||||
|
||||
A valid template-v2.svg file must contain:
|
||||
|
||||
1. **Standard SVG wrapper** with xmlns declaration
|
||||
2. **`<defs>` section** containing three required template elements:
|
||||
- `<g id="month-template">` - Defines how each month column is rendered
|
||||
- `<g id="lane-template">` - Defines how each lane (epic/swimlane) is rendered
|
||||
- `<g id="item-template">` - Defines how each task item is rendered
|
||||
3. **Main content area** with `{{MONTHS}}` and `{{LANES}}` placeholders
|
||||
4. **Optional styling** (gradients, filters, patterns, etc.)
|
||||
|
||||
### Minimal Template Example
|
||||
|
||||
```svg
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<!-- Month template -->
|
||||
<g id="month-template" style="display:none">
|
||||
<line x1="{{MONTH_X}}" y1="{{GRID_TOP}}" x2="{{MONTH_X}}" y2="{{GRID_BOTTOM}}"
|
||||
stroke="#E0E0E0" stroke-width="1"/>
|
||||
<text x="{{MONTH_TEXT_X}}" y="{{MONTH_LABEL_Y}}"
|
||||
font-family="Arial" font-size="12" fill="#424242">{{MONTH_LABEL}}</text>
|
||||
</g>
|
||||
|
||||
<!-- Lane template -->
|
||||
<g id="lane-template" style="display:none">
|
||||
<rect x="{{LANE_X}}" y="{{LANE_Y}}" width="{{LANE_WIDTH}}" height="{{LANE_HEIGHT}}"
|
||||
fill="#FAFAFA" stroke="#E0E0E0" rx="8"/>
|
||||
<text x="{{LABEL_X}}" y="{{LABEL_Y}}"
|
||||
font-family="Arial" font-size="14" font-weight="bold" fill="#212121">{{LANE_NAME}}</text>
|
||||
</g>
|
||||
|
||||
<!-- Item template -->
|
||||
<g id="item-template" style="display:none">
|
||||
<circle cx="{{ITEM_X}}" cy="{{ITEM_Y}}" r="6" fill="#1976D2"/>
|
||||
<text x="{{TEXT_X}}" y="{{TEXT_Y}}"
|
||||
font-family="Arial" font-size="11" fill="#424242">{{ITEM_ID}} {{ITEM_TITLE}}</text>
|
||||
</g>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#FFFFFF"/>
|
||||
{{MONTHS}}
|
||||
{{LANES}}
|
||||
</svg>
|
||||
```
|
||||
|
||||
## Available Placeholders
|
||||
|
||||
### Month Template Placeholders
|
||||
|
||||
| Placeholder | Description | Example Value |
|
||||
|------------|-------------|---------------|
|
||||
| `{{MONTH_X}}` | X position of month column | 340 |
|
||||
| `{{GRID_TOP}}` | Y position of grid start | 120 |
|
||||
| `{{GRID_BOTTOM}}` | Y position of grid end | 560 |
|
||||
| `{{MONTH_X_OFFSET}}` | X position offset for backgrounds | 310 |
|
||||
| `{{MONTH_LABEL_Y_OFFSET}}` | Y position offset for label background | 70 |
|
||||
| `{{MONTH_TEXT_X}}` | X position for month text | 344 |
|
||||
| `{{MONTH_LABEL_Y}}` | Y position for month text | 90 |
|
||||
| `{{MONTH_LABEL}}` | Month label text | "Jan 25" |
|
||||
| `{{MONTH_SEP_X}}` | X position for separator line | 339 |
|
||||
| `{{GRID_HEIGHT}}` | Height of the grid | 440 |
|
||||
|
||||
### Lane Template Placeholders
|
||||
|
||||
| Placeholder | Description | Example Value |
|
||||
|------------|-------------|---------------|
|
||||
| `{{LANE_X}}` | X position of lane | 40 |
|
||||
| `{{LANE_Y}}` | Y position of lane | 116 |
|
||||
| `{{LANE_WIDTH}}` | Width of lane | 2460 |
|
||||
| `{{LANE_HEIGHT}}` | Height of lane | 80 |
|
||||
| `{{LABEL_X}}` | X position for lane label | 56 |
|
||||
| `{{LABEL_Y}}` | Y position for lane label | 140 |
|
||||
| `{{LANE_NAME}}` | Lane name text (XML-escaped) | "Development" |
|
||||
|
||||
### Item Template Placeholders
|
||||
|
||||
| Placeholder | Description | Example Value |
|
||||
|------------|-------------|---------------|
|
||||
| `{{ITEM_X}}` | X position of item marker | 400 |
|
||||
| `{{ITEM_Y}}` | Y position of item marker | 150 |
|
||||
| `{{TEXT_X}}` | X position for item text | 412 |
|
||||
| `{{TEXT_Y}}` | Y position for item text | 154 |
|
||||
| `{{ITEM_ID}}` | Item ID (XML-escaped) | "T-123" |
|
||||
| `{{ITEM_TITLE}}` | Item title (XML-escaped) | "Implement feature" |
|
||||
|
||||
**Dynamic Data Placeholders:**
|
||||
|
||||
The generator automatically creates placeholders for **all properties** in your CSV data using the naming convention: `ITEM_{PROPERTY_UPPERCASE}`.
|
||||
|
||||
For example, if your `fieldMapping` includes:
|
||||
```json
|
||||
{
|
||||
"id": "ID",
|
||||
"title": "Title",
|
||||
"assignee": "Assignee",
|
||||
"priority": "Priority"
|
||||
}
|
||||
```
|
||||
|
||||
The following placeholders become available:
|
||||
- `{{ITEM_ID}}` - from the `id` field
|
||||
- `{{ITEM_TITLE}}` - from the `title` field
|
||||
- `{{ITEM_ASSIGNEE}}` - from the `assignee` field
|
||||
- `{{ITEM_PRIORITY}}` - from the `priority` field
|
||||
|
||||
**Note:** The `due` field is used for positioning and is not available as a placeholder (use `{{MONTH_LABEL}}` for date display).
|
||||
|
||||
**Custom Placeholder Mapping:**
|
||||
|
||||
If your template uses non-standard placeholder names, you can override the default convention using `placeholderMapping` in project.json:
|
||||
|
||||
```json
|
||||
{
|
||||
"fieldMapping": {
|
||||
"id": "ID",
|
||||
"title": "Title",
|
||||
"assignee": "Assignee"
|
||||
},
|
||||
"placeholderMapping": {
|
||||
"id": "TASK_ID", // Use {{TASK_ID}} instead of {{ITEM_ID}}
|
||||
"title": "TASK_NAME", // Use {{TASK_NAME}} instead of {{ITEM_TITLE}}
|
||||
"assignee": "ASSIGNEE" // Use {{ASSIGNEE}} instead of {{ITEM_ASSIGNEE}}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is useful when:
|
||||
- Working with existing templates that use different naming conventions
|
||||
- Migrating from other timeline tools
|
||||
- Maintaining compatibility with legacy templates
|
||||
|
||||
Without `placeholderMapping`, the default `ITEM_{PROPERTY}` convention is used.
|
||||
|
||||
### Global Placeholders
|
||||
|
||||
These appear in the main template body (not in template elements):
|
||||
|
||||
| Placeholder | Description |
|
||||
|------------|-------------|
|
||||
| `{{MONTHS}}` | Replaced with all rendered month elements |
|
||||
| `{{LANES}}` | Replaced with all rendered lane and task elements |
|
||||
|
||||
## Editing Templates in SVG Tools
|
||||
|
||||
### Inkscape
|
||||
|
||||
1. Open template-v2.svg in Inkscape
|
||||
2. Locate template elements in the Layers panel (inside `<defs>`)
|
||||
3. Edit shapes, colors, fonts, etc. as needed
|
||||
4. **Important**: Keep `id="month-template"`, `id="lane-template"`, `id="item-template"` unchanged
|
||||
5. **Important**: Keep `{{PLACEHOLDER}}` text exactly as is - these are replaced at runtime
|
||||
6. Save file (keep SVG format, avoid Inkscape-specific extensions)
|
||||
|
||||
### Adobe Illustrator
|
||||
|
||||
1. Open template-v2.svg in Illustrator
|
||||
2. Use Layers panel to find template groups
|
||||
3. Edit visual properties (stroke, fill, fonts)
|
||||
4. **Do not** change group IDs or placeholder text
|
||||
5. Export as SVG (use "SVG 1.1" profile)
|
||||
|
||||
### Figma
|
||||
|
||||
1. Import template-v2.svg into Figma
|
||||
2. Edit styling and layout
|
||||
3. Keep placeholder text intact (wrapped in `{{` and `}}`)
|
||||
4. Export as SVG
|
||||
5. Manually verify `id` attributes are preserved
|
||||
|
||||
## Common Customizations
|
||||
|
||||
### Changing Colors
|
||||
|
||||
Edit the `fill` and `stroke` attributes:
|
||||
|
||||
```svg
|
||||
<!-- Original -->
|
||||
<rect fill="#FAFAFA" stroke="#E0E0E0"/>
|
||||
|
||||
<!-- Dark theme -->
|
||||
<rect fill="#2C2C2C" stroke="#404040"/>
|
||||
```
|
||||
|
||||
### Changing Fonts
|
||||
|
||||
Edit `font-family`, `font-size`, and `font-weight` attributes:
|
||||
|
||||
```svg
|
||||
<!-- Original -->
|
||||
<text font-family="Arial" font-size="12" fill="#424242">{{MONTH_LABEL}}</text>
|
||||
|
||||
<!-- Custom font -->
|
||||
<text font-family="'Roboto', sans-serif" font-size="14" fill="#1A1A1A">{{MONTH_LABEL}}</text>
|
||||
```
|
||||
|
||||
### Adding Gradients
|
||||
|
||||
1. Define gradient in `<defs>`:
|
||||
|
||||
```svg
|
||||
<defs>
|
||||
<linearGradient id="laneGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#E3F2FD;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#BBDEFB;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<g id="lane-template" style="display:none">
|
||||
<rect x="{{LANE_X}}" y="{{LANE_Y}}" width="{{LANE_WIDTH}}" height="{{LANE_HEIGHT}}"
|
||||
fill="url(#laneGradient)" stroke="#90CAF9" rx="8"/>
|
||||
...
|
||||
</g>
|
||||
</defs>
|
||||
```
|
||||
|
||||
### Adding Drop Shadows
|
||||
|
||||
Use SVG filters:
|
||||
|
||||
```svg
|
||||
<defs>
|
||||
<filter id="dropShadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="2"/>
|
||||
<feOffset dx="0" dy="2" result="offsetblur"/>
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.2"/>
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
<g id="item-template" style="display:none">
|
||||
<circle cx="{{ITEM_X}}" cy="{{ITEM_Y}}" r="6" fill="#1976D2" filter="url(#dropShadow)"/>
|
||||
...
|
||||
</g>
|
||||
</defs>
|
||||
```
|
||||
|
||||
## Layout Constants
|
||||
|
||||
The generator uses these constants for positioning (defined in generator.js):
|
||||
|
||||
```javascript
|
||||
const left = 220; // Left margin
|
||||
const top = 140; // Top margin
|
||||
const monthWidth = 120; // Width of each month column
|
||||
const laneHeight = 80; // Height of each lane
|
||||
const laneGap = 16; // Vertical gap between lanes
|
||||
```
|
||||
|
||||
To change the overall layout, you would need to modify generator.js. Templates control the visual appearance within these constraints.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "Template is required"
|
||||
|
||||
**Cause**: No template was provided or template file couldn't be loaded.
|
||||
|
||||
**Fix**:
|
||||
- Verify `svgTemplate` field in project.json points to a valid template-v2.svg file
|
||||
- Check that the template file exists in the project folder
|
||||
|
||||
### Error: "Template is missing required elements: month-template"
|
||||
|
||||
**Cause**: The template is missing one or more required `<g id="*-template">` elements.
|
||||
|
||||
**Fix**:
|
||||
- Ensure template has all three required elements in `<defs>`:
|
||||
- `<g id="month-template">`
|
||||
- `<g id="lane-template">`
|
||||
- `<g id="item-template">`
|
||||
- Check that IDs are exactly as shown (case-sensitive)
|
||||
- Verify elements are inside `<defs>` section
|
||||
|
||||
### Error: "Failed to extract template element: month-template"
|
||||
|
||||
**Cause**: Template element was found during validation but couldn't be extracted (malformed structure).
|
||||
|
||||
**Fix**:
|
||||
- Ensure each template element is properly closed: `<g id="month-template">...</g>`
|
||||
- Check for invalid nested structures or unclosed tags
|
||||
- Validate SVG syntax using an online SVG validator
|
||||
|
||||
### Placeholders Not Being Replaced
|
||||
|
||||
**Symptoms**: Generated SVG contains literal `{{MONTH_X}}` text instead of numbers.
|
||||
|
||||
**Fix**:
|
||||
- Verify placeholder syntax: must be exactly `{{PLACEHOLDER}}` (case-sensitive)
|
||||
- Check for typos in placeholder names (see tables above for correct names)
|
||||
- Ensure placeholders are in text content, not in comments
|
||||
|
||||
### Template Elements Visible in Output
|
||||
|
||||
**Symptoms**: Template elements appear in the final SVG with `display:none` style.
|
||||
|
||||
**Fix**:
|
||||
- Verify that template elements have `style="display:none"` attribute
|
||||
- The generator removes this attribute when cloning, but if elements are leaking through, check template structure
|
||||
- Ensure template elements are only in `<defs>`, not in main content area
|
||||
|
||||
### SVG Dimensions Too Small/Large
|
||||
|
||||
**Cause**: Generator calculates dimensions based on number of months, lanes, and layout constants.
|
||||
|
||||
**Symptoms**: Content is cut off or timeline has excessive white space.
|
||||
|
||||
**Fix**:
|
||||
- Adjust `timelineMonths` setting in project.json to show more/fewer months
|
||||
- Modify layout constants in generator.js:151 for more control over spacing
|
||||
- Template styling doesn't affect dimensions - that's controlled by the generator
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep template elements hidden**: Always include `style="display:none"` on template groups
|
||||
2. **Use semantic IDs**: Don't change the required `id` attributes
|
||||
3. **Test frequently**: Generate a timeline after each template edit to verify changes
|
||||
4. **Version control**: Keep template files in version control to track changes
|
||||
5. **Start simple**: Begin with the minimal template example and add complexity gradually
|
||||
6. **Preserve placeholders**: Never modify the placeholder text - they're replaced at runtime
|
||||
7. **Use relative units carefully**: Absolute pixel values work best for positioning placeholders
|
||||
8. **Add comments**: Document custom styles and modifications for future reference
|
||||
|
||||
## Example Templates
|
||||
|
||||
See the `example/` and `my-project/` folders for working template-v2.svg files that demonstrate:
|
||||
- Professional styling with gradients and shadows
|
||||
- Custom color schemes
|
||||
- Different font choices
|
||||
- Grid patterns and backgrounds
|
||||
|
||||
## Further Resources
|
||||
|
||||
- **SVG Specification**: https://www.w3.org/TR/SVG2/
|
||||
- **Inkscape Tutorials**: https://inkscape.org/learn/tutorials/
|
||||
- **SVG Filters**: https://www.w3.org/TR/SVG11/filters.html
|
||||
- **Online SVG Editor**: https://svg-edit.github.io/svgedit/
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter issues not covered in this guide:
|
||||
1. Check that your template validates as proper SVG
|
||||
2. Compare with the example templates
|
||||
3. Review error messages carefully - they indicate what's missing or malformed
|
||||
4. File an issue at the project repository with your template attached
|
||||
403
TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,403 @@
|
||||
# Troubleshooting Guide
|
||||
|
||||
This document provides solutions to common issues encountered when developing with TimelineSvg.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Problem | Quick Fix | See Section |
|
||||
|---------|-----------|-------------|
|
||||
| `npm install` fails with SSL cipher errors in WSL | `export NODE_OPTIONS="--openssl-legacy-provider" && npm install` | [npm Install Failures](#npm-install-failures-in-wsl-with-nodejs-v24) |
|
||||
| CORS errors when opening index.html | `make serve` or `python3 -m http.server 8000` | [CORS Errors](#cors-errors-when-loading-timeline) |
|
||||
| "Template is missing required elements" | Check `<defs>` section has all 3 template IDs | [Template Validation](#template-validation-errors) |
|
||||
| CSV loads but shows 0 valid items | Verify `fieldMapping` matches CSV column names | [CSV Data Not Loading](#csv-data-not-loading) |
|
||||
| Tests failing or timing out | `make clean && make install` | [Tests Failing](#tests-failing) |
|
||||
|
||||
---
|
||||
|
||||
## npm Install Failures in WSL with Node.js v24
|
||||
|
||||
### Problem Description
|
||||
|
||||
**Symptoms:**
|
||||
- `npm install` fails with error: `ERR_SSL_CIPHER_OPERATION_FAILED`
|
||||
- Error message contains: `Provider routines:ossl_gcm_stream_update:cipher operation failed`
|
||||
- May also see `ETXTBSY` (text file busy) errors during esbuild installation
|
||||
- Occurs specifically in WSL (Windows Subsystem for Linux) environments
|
||||
- Appears with Node.js v24.x
|
||||
|
||||
**Error Example:**
|
||||
```
|
||||
npm error code ERR_SSL_CIPHER_OPERATION_FAILED
|
||||
npm error C09CD49A5C750000:error:1C800066:Provider routines:ossl_gcm_stream_update:cipher operation failed:../deps/openssl/openssl/providers/implementations/ciphers/ciphercommon_gcm.c:346:
|
||||
```
|
||||
|
||||
### Root Cause
|
||||
|
||||
Node.js v24 upgraded to OpenSSL 3.x, which introduced stricter cipher requirements and different default providers. When running in WSL, there's a compatibility issue between:
|
||||
- Node.js v24's OpenSSL 3.x implementation
|
||||
- WSL's kernel/filesystem handling of cryptographic operations
|
||||
- npm's package extraction process (which uses encryption for tar.gz files)
|
||||
|
||||
The cipher operation fails during package decompression, preventing successful installation.
|
||||
|
||||
### Solution
|
||||
|
||||
**Quick Fix (Recommended):**
|
||||
|
||||
Use the provided Makefile, which includes the workaround:
|
||||
```bash
|
||||
make install
|
||||
```
|
||||
|
||||
**Manual Fix:**
|
||||
|
||||
If not using the Makefile, set the `NODE_OPTIONS` environment variable before running npm:
|
||||
```bash
|
||||
export NODE_OPTIONS="--openssl-legacy-provider"
|
||||
npm install
|
||||
```
|
||||
|
||||
**Permanent Fix (for your shell session):**
|
||||
|
||||
Add to your `~/.bashrc` or `~/.zshrc`:
|
||||
```bash
|
||||
export NODE_OPTIONS="--openssl-legacy-provider"
|
||||
```
|
||||
|
||||
Then reload your shell:
|
||||
```bash
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
### Step-by-Step Recovery
|
||||
|
||||
If you're in a broken state with partial installation:
|
||||
|
||||
1. **Clean everything:**
|
||||
```bash
|
||||
rm -rf node_modules package-lock.json
|
||||
npm cache clean --force
|
||||
```
|
||||
|
||||
2. **Install with workaround:**
|
||||
```bash
|
||||
export NODE_OPTIONS="--openssl-legacy-provider"
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Verify installation:**
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Should show: `58 passed (58)` across 3 test files
|
||||
|
||||
### Alternative Solutions
|
||||
|
||||
**Option A: Use Node.js v22 (LTS)**
|
||||
|
||||
If you don't need Node.js v24 features:
|
||||
```bash
|
||||
# Using nvm (Node Version Manager)
|
||||
nvm install 22
|
||||
nvm use 22
|
||||
npm install
|
||||
```
|
||||
|
||||
Node.js v22 uses OpenSSL 1.1.1 and doesn't have this issue.
|
||||
|
||||
**Option B: Use npm's legacy peer deps flag**
|
||||
|
||||
Sometimes combining flags helps:
|
||||
```bash
|
||||
export NODE_OPTIONS="--openssl-legacy-provider"
|
||||
npm install --legacy-peer-deps
|
||||
```
|
||||
|
||||
### Prevention
|
||||
|
||||
**For New Projects:**
|
||||
|
||||
Update your project's documentation and Makefile to include the workaround automatically. See the current `Makefile` for reference.
|
||||
|
||||
**For CI/CD:**
|
||||
|
||||
Add to your CI configuration:
|
||||
```yaml
|
||||
# GitHub Actions example
|
||||
env:
|
||||
NODE_OPTIONS: "--openssl-legacy-provider"
|
||||
|
||||
# GitLab CI example
|
||||
variables:
|
||||
NODE_OPTIONS: "--openssl-legacy-provider"
|
||||
```
|
||||
|
||||
### Verification
|
||||
|
||||
After applying the fix, verify with:
|
||||
|
||||
```bash
|
||||
# Check Node.js version
|
||||
node --version # Should show v24.x.x
|
||||
|
||||
# Check installation succeeded
|
||||
ls -la node_modules | head -5
|
||||
|
||||
# Run tests to confirm everything works
|
||||
npm test
|
||||
|
||||
# Expected output:
|
||||
# ✓ Test Files 3 passed (3)
|
||||
# ✓ Tests 58 passed (58)
|
||||
```
|
||||
|
||||
### Related Issues
|
||||
|
||||
- **esbuild ETXTBSY errors**: Often occur together with SSL cipher errors. The same solution fixes both.
|
||||
- **Vitest installation failures**: Vitest depends on esbuild, so the fix resolves Vitest installation issues too.
|
||||
|
||||
### When This Won't Help
|
||||
|
||||
This solution specifically addresses Node.js v24 + OpenSSL 3.x + WSL issues. It won't help with:
|
||||
- Network connectivity problems (check `npm config get registry`)
|
||||
- Permissions issues (avoid `sudo npm install`, use `nvm` instead)
|
||||
- Disk space problems (check with `df -h`)
|
||||
- Corrupted npm cache (run `npm cache verify`)
|
||||
|
||||
---
|
||||
|
||||
## CORS Errors When Loading Timeline
|
||||
|
||||
### Problem Description
|
||||
|
||||
**Symptoms:**
|
||||
- Opening `index.html` directly in browser (file:// protocol)
|
||||
- Console shows CORS errors when trying to load CSV, CSS, or SVG files
|
||||
- Timeline doesn't render, shows "Keine Timeline verfügbar"
|
||||
|
||||
### Solution
|
||||
|
||||
Serve the application via HTTP:
|
||||
|
||||
```bash
|
||||
# Using the Makefile (recommended)
|
||||
make serve
|
||||
|
||||
# Or manually with Python 3
|
||||
python3 -m http.server 8000
|
||||
|
||||
# Or with Python 2
|
||||
python -m SimpleHTTPServer 8000
|
||||
|
||||
# Or with Node.js/npx
|
||||
npx serve -l 8000
|
||||
```
|
||||
|
||||
Then open: `http://localhost:8000`
|
||||
|
||||
### Why This Happens
|
||||
|
||||
Browsers enforce CORS (Cross-Origin Resource Sharing) restrictions on `file://` URLs for security. Loading external resources (CSV, CSS, SVG) requires HTTP protocol.
|
||||
|
||||
---
|
||||
|
||||
## Template Validation Errors
|
||||
|
||||
### Problem Description
|
||||
|
||||
**Symptoms:**
|
||||
- Error: "Template is missing required elements: month-template"
|
||||
- Error: "Failed to extract template element"
|
||||
- Template renders but with empty placeholders
|
||||
|
||||
### Solution
|
||||
|
||||
**Check template structure:**
|
||||
|
||||
1. Open your `template-v2.svg` in a text editor
|
||||
2. Verify it has a `<defs>` section
|
||||
3. Ensure all three required template elements exist:
|
||||
```svg
|
||||
<defs>
|
||||
<g id="month-template" style="display:none">...</g>
|
||||
<g id="lane-template" style="display:none">...</g>
|
||||
<g id="item-template" style="display:none">...</g>
|
||||
</defs>
|
||||
```
|
||||
|
||||
4. Check that IDs are exactly as shown (case-sensitive)
|
||||
5. Verify templates contain proper placeholders (e.g., `{{MONTH_X}}`, `{{LANE_NAME}}`)
|
||||
|
||||
**Common mistakes:**
|
||||
- Template elements outside `<defs>` section
|
||||
- Incorrect `id` attributes (e.g., `month_template` instead of `month-template`)
|
||||
- Missing `style="display:none"` (causes templates to show in output)
|
||||
- Malformed XML/SVG (unclosed tags)
|
||||
|
||||
**Quick fix:**
|
||||
|
||||
Copy a working template from `example/template-v2.svg` and modify it.
|
||||
|
||||
See `TEMPLATE_V2_GUIDE.md` for comprehensive template documentation.
|
||||
|
||||
---
|
||||
|
||||
## Project Files Not Auto-Loading
|
||||
|
||||
### Problem Description
|
||||
|
||||
**Symptoms:**
|
||||
- Uploaded project.json but CSV/SVG/CSS files don't load
|
||||
- Have to manually select each file individually
|
||||
- Timeline doesn't render after selecting project.json
|
||||
|
||||
### Solution
|
||||
|
||||
This is expected behavior due to browser security restrictions. When you upload a single project.json file, the browser doesn't allow automatic access to other files in the same directory.
|
||||
|
||||
**Use the Folder Picker (Recommended):**
|
||||
|
||||
1. Click **"📂 Load Project Folder"** (blue button at top of file manager)
|
||||
2. Select the entire project folder
|
||||
3. All files will be loaded automatically
|
||||
|
||||
This works because you're explicitly granting permission to access all files in the folder.
|
||||
|
||||
**Alternative:** Load files individually using the separate upload buttons for CSV, SVG, and CSS.
|
||||
|
||||
**For developers running from local server:** Files auto-load when served via HTTP (no manual upload needed).
|
||||
|
||||
---
|
||||
|
||||
## CSV Data Not Loading
|
||||
|
||||
### Problem Description
|
||||
|
||||
**Symptoms:**
|
||||
- CSV file loads but no items appear
|
||||
- Debug panel shows "Valid Items: 0"
|
||||
- Console shows "Keine gültigen Items gefunden"
|
||||
|
||||
### Solution
|
||||
|
||||
**Check field mappings:**
|
||||
|
||||
1. Open debug panel in the UI (should show automatically)
|
||||
2. Compare "CSV Structure Preview" headers with "Field Mapping"
|
||||
3. Ensure `project.json` field mappings match your CSV column names exactly
|
||||
|
||||
**Example:**
|
||||
|
||||
If your CSV has:
|
||||
```csv
|
||||
Task_ID,Task_Name,Due_Date,Epic
|
||||
```
|
||||
|
||||
Your `project.json` must have:
|
||||
```json
|
||||
{
|
||||
"fieldMapping": {
|
||||
"id": "Task_ID",
|
||||
"title": "Task_Name",
|
||||
"lane": "Epic",
|
||||
"due": ["Due_Date"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Check date formats:**
|
||||
|
||||
Supported formats:
|
||||
- `YYYY-MM-DD` (e.g., `2025-12-31`)
|
||||
- `YYYY/MM/DD` (e.g., `2025/12/31`)
|
||||
- `DD.MM.YYYY` (e.g., `31.12.2025`)
|
||||
|
||||
Unsupported formats will cause items to be filtered out.
|
||||
|
||||
### Verification
|
||||
|
||||
Look at the debug panel:
|
||||
- "Parsed Rows" should match your CSV row count
|
||||
- "Valid Items" should equal "Parsed Rows" (or close to it)
|
||||
- If they differ significantly, check date formats and required fields
|
||||
|
||||
---
|
||||
|
||||
## Tests Failing
|
||||
|
||||
### Problem Description
|
||||
|
||||
**Symptoms:**
|
||||
- `npm test` fails with errors
|
||||
- Tests timeout or hang
|
||||
- jsdom errors
|
||||
|
||||
### Solution
|
||||
|
||||
**Basic troubleshooting:**
|
||||
|
||||
1. **Reinstall dependencies:**
|
||||
```bash
|
||||
make clean
|
||||
make install
|
||||
```
|
||||
|
||||
2. **Check Node.js version:**
|
||||
```bash
|
||||
node --version # Should be v22+ or v24+
|
||||
```
|
||||
|
||||
3. **Run specific test file:**
|
||||
```bash
|
||||
npm test test/generator.test.js
|
||||
```
|
||||
|
||||
4. **Check test environment:**
|
||||
```bash
|
||||
cat vitest.config.js
|
||||
```
|
||||
Should show `environment: 'jsdom'`
|
||||
|
||||
5. **Clear Vitest cache:**
|
||||
```bash
|
||||
rm -rf .vitest node_modules/.vitest
|
||||
npm test
|
||||
```
|
||||
|
||||
### Expected Test Results
|
||||
|
||||
All tests passing:
|
||||
```
|
||||
✓ test/generator.test.js (36 tests)
|
||||
✓ test/engine.test.js (7 tests)
|
||||
✓ test/integration.test.js (15 tests)
|
||||
|
||||
Test Files 3 passed (3)
|
||||
Tests 58 passed (58)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Need More Help?
|
||||
|
||||
1. Check existing documentation:
|
||||
- `README.md` - Overview and features
|
||||
- `TEMPLATE_V2_GUIDE.md` - Template creation and customization
|
||||
- `CLAUDE.md` - Architecture and development guide
|
||||
|
||||
2. Enable debug mode:
|
||||
- Debug panel shows automatically when loading projects
|
||||
- Check browser console for detailed error messages
|
||||
|
||||
3. Verify file paths:
|
||||
- All paths in `project.json` are relative to the project folder
|
||||
- Use forward slashes (`/`) not backslashes (`\`)
|
||||
|
||||
4. Test with example project:
|
||||
```bash
|
||||
# Open browser to http://localhost:8000
|
||||
# Example project should auto-load successfully
|
||||
```
|
||||
|
||||
If example works but your project doesn't, compare configurations.
|
||||
228
WINDOWS_USAGE.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# Using Timeline SVG on Windows
|
||||
|
||||
This guide explains how to use Timeline SVG Generator on Windows after building the distribution in WSL.
|
||||
|
||||
## Building the Distribution (in WSL)
|
||||
|
||||
1. In your WSL terminal, navigate to the project directory:
|
||||
```bash
|
||||
cd /home/worsch/timeline-svg
|
||||
```
|
||||
|
||||
2. Build the distribution package:
|
||||
```bash
|
||||
make dist-zip
|
||||
```
|
||||
|
||||
This creates either `timeline-svg-dist.zip` (if zip is installed) or `timeline-svg-dist.tar.gz`.
|
||||
|
||||
3. The distribution file is now in your project directory and accessible from Windows at:
|
||||
```
|
||||
\\wsl$\Ubuntu\home\worsch\timeline-svg\timeline-svg-dist.tar.gz
|
||||
```
|
||||
|
||||
Or if you have the WSL path mounted:
|
||||
```
|
||||
\\wsl.localhost\Ubuntu\home\worsch\timeline-svg\timeline-svg-dist.tar.gz
|
||||
```
|
||||
|
||||
## Transferring to Windows
|
||||
|
||||
### Option 1: Copy from WSL Location
|
||||
|
||||
1. Open Windows Explorer
|
||||
2. Navigate to `\\wsl$\Ubuntu\home\worsch\timeline-svg\`
|
||||
3. Copy `timeline-svg-dist.tar.gz` (or `.zip`) to your Windows directory
|
||||
- Example: `C:\Users\YourName\Documents\timeline-svg\`
|
||||
|
||||
### Option 2: Use Command Line
|
||||
|
||||
In WSL terminal:
|
||||
```bash
|
||||
# Copy to Windows user directory
|
||||
cp timeline-svg-dist.tar.gz /mnt/c/Users/YourName/Documents/
|
||||
```
|
||||
|
||||
Or from Windows PowerShell:
|
||||
```powershell
|
||||
# Copy from WSL to Windows
|
||||
Copy-Item "\\wsl$\Ubuntu\home\worsch\timeline-svg\timeline-svg-dist.tar.gz" -Destination "C:\Users\YourName\Documents\"
|
||||
```
|
||||
|
||||
## Extracting on Windows
|
||||
|
||||
### If you have a .zip file:
|
||||
- Right-click the file → "Extract All..."
|
||||
- Choose destination folder
|
||||
|
||||
### If you have a .tar.gz file:
|
||||
- Install 7-Zip (free): https://www.7-zip.org/
|
||||
- Right-click → 7-Zip → Extract Here (twice - first for .gz, then for .tar)
|
||||
|
||||
## Running on Windows
|
||||
|
||||
Once extracted, you have several options:
|
||||
|
||||
### Option 1: Use the Batch Script (Easiest)
|
||||
|
||||
1. Double-click `start-server.bat` in the extracted folder
|
||||
2. A command window will open and your browser will launch automatically
|
||||
3. The application will be available at http://localhost:8000
|
||||
4. To stop the server, close the command window or press Ctrl+C
|
||||
|
||||
**Note:** Requires Python to be installed on Windows. Download from https://www.python.org/ if needed.
|
||||
|
||||
### Option 2: Use Python Directly
|
||||
|
||||
1. Open Command Prompt or PowerShell in the extracted folder
|
||||
2. Run one of these commands:
|
||||
```cmd
|
||||
python -m http.server 8000
|
||||
```
|
||||
or
|
||||
```cmd
|
||||
python3 -m http.server 8000
|
||||
```
|
||||
3. Open your browser to http://localhost:8000
|
||||
|
||||
### Option 3: Use Node.js http-server
|
||||
|
||||
If you have Node.js installed:
|
||||
```cmd
|
||||
npx http-server -p 8000
|
||||
```
|
||||
|
||||
### Option 4: Direct File Access (Limited)
|
||||
|
||||
You can open `index.html` directly in your browser, but due to CORS restrictions:
|
||||
- ✅ UI will load
|
||||
- ❌ Auto-loading of example projects won't work
|
||||
- ❌ CSV/template files must be manually uploaded
|
||||
|
||||
**Recommendation:** Use a local server (Option 1-3) for full functionality.
|
||||
|
||||
## Working with Your Own Projects
|
||||
|
||||
### Creating a New Project
|
||||
|
||||
1. Copy one of the example folders (e.g., `example\`)
|
||||
2. Rename it to your project name
|
||||
3. Edit the files:
|
||||
- `project.json` - Configure field mappings and settings
|
||||
- `sample.csv` - Replace with your timeline data
|
||||
- `style.css` - Customize colors and fonts
|
||||
- `template-v2.svg` - Design your timeline layout
|
||||
|
||||
### Loading Your Project
|
||||
|
||||
You have two options:
|
||||
|
||||
#### Option 1: Load Entire Project Folder (Recommended)
|
||||
|
||||
1. Click **"📂 Load Project Folder"**
|
||||
2. Select your project folder (e.g., `example\` or your custom project folder)
|
||||
3. All files (project.json, CSV, SVG, CSS) will be loaded automatically
|
||||
4. Timeline generates immediately
|
||||
|
||||
This is the easiest method - one click loads everything!
|
||||
|
||||
#### Option 2: Load Files Individually
|
||||
|
||||
1. Click **"📁 Load"** next to "Project Configuration"
|
||||
2. Select your `project.json` file
|
||||
3. Manually upload CSV, CSS, and SVG files using the respective buttons
|
||||
|
||||
**Note:** Due to browser security, uploading just project.json won't automatically load the other files from the same directory. Use the folder picker (Option 1) for automatic loading.
|
||||
|
||||
## File Paths on Windows
|
||||
|
||||
The application uses forward slashes (`/`) in URLs, which work correctly in browsers on Windows. You don't need to change anything in the code.
|
||||
|
||||
**Example project.json on Windows:**
|
||||
```json
|
||||
{
|
||||
"name": "My Timeline",
|
||||
"dataSource": "sample.csv",
|
||||
"stylesheet": "style.css",
|
||||
"svgTemplate": "template-v2.svg"
|
||||
}
|
||||
```
|
||||
|
||||
These relative paths work the same way on Windows and Linux when served via HTTP.
|
||||
|
||||
## Troubleshooting on Windows
|
||||
|
||||
### "Python not found"
|
||||
|
||||
Install Python:
|
||||
1. Download from https://www.python.org/downloads/
|
||||
2. **Important:** Check "Add Python to PATH" during installation
|
||||
3. Restart Command Prompt
|
||||
4. Try running `start-server.bat` again
|
||||
|
||||
### Port 8000 already in use
|
||||
|
||||
Change the port in `start-server.bat`:
|
||||
- Edit the file with Notepad
|
||||
- Change `8000` to another port like `8080` or `3000`
|
||||
- Update the browser URL accordingly
|
||||
|
||||
### Files not loading (CORS errors)
|
||||
|
||||
- Make sure you're using the local server (not opening index.html directly)
|
||||
- Check that all project files are in the same directory structure
|
||||
- Look at browser console (F12) for specific error messages
|
||||
|
||||
### Browser doesn't open automatically
|
||||
|
||||
If `start http://localhost:8000` doesn't work:
|
||||
- Manually open your browser
|
||||
- Navigate to http://localhost:8000
|
||||
- The application should load normally
|
||||
|
||||
## Syncing Between WSL and Windows
|
||||
|
||||
If you want to develop in WSL but test on Windows:
|
||||
|
||||
### Option 1: Work in WSL-accessible location
|
||||
|
||||
Develop in a Windows folder accessed from WSL:
|
||||
```bash
|
||||
cd /mnt/c/Users/YourName/Documents/timeline-svg
|
||||
```
|
||||
|
||||
Changes made in WSL are immediately visible in Windows.
|
||||
|
||||
### Option 2: Rebuild distribution after changes
|
||||
|
||||
1. Make changes in WSL
|
||||
2. Rebuild: `make dist-zip`
|
||||
3. Copy to Windows
|
||||
4. Extract and test
|
||||
|
||||
### Option 3: Use the Windows folder directly
|
||||
|
||||
The distribution folder can be used from both environments:
|
||||
- WSL: `cd /mnt/c/Users/YourName/Documents/timeline-svg/dist`
|
||||
- Windows: `C:\Users\YourName\Documents\timeline-svg\dist`
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- The application runs entirely in the browser (no backend needed)
|
||||
- Performance is identical on Windows and Linux
|
||||
- Large CSV files (>1000 rows) may take a few seconds to process
|
||||
- SVG generation is client-side and depends on your browser/CPU speed
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- See `README.md` for application overview and features
|
||||
- See `TEMPLATE_V2_GUIDE.md` for template customization
|
||||
- See `TROUBLESHOOTING.md` for common issues and solutions
|
||||
|
||||
## Questions or Issues?
|
||||
|
||||
If you encounter problems specific to Windows:
|
||||
1. Check the browser console (F12 → Console tab) for error messages
|
||||
2. Verify Python is installed and in PATH
|
||||
3. Try the alternative server options above
|
||||
4. Check file permissions (extracted files should be readable)
|
||||
427
engine.js
@@ -3,6 +3,8 @@ window.timelineEngine = {
|
||||
template: null,
|
||||
csvOverride: false,
|
||||
cssOverride: false,
|
||||
csvData: null, // Store current CSV text
|
||||
cssData: null, // Store current CSS text
|
||||
|
||||
projectBasePath: '',
|
||||
|
||||
@@ -190,33 +192,49 @@ window.timelineEngine = {
|
||||
// Update project status
|
||||
this.updateFileStatus('project', name, 'loaded');
|
||||
|
||||
// Enable project edit button
|
||||
if (window.fileEditor) {
|
||||
window.fileEditor.enableEditButton('project');
|
||||
}
|
||||
|
||||
// Show field mappings in debug panel
|
||||
this.showFieldMappings();
|
||||
|
||||
// Track loading errors for user feedback
|
||||
const loadingErrors = [];
|
||||
|
||||
// Stylesheet
|
||||
if (cfg.stylesheet && !this.cssOverride) {
|
||||
const stylesheetPath = this.resolveProjectPath(cfg.stylesheet);
|
||||
const linkElement = document.getElementById("dynamicCss");
|
||||
|
||||
// Set up load/error event handlers before setting href
|
||||
const handleLoad = () => {
|
||||
console.log("Stylesheet loaded successfully:", stylesheetPath);
|
||||
this.updateFileStatus('css', cfg.stylesheet + ' ✨', 'loaded');
|
||||
linkElement.removeEventListener('load', handleLoad);
|
||||
linkElement.removeEventListener('error', handleError);
|
||||
};
|
||||
// Load CSS via fetch to store content for editing
|
||||
try {
|
||||
const cssResponse = await fetch(stylesheetPath);
|
||||
if (cssResponse.ok) {
|
||||
const cssText = await cssResponse.text();
|
||||
this.cssData = cssText; // Store for editing
|
||||
|
||||
const handleError = () => {
|
||||
console.error("Stylesheet could not be loaded:", stylesheetPath);
|
||||
// Apply CSS
|
||||
const blob = new Blob([cssText], { type: "text/css" });
|
||||
const linkElement = document.getElementById("dynamicCss");
|
||||
linkElement.href = URL.createObjectURL(blob);
|
||||
|
||||
this.updateFileStatus('css', cfg.stylesheet + ' ✨', 'loaded');
|
||||
|
||||
// Enable CSS edit button
|
||||
if (window.fileEditor) {
|
||||
window.fileEditor.enableEditButton('css');
|
||||
}
|
||||
|
||||
console.log("Stylesheet loaded successfully:", stylesheetPath);
|
||||
} else {
|
||||
throw new Error(`HTTP ${cssResponse.status}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Stylesheet could not be loaded:", e);
|
||||
this.updateFileStatus('css', cfg.stylesheet, 'error');
|
||||
loadingErrors.push(`Stylesheet: ${cfg.stylesheet}`);
|
||||
linkElement.removeEventListener('load', handleLoad);
|
||||
linkElement.removeEventListener('error', handleError);
|
||||
};
|
||||
|
||||
linkElement.addEventListener('load', handleLoad);
|
||||
linkElement.addEventListener('error', handleError);
|
||||
linkElement.href = stylesheetPath;
|
||||
}
|
||||
}
|
||||
|
||||
// SVG template
|
||||
@@ -224,17 +242,25 @@ window.timelineEngine = {
|
||||
try {
|
||||
const templatePath = this.resolveProjectPath(cfg.svgTemplate);
|
||||
console.log("Attempting to fetch SVG template from:", templatePath);
|
||||
const response = await fetch(templatePath);
|
||||
console.log("SVG template fetch response:", response.status, response.statusText);
|
||||
const svgResponse = await fetch(templatePath);
|
||||
console.log("SVG template fetch response:", svgResponse.status, svgResponse.statusText);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
if (!svgResponse.ok) {
|
||||
throw new Error(`HTTP ${svgResponse.status}: ${svgResponse.statusText}`);
|
||||
}
|
||||
|
||||
this.template = await response.text();
|
||||
this.template = await svgResponse.text();
|
||||
console.log("SVG template loaded, length:", this.template.length);
|
||||
this.updateFileStatus('svg', cfg.svgTemplate, 'loaded');
|
||||
|
||||
// Enable SVG edit button
|
||||
if (window.fileEditor) {
|
||||
window.fileEditor.enableEditButton('svg');
|
||||
}
|
||||
|
||||
// Show template fields in debug panel
|
||||
this.showTemplateFields();
|
||||
|
||||
// Show template preview if no CSV data is loaded yet
|
||||
if (!this.csvOverride && document.querySelector("#viewer").innerHTML.includes("Keine Timeline verfügbar")) {
|
||||
this.showTemplatePreview();
|
||||
@@ -252,18 +278,24 @@ window.timelineEngine = {
|
||||
try {
|
||||
const csvPath = this.resolveProjectPath(cfg.dataSource);
|
||||
console.log("Attempting to fetch CSV from:", csvPath);
|
||||
const response = await fetch(csvPath);
|
||||
console.log("CSV fetch response:", response.status, response.statusText);
|
||||
const csvResponse = await fetch(csvPath);
|
||||
console.log("CSV fetch response:", csvResponse.status, csvResponse.statusText);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
if (!csvResponse.ok) {
|
||||
throw new Error(`HTTP ${csvResponse.status}: ${csvResponse.statusText}`);
|
||||
}
|
||||
|
||||
const csvText = await response.text();
|
||||
const csvText = await csvResponse.text();
|
||||
console.log("CSV text loaded, length:", csvText.length);
|
||||
console.log("CSV preview:", csvText.substring(0, 200));
|
||||
|
||||
this.updateFileStatus('csv', cfg.dataSource, 'loaded');
|
||||
|
||||
// Enable CSV edit button
|
||||
if (window.fileEditor) {
|
||||
window.fileEditor.enableEditButton('csv');
|
||||
}
|
||||
|
||||
this.processCsv(csvText);
|
||||
} catch (e) {
|
||||
console.error("CSV could not be loaded:", e);
|
||||
@@ -300,6 +332,9 @@ window.timelineEngine = {
|
||||
processCsv(text) {
|
||||
console.log("processCsv called with text length:", text?.length);
|
||||
|
||||
// Store CSV data for editing
|
||||
this.csvData = text;
|
||||
|
||||
if (!this.config || !this.config.fieldMapping) {
|
||||
console.error("No config or fieldMapping found.");
|
||||
return;
|
||||
@@ -321,6 +356,10 @@ window.timelineEngine = {
|
||||
complete: (res) => {
|
||||
console.log("Papa.parse complete, found", res.data.length, "rows");
|
||||
const rows = res.data;
|
||||
|
||||
// Show CSV preview in debug panel
|
||||
self.showCSVPreview(text, rows);
|
||||
|
||||
const items = rows.map((r) => {
|
||||
const dueField = (m.due || []).find(f => r[f]);
|
||||
const item = {
|
||||
@@ -350,7 +389,25 @@ window.timelineEngine = {
|
||||
|
||||
try {
|
||||
console.log("Generating timeline with:", items, self.config, self.template ? "template loaded" : "no template");
|
||||
const svg = window.timelineGenerator.generate(items, self.config, self.template);
|
||||
|
||||
// Auto-detect template type: prototype-based (new) or template-based (old)
|
||||
const isPrototypeBased = self.template && (
|
||||
self.template.includes('id="month-proto"') ||
|
||||
self.template.includes('id="lane-proto"') ||
|
||||
self.template.includes('id="item-proto"')
|
||||
);
|
||||
|
||||
let svg;
|
||||
if (isPrototypeBased && window.timelineGeneratorDOM) {
|
||||
console.log("Using DOM-based generator (prototype templates)");
|
||||
svg = window.timelineGeneratorDOM.generate(items, self.config, self.template);
|
||||
} else if (window.timelineGenerator) {
|
||||
console.log("Using string-based generator (template-v2)");
|
||||
svg = window.timelineGenerator.generate(items, self.config, self.template);
|
||||
} else {
|
||||
throw new Error("No timeline generator available");
|
||||
}
|
||||
|
||||
document.getElementById("viewer").innerHTML = svg;
|
||||
const dlBtn = document.getElementById("downloadSvg");
|
||||
dlBtn.disabled = false;
|
||||
@@ -366,7 +423,7 @@ window.timelineEngine = {
|
||||
} catch (error) {
|
||||
console.error("Error generating timeline:", error);
|
||||
document.getElementById("viewer").innerHTML =
|
||||
"<em style='color:#dc3545;'>Fehler beim Generieren der Timeline.</em>";
|
||||
"<em style='color:#dc3545;'>Fehler beim Generieren der Timeline: " + error.message + "</em>";
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -396,6 +453,132 @@ window.timelineEngine = {
|
||||
// Fallback
|
||||
d = new Date(str);
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
},
|
||||
|
||||
// --------- Debug Display Functions ---------
|
||||
|
||||
showFieldMappings() {
|
||||
if (!this.config || !this.config.fieldMapping) return;
|
||||
|
||||
const debugInfo = document.getElementById("debugInfo");
|
||||
const fieldMappingInfo = document.getElementById("fieldMappingInfo");
|
||||
const fieldMappingDisplay = document.getElementById("fieldMappingDisplay");
|
||||
|
||||
if (debugInfo && fieldMappingInfo && fieldMappingDisplay) {
|
||||
debugInfo.style.display = "block";
|
||||
fieldMappingInfo.style.display = "block";
|
||||
|
||||
const mapping = this.config.fieldMapping;
|
||||
let display = "CSV Column → Timeline Field\n";
|
||||
display += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
|
||||
display += `ID: ${JSON.stringify(mapping.id)}\n`;
|
||||
display += `Title: ${JSON.stringify(mapping.title)}\n`;
|
||||
display += `Lane: ${JSON.stringify(mapping.lane)}\n`;
|
||||
display += `Due: ${JSON.stringify(mapping.due)}\n`;
|
||||
if (mapping.epic) display += `Epic: ${JSON.stringify(mapping.epic)}\n`;
|
||||
if (mapping.type) display += `Type: ${JSON.stringify(mapping.type)}\n`;
|
||||
if (mapping.color) display += `Color: ${JSON.stringify(mapping.color)}\n`;
|
||||
|
||||
fieldMappingDisplay.textContent = display;
|
||||
}
|
||||
},
|
||||
|
||||
showTemplateFields() {
|
||||
if (!this.template) return;
|
||||
|
||||
const debugInfo = document.getElementById("debugInfo");
|
||||
const templateFieldsInfo = document.getElementById("templateFieldsInfo");
|
||||
const templateFieldsDisplay = document.getElementById("templateFieldsDisplay");
|
||||
|
||||
if (debugInfo && templateFieldsInfo && templateFieldsDisplay) {
|
||||
debugInfo.style.display = "block";
|
||||
templateFieldsInfo.style.display = "block";
|
||||
|
||||
// Extract placeholders from template
|
||||
const placeholders = new Set();
|
||||
const placeholderRegex = /\{\{([A-Z_]+)\}\}/g;
|
||||
let match;
|
||||
while ((match = placeholderRegex.exec(this.template)) !== null) {
|
||||
placeholders.add(match[1]);
|
||||
}
|
||||
|
||||
let display = "Required Template Placeholders:\n";
|
||||
display += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
|
||||
|
||||
// Group by category
|
||||
const monthFields = Array.from(placeholders).filter(p => p.includes('MONTH'));
|
||||
const laneFields = Array.from(placeholders).filter(p => p.includes('LANE'));
|
||||
const taskFields = Array.from(placeholders).filter(p => p.includes('TASK') || p.includes('TEXT'));
|
||||
const otherFields = Array.from(placeholders).filter(p =>
|
||||
!monthFields.includes(p) && !laneFields.includes(p) && !taskFields.includes(p)
|
||||
);
|
||||
|
||||
if (monthFields.length > 0) {
|
||||
display += "\nMonth Fields:\n";
|
||||
monthFields.forEach(f => display += ` • ${f}\n`);
|
||||
}
|
||||
if (laneFields.length > 0) {
|
||||
display += "\nLane Fields:\n";
|
||||
laneFields.forEach(f => display += ` • ${f}\n`);
|
||||
}
|
||||
if (taskFields.length > 0) {
|
||||
display += "\nTask Fields:\n";
|
||||
taskFields.forEach(f => display += ` • ${f}\n`);
|
||||
}
|
||||
if (otherFields.length > 0) {
|
||||
display += "\nOther Fields:\n";
|
||||
otherFields.forEach(f => display += ` • ${f}\n`);
|
||||
}
|
||||
|
||||
templateFieldsDisplay.textContent = display;
|
||||
}
|
||||
},
|
||||
|
||||
showCSVPreview(csvText, parsedData) {
|
||||
const debugInfo = document.getElementById("debugInfo");
|
||||
const csvDataInfo = document.getElementById("csvDataInfo");
|
||||
const csvDataDisplay = document.getElementById("csvDataDisplay");
|
||||
|
||||
if (debugInfo && csvDataInfo && csvDataDisplay) {
|
||||
debugInfo.style.display = "block";
|
||||
csvDataInfo.style.display = "block";
|
||||
|
||||
const lines = csvText.trim().split('\n');
|
||||
const headers = lines[0] ? lines[0].split(',') : [];
|
||||
const firstDataLine = lines[1] ? lines[1].split(',') : [];
|
||||
|
||||
let display = "CSV Structure Preview:\n";
|
||||
display += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
|
||||
display += `Headers: ${headers.join(', ')}\n\n`;
|
||||
|
||||
if (firstDataLine.length > 0) {
|
||||
display += "First Data Row:\n";
|
||||
headers.forEach((header, i) => {
|
||||
display += ` ${header}: "${firstDataLine[i] || ''}"\n`;
|
||||
});
|
||||
}
|
||||
|
||||
if (parsedData) {
|
||||
display += `\nParsed Rows: ${parsedData.length}\n`;
|
||||
const validCount = parsedData.filter(r => {
|
||||
const mapping = this.config?.fieldMapping || {};
|
||||
const titleField = mapping.title;
|
||||
const dueField = Array.isArray(mapping.due) ? mapping.due.find(f => r[f]) : mapping.due;
|
||||
return r[titleField] && r[dueField];
|
||||
}).length;
|
||||
display += `Valid Items: ${validCount}\n`;
|
||||
|
||||
if (validCount === 0 && parsedData.length > 0) {
|
||||
display += "\n⚠️ WARNING: No valid items found!\n";
|
||||
display += "Check that:\n";
|
||||
display += " • CSV headers match field mappings\n";
|
||||
display += " • Title and Due fields have values\n";
|
||||
display += " • Date format is parseable (e.g., YYYY-MM-DD)\n";
|
||||
}
|
||||
}
|
||||
|
||||
csvDataDisplay.textContent = display;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -532,88 +715,134 @@ window.svgViewer = {
|
||||
// --------- UI event handlers ---------
|
||||
|
||||
window.setupEventHandlers = function() {
|
||||
const projectInput = document.getElementById("projectInput");
|
||||
if (projectInput) {
|
||||
projectInput.addEventListener("change", async (ev) => {
|
||||
const file = ev.target.files[0];
|
||||
if (!file) return;
|
||||
// Handler for loading entire project folder
|
||||
const folderInput = document.getElementById("folderInput");
|
||||
if (folderInput) {
|
||||
folderInput.addEventListener("change", async (ev) => {
|
||||
const files = Array.from(ev.target.files);
|
||||
if (!files.length) return;
|
||||
|
||||
console.log("Folder selected with", files.length, "files");
|
||||
|
||||
try {
|
||||
const text = await file.text();
|
||||
const cfg = JSON.parse(text);
|
||||
// Find project.json in the uploaded files
|
||||
const projectFile = files.find(f => f.name === 'project.json');
|
||||
if (!projectFile) {
|
||||
alert('No project.json found in selected folder. Please select a folder containing a project.json file.');
|
||||
return;
|
||||
}
|
||||
|
||||
// For manually loaded projects, try to infer base path from filename
|
||||
// If it's example/project.json or binect/project.json, set appropriate base path
|
||||
const filename = file.name;
|
||||
if (filename === 'project.json') {
|
||||
// Try to detect if this is a known project by checking the config
|
||||
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/");
|
||||
// Parse project.json
|
||||
const projectText = await projectFile.text();
|
||||
const cfg = JSON.parse(projectText);
|
||||
console.log("Loaded project configuration:", cfg);
|
||||
|
||||
// Clear projectBasePath since we're loading from uploaded files
|
||||
window.timelineEngine.projectBasePath = '';
|
||||
|
||||
// Update project status
|
||||
window.timelineEngine.updateFileStatus('project', projectFile.name, 'loaded');
|
||||
|
||||
// Enable project edit button
|
||||
if (window.fileEditor) {
|
||||
window.fileEditor.enableEditButton('project');
|
||||
}
|
||||
|
||||
// Load referenced files from the folder
|
||||
const errors = [];
|
||||
|
||||
// Load stylesheet
|
||||
if (cfg.stylesheet) {
|
||||
const cssFile = files.find(f => f.name === cfg.stylesheet || f.webkitRelativePath.endsWith(cfg.stylesheet));
|
||||
if (cssFile) {
|
||||
const cssText = await cssFile.text();
|
||||
window.timelineEngine.cssData = cssText; // Store for editing
|
||||
window.timelineEngine.cssOverride = true;
|
||||
const blob = new Blob([cssText], { type: "text/css" });
|
||||
document.getElementById("dynamicCss").href = URL.createObjectURL(blob);
|
||||
window.timelineEngine.updateFileStatus('css', cssFile.name, 'loaded');
|
||||
|
||||
// Enable CSS edit button
|
||||
if (window.fileEditor) {
|
||||
window.fileEditor.enableEditButton('css');
|
||||
}
|
||||
|
||||
console.log("Loaded stylesheet:", cssFile.name);
|
||||
} else {
|
||||
window.timelineEngine.projectBasePath = '';
|
||||
console.log("Unknown project, clearing base path");
|
||||
errors.push(`Stylesheet: ${cfg.stylesheet}`);
|
||||
window.timelineEngine.updateFileStatus('css', cfg.stylesheet, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Load SVG template
|
||||
if (cfg.svgTemplate) {
|
||||
const svgFile = files.find(f => f.name === cfg.svgTemplate || f.webkitRelativePath.endsWith(cfg.svgTemplate));
|
||||
if (svgFile) {
|
||||
window.timelineEngine.template = await svgFile.text();
|
||||
window.timelineEngine.updateFileStatus('svg', svgFile.name, 'loaded');
|
||||
window.timelineEngine.showTemplateFields();
|
||||
|
||||
// Enable SVG edit button
|
||||
if (window.fileEditor) {
|
||||
window.fileEditor.enableEditButton('svg');
|
||||
}
|
||||
|
||||
console.log("Loaded SVG template:", svgFile.name);
|
||||
} else {
|
||||
errors.push(`SVG template: ${cfg.svgTemplate}`);
|
||||
window.timelineEngine.updateFileStatus('svg', cfg.svgTemplate, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Load CSV data
|
||||
if (cfg.dataSource) {
|
||||
const csvFile = files.find(f => f.name === cfg.dataSource || f.webkitRelativePath.endsWith(cfg.dataSource));
|
||||
if (csvFile) {
|
||||
const csvText = await csvFile.text();
|
||||
window.timelineEngine.csvOverride = true;
|
||||
window.timelineEngine.updateFileStatus('csv', csvFile.name, 'loaded');
|
||||
|
||||
// Enable CSV edit button
|
||||
if (window.fileEditor) {
|
||||
window.fileEditor.enableEditButton('csv');
|
||||
}
|
||||
|
||||
console.log("Loaded CSV data:", csvFile.name);
|
||||
|
||||
// Set config and process CSV
|
||||
window.timelineEngine.config = cfg;
|
||||
window.timelineEngine.processCsv(csvText);
|
||||
} else {
|
||||
errors.push(`CSV data: ${cfg.dataSource}`);
|
||||
window.timelineEngine.updateFileStatus('csv', cfg.dataSource, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Update project name and description
|
||||
const name = cfg.name || "Timeline";
|
||||
document.getElementById("projectName").textContent = name;
|
||||
document.getElementById("projectSubtitle").textContent = cfg.description || "Project loaded from folder.";
|
||||
|
||||
// Show field mappings
|
||||
window.timelineEngine.showFieldMappings();
|
||||
|
||||
// Show any errors
|
||||
if (errors.length > 0) {
|
||||
setTimeout(() => {
|
||||
alert(`⚠️ Some files could not be loaded:\n\n${errors.join('\n')}\n\nMake sure all referenced files are in the selected folder.`);
|
||||
}, 500);
|
||||
} else {
|
||||
window.timelineEngine.projectBasePath = '';
|
||||
console.log("✅ All project files loaded successfully from folder");
|
||||
}
|
||||
|
||||
window.timelineEngine.updateFileStatus('project', file.name, 'loaded');
|
||||
await window.timelineEngine.loadProjectConfigObject(cfg);
|
||||
|
||||
// Show message about relative paths if project has data sources
|
||||
if (cfg.dataSource || cfg.stylesheet || cfg.svgTemplate) {
|
||||
const viewer = document.getElementById("viewer");
|
||||
if (viewer && (viewer.innerHTML.includes("could not be loaded") || viewer.innerHTML.includes("Keine gültigen Items"))) {
|
||||
viewer.innerHTML += "<br><br><em style='color:#6c757d; font-size:12px;'>💡 Hinweis: Stelle sicher, dass sich die referenzierten Dateien (CSV, CSS, SVG) im gleichen Verzeichnis wie die HTML-Datei befinden.</em>";
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading project:", error);
|
||||
window.timelineEngine.updateFileStatus('project', file.name, 'error');
|
||||
console.error("Error loading project folder:", error);
|
||||
alert(`Error loading project: ${error.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const csvInput = document.getElementById("csvInput");
|
||||
if (csvInput) {
|
||||
csvInput.addEventListener("change", async (ev) => {
|
||||
const file = ev.target.files[0];
|
||||
if (!file) return;
|
||||
const text = await file.text();
|
||||
window.timelineEngine.csvOverride = true;
|
||||
window.timelineEngine.updateFileStatus('csv', file.name, 'loaded');
|
||||
window.timelineEngine.processCsv(text);
|
||||
});
|
||||
}
|
||||
|
||||
const cssInput = document.getElementById("cssInput");
|
||||
if (cssInput) {
|
||||
cssInput.addEventListener("change", async (ev) => {
|
||||
const file = ev.target.files[0];
|
||||
if (!file) return;
|
||||
const cssText = await file.text();
|
||||
window.timelineEngine.cssOverride = true;
|
||||
const blob = new Blob([cssText], { type: "text/css" });
|
||||
document.getElementById("dynamicCss").href = URL.createObjectURL(blob);
|
||||
window.timelineEngine.updateFileStatus('css', file.name, 'loaded');
|
||||
});
|
||||
}
|
||||
|
||||
const svgInput = document.getElementById("svgInput");
|
||||
if (svgInput) {
|
||||
svgInput.addEventListener("change", async (ev) => {
|
||||
const file = ev.target.files[0];
|
||||
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();
|
||||
});
|
||||
}
|
||||
// Individual file inputs removed - use folder picker instead
|
||||
|
||||
const downloadSvg = document.getElementById("downloadSvg");
|
||||
if (downloadSvg) {
|
||||
|
||||
379
example-1/output.svg
Normal file
@@ -0,0 +1,379 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="background: #ffffff;" width="1880" height="508" viewBox="0 0 1880 508">
|
||||
<defs>
|
||||
<!-- Clean, minimal month markers -->
|
||||
|
||||
|
||||
<!-- Central timeline bar -->
|
||||
|
||||
|
||||
<!-- Item with anchor line -->
|
||||
|
||||
</defs>
|
||||
|
||||
<!-- Clean white background -->
|
||||
<rect width="100%" height="100%" fill="#ffffff"/>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="40" y="40" font-family="Arial, sans-serif" font-size="24"
|
||||
font-weight="300" fill="#333">Timeline View</text>
|
||||
<text x="40" y="65" font-family="Arial, sans-serif" font-size="13"
|
||||
fill="#999">Clean timeline visualization without swimlanes</text>
|
||||
|
||||
<!-- Timeline label -->
|
||||
<text x="40" y="190" font-family="Arial, sans-serif" font-size="12"
|
||||
font-weight="600" fill="#666" text-anchor="end" dominant-baseline="middle">Timeline</text>
|
||||
|
||||
<!-- Month markers -->
|
||||
<g class="months">
|
||||
<g >
|
||||
<!-- Month separator line -->
|
||||
<line x1="220" y1="180" x2="220" y2="200"
|
||||
stroke="#d0d0d0" stroke-width="1"/>
|
||||
<!-- Month label -->
|
||||
<text x="224" y="220"
|
||||
font-family="Arial, sans-serif" font-size="11"
|
||||
fill="#666" text-anchor="start">Jan. 25</text>
|
||||
</g><g >
|
||||
<!-- Month separator line -->
|
||||
<line x1="340" y1="180" x2="340" y2="200"
|
||||
stroke="#d0d0d0" stroke-width="1"/>
|
||||
<!-- Month label -->
|
||||
<text x="344" y="220"
|
||||
font-family="Arial, sans-serif" font-size="11"
|
||||
fill="#666" text-anchor="start">Feb. 25</text>
|
||||
</g><g >
|
||||
<!-- Month separator line -->
|
||||
<line x1="460" y1="180" x2="460" y2="200"
|
||||
stroke="#d0d0d0" stroke-width="1"/>
|
||||
<!-- Month label -->
|
||||
<text x="464" y="220"
|
||||
font-family="Arial, sans-serif" font-size="11"
|
||||
fill="#666" text-anchor="start">März 25</text>
|
||||
</g><g >
|
||||
<!-- Month separator line -->
|
||||
<line x1="580" y1="180" x2="580" y2="200"
|
||||
stroke="#d0d0d0" stroke-width="1"/>
|
||||
<!-- Month label -->
|
||||
<text x="584" y="220"
|
||||
font-family="Arial, sans-serif" font-size="11"
|
||||
fill="#666" text-anchor="start">Apr. 25</text>
|
||||
</g><g >
|
||||
<!-- Month separator line -->
|
||||
<line x1="700" y1="180" x2="700" y2="200"
|
||||
stroke="#d0d0d0" stroke-width="1"/>
|
||||
<!-- Month label -->
|
||||
<text x="704" y="220"
|
||||
font-family="Arial, sans-serif" font-size="11"
|
||||
fill="#666" text-anchor="start">Mai 25</text>
|
||||
</g><g >
|
||||
<!-- Month separator line -->
|
||||
<line x1="820" y1="180" x2="820" y2="200"
|
||||
stroke="#d0d0d0" stroke-width="1"/>
|
||||
<!-- Month label -->
|
||||
<text x="824" y="220"
|
||||
font-family="Arial, sans-serif" font-size="11"
|
||||
fill="#666" text-anchor="start">Juni 25</text>
|
||||
</g><g >
|
||||
<!-- Month separator line -->
|
||||
<line x1="940" y1="180" x2="940" y2="200"
|
||||
stroke="#d0d0d0" stroke-width="1"/>
|
||||
<!-- Month label -->
|
||||
<text x="944" y="220"
|
||||
font-family="Arial, sans-serif" font-size="11"
|
||||
fill="#666" text-anchor="start">Juli 25</text>
|
||||
</g><g >
|
||||
<!-- Month separator line -->
|
||||
<line x1="1060" y1="180" x2="1060" y2="200"
|
||||
stroke="#d0d0d0" stroke-width="1"/>
|
||||
<!-- Month label -->
|
||||
<text x="1064" y="220"
|
||||
font-family="Arial, sans-serif" font-size="11"
|
||||
fill="#666" text-anchor="start">Aug. 25</text>
|
||||
</g><g >
|
||||
<!-- Month separator line -->
|
||||
<line x1="1180" y1="180" x2="1180" y2="200"
|
||||
stroke="#d0d0d0" stroke-width="1"/>
|
||||
<!-- Month label -->
|
||||
<text x="1184" y="220"
|
||||
font-family="Arial, sans-serif" font-size="11"
|
||||
fill="#666" text-anchor="start">Sept. 25</text>
|
||||
</g><g >
|
||||
<!-- Month separator line -->
|
||||
<line x1="1300" y1="180" x2="1300" y2="200"
|
||||
stroke="#d0d0d0" stroke-width="1"/>
|
||||
<!-- Month label -->
|
||||
<text x="1304" y="220"
|
||||
font-family="Arial, sans-serif" font-size="11"
|
||||
fill="#666" text-anchor="start">Okt. 25</text>
|
||||
</g><g >
|
||||
<!-- Month separator line -->
|
||||
<line x1="1420" y1="180" x2="1420" y2="200"
|
||||
stroke="#d0d0d0" stroke-width="1"/>
|
||||
<!-- Month label -->
|
||||
<text x="1424" y="220"
|
||||
font-family="Arial, sans-serif" font-size="11"
|
||||
fill="#666" text-anchor="start">Nov. 25</text>
|
||||
</g><g >
|
||||
<!-- Month separator line -->
|
||||
<line x1="1540" y1="180" x2="1540" y2="200"
|
||||
stroke="#d0d0d0" stroke-width="1"/>
|
||||
<!-- Month label -->
|
||||
<text x="1544" y="220"
|
||||
font-family="Arial, sans-serif" font-size="11"
|
||||
fill="#666" text-anchor="start">Dez. 25</text>
|
||||
</g><g >
|
||||
<!-- Month separator line -->
|
||||
<line x1="1660" y1="180" x2="1660" y2="200"
|
||||
stroke="#d0d0d0" stroke-width="1"/>
|
||||
<!-- Month label -->
|
||||
<text x="1664" y="220"
|
||||
font-family="Arial, sans-serif" font-size="11"
|
||||
fill="#666" text-anchor="start">Jan. 26</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Timeline bar and items -->
|
||||
<g class="timeline-content">
|
||||
<g >
|
||||
<!-- Main timeline horizontal line -->
|
||||
<line x1="40" y1="190" x2="1780" y2="190"
|
||||
stroke="#333" stroke-width="3" stroke-linecap="round"/>
|
||||
</g><g >
|
||||
<!-- Vertical anchor line from timeline to item -->
|
||||
<line x1="280" y1="190" x2="280" y2="150"
|
||||
stroke="#999" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
|
||||
<!-- Circle marker at item position -->
|
||||
<circle cx="280" cy="150" r="4"
|
||||
fill="#4a90e2" stroke="#fff" stroke-width="2"/>
|
||||
|
||||
<!-- Item text -->
|
||||
<text x="280" y="150"
|
||||
font-family="Arial, sans-serif" font-size="12"
|
||||
fill="#333" text-anchor="middle" dy="-10">
|
||||
<tspan font-weight="600" fill="#4a90e2">D1</tspan>
|
||||
<tspan dx="4">Research Phase</tspan>
|
||||
</text>
|
||||
</g><g >
|
||||
<!-- Vertical anchor line from timeline to item -->
|
||||
<line x1="520" y1="190" x2="520" y2="168"
|
||||
stroke="#999" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
|
||||
<!-- Circle marker at item position -->
|
||||
<circle cx="520" cy="168" r="4"
|
||||
fill="#4a90e2" stroke="#fff" stroke-width="2"/>
|
||||
|
||||
<!-- Item text -->
|
||||
<text x="520" y="168"
|
||||
font-family="Arial, sans-serif" font-size="12"
|
||||
fill="#333" text-anchor="middle" dy="-10">
|
||||
<tspan font-weight="600" fill="#4a90e2">D2</tspan>
|
||||
<tspan dx="4">Prototype Development</tspan>
|
||||
</text>
|
||||
</g><g >
|
||||
<!-- Vertical anchor line from timeline to item -->
|
||||
<line x1="760" y1="190" x2="760" y2="186"
|
||||
stroke="#999" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
|
||||
<!-- Circle marker at item position -->
|
||||
<circle cx="760" cy="186" r="4"
|
||||
fill="#4a90e2" stroke="#fff" stroke-width="2"/>
|
||||
|
||||
<!-- Item text -->
|
||||
<text x="760" y="186"
|
||||
font-family="Arial, sans-serif" font-size="12"
|
||||
fill="#333" text-anchor="middle" dy="-10">
|
||||
<tspan font-weight="600" fill="#4a90e2">D3</tspan>
|
||||
<tspan dx="4">Core Features</tspan>
|
||||
</text>
|
||||
</g><g >
|
||||
<!-- Vertical anchor line from timeline to item -->
|
||||
<line x1="1000" y1="190" x2="1000" y2="204"
|
||||
stroke="#999" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
|
||||
<!-- Circle marker at item position -->
|
||||
<circle cx="1000" cy="204" r="4"
|
||||
fill="#4a90e2" stroke="#fff" stroke-width="2"/>
|
||||
|
||||
<!-- Item text -->
|
||||
<text x="1000" y="204"
|
||||
font-family="Arial, sans-serif" font-size="12"
|
||||
fill="#333" text-anchor="middle" dy="-10">
|
||||
<tspan font-weight="600" fill="#4a90e2">D4</tspan>
|
||||
<tspan dx="4">Integration Testing</tspan>
|
||||
</text>
|
||||
</g><g >
|
||||
<!-- Vertical anchor line from timeline to item -->
|
||||
<line x1="1120" y1="190" x2="1120" y2="222"
|
||||
stroke="#999" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
|
||||
<!-- Circle marker at item position -->
|
||||
<circle cx="1120" cy="222" r="4"
|
||||
fill="#4a90e2" stroke="#fff" stroke-width="2"/>
|
||||
|
||||
<!-- Item text -->
|
||||
<text x="1120" y="222"
|
||||
font-family="Arial, sans-serif" font-size="12"
|
||||
fill="#333" text-anchor="middle" dy="-10">
|
||||
<tspan font-weight="600" fill="#4a90e2">D5</tspan>
|
||||
<tspan dx="4">Performance Optimization</tspan>
|
||||
</text>
|
||||
</g><g >
|
||||
<!-- Main timeline horizontal line -->
|
||||
<line x1="40" y1="190" x2="1780" y2="190"
|
||||
stroke="#333" stroke-width="3" stroke-linecap="round"/>
|
||||
</g><g >
|
||||
<!-- Vertical anchor line from timeline to item -->
|
||||
<line x1="280" y1="190" x2="280" y2="246"
|
||||
stroke="#999" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
|
||||
<!-- Circle marker at item position -->
|
||||
<circle cx="280" cy="246" r="4"
|
||||
fill="#4a90e2" stroke="#fff" stroke-width="2"/>
|
||||
|
||||
<!-- Item text -->
|
||||
<text x="280" y="246"
|
||||
font-family="Arial, sans-serif" font-size="12"
|
||||
fill="#333" text-anchor="middle" dy="-10">
|
||||
<tspan font-weight="600" fill="#4a90e2">M1</tspan>
|
||||
<tspan dx="4">Project Kickoff</tspan>
|
||||
</text>
|
||||
</g><g >
|
||||
<!-- Vertical anchor line from timeline to item -->
|
||||
<line x1="400" y1="190" x2="400" y2="264"
|
||||
stroke="#999" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
|
||||
<!-- Circle marker at item position -->
|
||||
<circle cx="400" cy="264" r="4"
|
||||
fill="#4a90e2" stroke="#fff" stroke-width="2"/>
|
||||
|
||||
<!-- Item text -->
|
||||
<text x="400" y="264"
|
||||
font-family="Arial, sans-serif" font-size="12"
|
||||
fill="#333" text-anchor="middle" dy="-10">
|
||||
<tspan font-weight="600" fill="#4a90e2">M2</tspan>
|
||||
<tspan dx="4">Requirements Complete</tspan>
|
||||
</text>
|
||||
</g><g >
|
||||
<!-- Vertical anchor line from timeline to item -->
|
||||
<line x1="640" y1="190" x2="640" y2="282"
|
||||
stroke="#999" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
|
||||
<!-- Circle marker at item position -->
|
||||
<circle cx="640" cy="282" r="4"
|
||||
fill="#4a90e2" stroke="#fff" stroke-width="2"/>
|
||||
|
||||
<!-- Item text -->
|
||||
<text x="640" y="282"
|
||||
font-family="Arial, sans-serif" font-size="12"
|
||||
fill="#333" text-anchor="middle" dy="-10">
|
||||
<tspan font-weight="600" fill="#4a90e2">M3</tspan>
|
||||
<tspan dx="4">Design Review</tspan>
|
||||
</text>
|
||||
</g><g >
|
||||
<!-- Vertical anchor line from timeline to item -->
|
||||
<line x1="880" y1="190" x2="880" y2="300"
|
||||
stroke="#999" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
|
||||
<!-- Circle marker at item position -->
|
||||
<circle cx="880" cy="300" r="4"
|
||||
fill="#4a90e2" stroke="#fff" stroke-width="2"/>
|
||||
|
||||
<!-- Item text -->
|
||||
<text x="880" y="300"
|
||||
font-family="Arial, sans-serif" font-size="12"
|
||||
fill="#333" text-anchor="middle" dy="-10">
|
||||
<tspan font-weight="600" fill="#4a90e2">M4</tspan>
|
||||
<tspan dx="4">Beta Release</tspan>
|
||||
</text>
|
||||
</g><g >
|
||||
<!-- Vertical anchor line from timeline to item -->
|
||||
<line x1="1240" y1="190" x2="1240" y2="318"
|
||||
stroke="#999" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
|
||||
<!-- Circle marker at item position -->
|
||||
<circle cx="1240" cy="318" r="4"
|
||||
fill="#4a90e2" stroke="#fff" stroke-width="2"/>
|
||||
|
||||
<!-- Item text -->
|
||||
<text x="1240" y="318"
|
||||
font-family="Arial, sans-serif" font-size="12"
|
||||
fill="#333" text-anchor="middle" dy="-10">
|
||||
<tspan font-weight="600" fill="#4a90e2">M5</tspan>
|
||||
<tspan dx="4">Launch</tspan>
|
||||
</text>
|
||||
</g><g >
|
||||
<!-- Main timeline horizontal line -->
|
||||
<line x1="40" y1="190" x2="1780" y2="190"
|
||||
stroke="#333" stroke-width="3" stroke-linecap="round"/>
|
||||
</g><g >
|
||||
<!-- Vertical anchor line from timeline to item -->
|
||||
<line x1="400" y1="190" x2="400" y2="342"
|
||||
stroke="#999" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
|
||||
<!-- Circle marker at item position -->
|
||||
<circle cx="400" cy="342" r="4"
|
||||
fill="#4a90e2" stroke="#fff" stroke-width="2"/>
|
||||
|
||||
<!-- Item text -->
|
||||
<text x="400" y="342"
|
||||
font-family="Arial, sans-serif" font-size="12"
|
||||
fill="#333" text-anchor="middle" dy="-10">
|
||||
<tspan font-weight="600" fill="#4a90e2">R1</tspan>
|
||||
<tspan dx="4">User Feedback Round 1</tspan>
|
||||
</text>
|
||||
</g><g >
|
||||
<!-- Vertical anchor line from timeline to item -->
|
||||
<line x1="640" y1="190" x2="640" y2="360"
|
||||
stroke="#999" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
|
||||
<!-- Circle marker at item position -->
|
||||
<circle cx="640" cy="360" r="4"
|
||||
fill="#4a90e2" stroke="#fff" stroke-width="2"/>
|
||||
|
||||
<!-- Item text -->
|
||||
<text x="640" y="360"
|
||||
font-family="Arial, sans-serif" font-size="12"
|
||||
fill="#333" text-anchor="middle" dy="-10">
|
||||
<tspan font-weight="600" fill="#4a90e2">R2</tspan>
|
||||
<tspan dx="4">Market Analysis</tspan>
|
||||
</text>
|
||||
</g><g >
|
||||
<!-- Vertical anchor line from timeline to item -->
|
||||
<line x1="880" y1="190" x2="880" y2="378"
|
||||
stroke="#999" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
|
||||
<!-- Circle marker at item position -->
|
||||
<circle cx="880" cy="378" r="4"
|
||||
fill="#4a90e2" stroke="#fff" stroke-width="2"/>
|
||||
|
||||
<!-- Item text -->
|
||||
<text x="880" y="378"
|
||||
font-family="Arial, sans-serif" font-size="12"
|
||||
fill="#333" text-anchor="middle" dy="-10">
|
||||
<tspan font-weight="600" fill="#4a90e2">R3</tspan>
|
||||
<tspan dx="4">User Feedback Round 2</tspan>
|
||||
</text>
|
||||
</g><g >
|
||||
<!-- Vertical anchor line from timeline to item -->
|
||||
<line x1="1120" y1="190" x2="1120" y2="396"
|
||||
stroke="#999" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
|
||||
<!-- Circle marker at item position -->
|
||||
<circle cx="1120" cy="396" r="4"
|
||||
fill="#4a90e2" stroke="#fff" stroke-width="2"/>
|
||||
|
||||
<!-- Item text -->
|
||||
<text x="1120" y="396"
|
||||
font-family="Arial, sans-serif" font-size="12"
|
||||
fill="#333" text-anchor="middle" dy="-10">
|
||||
<tspan font-weight="600" fill="#4a90e2">R4</tspan>
|
||||
<tspan dx="4">Final User Testing</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Subtle border -->
|
||||
<rect x="2" y="2" width="calc(100% - 4)" height="calc(100% - 4)"
|
||||
fill="none" stroke="#e0e0e0" stroke-width="1" rx="4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
16
example-1/project.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "Clean Timeline Example",
|
||||
"description": "Minimalist timeline visualization without swimlanes, using vertical anchor lines to connect items to a central timeline bar",
|
||||
"dataSource": "sample.csv",
|
||||
"stylesheet": "style.css",
|
||||
"svgTemplate": "template-v2.svg",
|
||||
"settings": {
|
||||
"timelineMonths": 12
|
||||
},
|
||||
"fieldMapping": {
|
||||
"id": "ID",
|
||||
"title": "Title",
|
||||
"lane": "Category",
|
||||
"due": ["Due Date"]
|
||||
}
|
||||
}
|
||||
15
example-1/sample.csv
Normal file
@@ -0,0 +1,15 @@
|
||||
ID,Title,Category,Due Date
|
||||
M1,Project Kickoff,Milestones,2025-01-15
|
||||
M2,Requirements Complete,Milestones,2025-02-28
|
||||
M3,Design Review,Milestones,2025-04-15
|
||||
M4,Beta Release,Milestones,2025-06-30
|
||||
M5,Launch,Milestones,2025-09-01
|
||||
D1,Research Phase,Development,2025-01-20
|
||||
D2,Prototype Development,Development,2025-03-10
|
||||
D3,Core Features,Development,2025-05-15
|
||||
D4,Integration Testing,Development,2025-07-20
|
||||
D5,Performance Optimization,Development,2025-08-25
|
||||
R1,User Feedback Round 1,Research,2025-02-15
|
||||
R2,Market Analysis,Research,2025-04-01
|
||||
R3,User Feedback Round 2,Research,2025-06-15
|
||||
R4,Final User Testing,Research,2025-08-15
|
||||
|
73
example-1/style.css
Normal file
@@ -0,0 +1,73 @@
|
||||
/* Clean Timeline Example - Minimal Styling */
|
||||
|
||||
body {
|
||||
font-family: 'Arial', 'Helvetica', sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#svgContainer {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
margin: 20px auto;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* SVG styling enhancements */
|
||||
svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Timeline elements */
|
||||
.months text {
|
||||
font-family: 'Arial', sans-serif;
|
||||
font-size: 11px;
|
||||
fill: #666;
|
||||
}
|
||||
|
||||
.timeline-content line {
|
||||
stroke: #333;
|
||||
}
|
||||
|
||||
.timeline-content circle {
|
||||
cursor: pointer;
|
||||
transition: r 0.2s ease;
|
||||
}
|
||||
|
||||
.timeline-content circle:hover {
|
||||
r: 6;
|
||||
}
|
||||
|
||||
.timeline-content text {
|
||||
font-family: 'Arial', sans-serif;
|
||||
font-size: 12px;
|
||||
fill: #333;
|
||||
}
|
||||
|
||||
.item-id {
|
||||
font-weight: 600;
|
||||
fill: #4a90e2;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
fill: #333;
|
||||
}
|
||||
|
||||
/* Responsive container */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#svgContainer {
|
||||
padding: 10px;
|
||||
margin: 10px auto;
|
||||
}
|
||||
}
|
||||
67
example-1/template-v2.svg
Normal file
@@ -0,0 +1,67 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="background: #ffffff;">
|
||||
<defs>
|
||||
<!-- Clean, minimal month markers -->
|
||||
<g id="month-template" style="display: none;">
|
||||
<!-- Month separator line -->
|
||||
<line x1="{{MONTH_X}}" y1="180" x2="{{MONTH_X}}" y2="200"
|
||||
stroke="#d0d0d0" stroke-width="1"/>
|
||||
<!-- Month label -->
|
||||
<text x="{{MONTH_TEXT_X}}" y="220"
|
||||
font-family="Arial, sans-serif" font-size="11"
|
||||
fill="#666" text-anchor="start">{{MONTH_LABEL}}</text>
|
||||
</g>
|
||||
|
||||
<!-- Central timeline bar -->
|
||||
<g id="lane-template" style="display: none;">
|
||||
<!-- Main timeline horizontal line -->
|
||||
<line x1="{{LANE_X}}" y1="190" x2="{{LANE_WIDTH}}" y2="190"
|
||||
stroke="#333" stroke-width="3" stroke-linecap="round"/>
|
||||
</g>
|
||||
|
||||
<!-- Item with anchor line -->
|
||||
<g id="item-template" style="display: none;">
|
||||
<!-- Vertical anchor line from timeline to item -->
|
||||
<line x1="{{ITEM_X}}" y1="190" x2="{{ITEM_X}}" y2="{{ITEM_Y}}"
|
||||
stroke="#999" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
|
||||
<!-- Circle marker at item position -->
|
||||
<circle cx="{{ITEM_X}}" cy="{{ITEM_Y}}" r="4"
|
||||
fill="#4a90e2" stroke="#fff" stroke-width="2"/>
|
||||
|
||||
<!-- Item text -->
|
||||
<text x="{{ITEM_X}}" y="{{ITEM_Y}}"
|
||||
font-family="Arial, sans-serif" font-size="12"
|
||||
fill="#333" text-anchor="middle" dy="-10">
|
||||
<tspan font-weight="600" fill="#4a90e2">{{ITEM_ID}}</tspan>
|
||||
<tspan dx="4">{{ITEM_TITLE}}</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</defs>
|
||||
|
||||
<!-- Clean white background -->
|
||||
<rect width="100%" height="100%" fill="#ffffff"/>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="40" y="40" font-family="Arial, sans-serif" font-size="24"
|
||||
font-weight="300" fill="#333">Timeline View</text>
|
||||
<text x="40" y="65" font-family="Arial, sans-serif" font-size="13"
|
||||
fill="#999">Clean timeline visualization without swimlanes</text>
|
||||
|
||||
<!-- Timeline label -->
|
||||
<text x="40" y="190" font-family="Arial, sans-serif" font-size="12"
|
||||
font-weight="600" fill="#666" text-anchor="end" dominant-baseline="middle">Timeline</text>
|
||||
|
||||
<!-- Month markers -->
|
||||
<g class="months">
|
||||
{{MONTHS}}
|
||||
</g>
|
||||
|
||||
<!-- Timeline bar and items -->
|
||||
<g class="timeline-content">
|
||||
{{LANES}}
|
||||
</g>
|
||||
|
||||
<!-- Subtle border -->
|
||||
<rect x="2" y="2" width="calc(100% - 4)" height="calc(100% - 4)"
|
||||
fill="none" stroke="#e0e0e0" stroke-width="1" rx="4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
21
example-proto/project.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Prototype Template Example",
|
||||
"description": "Example using DOM-based prototype templates - fully editable in Inkscape!",
|
||||
"dataSource": "sample.csv",
|
||||
"stylesheet": "style.css",
|
||||
"svgTemplate": "template-proto.svg",
|
||||
"settings": {
|
||||
"timelineMonths": 18,
|
||||
"marginLeft": 220,
|
||||
"marginTop": 140,
|
||||
"monthWidth": 120,
|
||||
"laneHeight": 80,
|
||||
"laneGap": 16
|
||||
},
|
||||
"fieldMapping": {
|
||||
"id": "ID",
|
||||
"title": "Title",
|
||||
"lane": "Lane",
|
||||
"due": ["Due"]
|
||||
}
|
||||
}
|
||||
4
example-proto/sample.csv
Normal file
@@ -0,0 +1,4 @@
|
||||
ID,Title,Due,Lane
|
||||
1,Example Task A,2025-12-01,Team Alpha
|
||||
2,Example Task B,2026-02-15,Team Beta
|
||||
3,Example Task C,2026-03-10,Team Alpha
|
||||
|
64
example-proto/style.css
Normal file
@@ -0,0 +1,64 @@
|
||||
/* Example Project Dark Green Theme */
|
||||
/* This CSS demonstrates successful external stylesheet loading */
|
||||
|
||||
body {
|
||||
background: #1e3a2f !important;
|
||||
}
|
||||
|
||||
#projectName {
|
||||
color: #2d8659 !important;
|
||||
border-bottom: 2px solid #2d8659;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
#projectSubtitle {
|
||||
color: #4a9b6b !important;
|
||||
}
|
||||
|
||||
/* File Manager Override */
|
||||
#fileManager {
|
||||
background: #243329 !important;
|
||||
border-color: #2d8659 !important;
|
||||
}
|
||||
|
||||
#fileManager h3 {
|
||||
color: #4a9b6b !important;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
background: #2a3f32 !important;
|
||||
border-color: #2d8659 !important;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
border-color: #4a9b6b !important;
|
||||
box-shadow: 0 2px 8px rgba(45, 134, 89, 0.2) !important;
|
||||
}
|
||||
|
||||
.file-label {
|
||||
color: #4a9b6b !important;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
background: #2d8659 !important;
|
||||
}
|
||||
|
||||
.upload-btn:hover {
|
||||
background: #1e5a3d !important;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
button {
|
||||
background: #2d8659 !important;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background: #4a9b6b !important;
|
||||
}
|
||||
|
||||
/* Viewer */
|
||||
#viewer {
|
||||
background: #2a3f32 !important;
|
||||
border-color: #2d8659 !important;
|
||||
color: #e8f5e8 !important;
|
||||
}
|
||||
60
example-proto/template-proto.svg
Normal file
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 800">
|
||||
<defs>
|
||||
<!-- Styles that can be edited in Inkscape -->
|
||||
<style>
|
||||
.month-label { font-family: Arial, sans-serif; font-size: 11px; fill: #666; font-weight: 600; }
|
||||
.month-grid { stroke: #ddd; stroke-width: 1; }
|
||||
.lane-bg { fill: #f8f9fa; stroke: #e9ecef; stroke-width: 1; }
|
||||
.lane-label { font-family: Arial, sans-serif; font-size: 12px; fill: #495057; font-weight: 600; }
|
||||
.item-marker { fill: #007bff; }
|
||||
.item-title { font-family: Arial, sans-serif; font-size: 10px; fill: #212529; }
|
||||
.item-id { font-family: monospace; font-size: 9px; fill: #6c757d; }
|
||||
</style>
|
||||
|
||||
<!-- Gradients, filters, etc. can be added here and edited in Inkscape -->
|
||||
<linearGradient id="laneGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#f8f9fa;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Title and metadata (visible in Inkscape) -->
|
||||
<text x="20" y="30" style="font-family:Arial; font-size:20px; font-weight:bold; fill:#212529;">
|
||||
Timeline Prototype
|
||||
</text>
|
||||
<text x="20" y="50" style="font-family:Arial; font-size:12px; fill:#6c757d;">
|
||||
Edit this SVG in Inkscape - prototypes will be cloned for each data item
|
||||
</text>
|
||||
|
||||
<!-- Prototypes layer (will be hidden after cloning) -->
|
||||
<g id="prototypes" style="opacity:0.5">
|
||||
<text x="20" y="80" style="font-family:Arial; font-size:11px; fill:#fd7e14; font-weight:bold;">
|
||||
PROTOTYPES (will be cloned):
|
||||
</text>
|
||||
|
||||
<!-- Month prototype -->
|
||||
<g id="month-proto" transform="translate(220, 0)">
|
||||
<line x1="0" y1="120" x2="0" y2="600" class="month-grid" />
|
||||
<text x="4" y="90" class="month-label">Jan 25</text>
|
||||
</g>
|
||||
|
||||
<!-- Lane prototype -->
|
||||
<g id="lane-proto" transform="translate(0, 140)">
|
||||
<rect x="40" y="-24" width="1000" height="80" class="lane-bg" rx="4" ry="4" style="fill:url(#laneGradient)" />
|
||||
<text x="56" y="-4" class="lane-label">Lane Name</text>
|
||||
</g>
|
||||
|
||||
<!-- Item prototype -->
|
||||
<g id="item-proto" transform="translate(280, 150)">
|
||||
<circle cx="0" cy="0" r="5" class="item-marker" />
|
||||
<text x="12" y="4" class="item-title">Task Title</text>
|
||||
<text x="12" y="-8" class="item-id">T-123</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Containers where clones will be placed -->
|
||||
<g id="months-container"></g>
|
||||
<g id="lanes-container"></g>
|
||||
<g id="items-container"></g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -3,7 +3,7 @@
|
||||
"description": "Neutrales Beispielprojekt f\u00fcr die Timeline Engine.",
|
||||
"dataSource": "sample.csv",
|
||||
"stylesheet": "style.css",
|
||||
"svgTemplate": "template.svg",
|
||||
"svgTemplate": "template-v2.svg",
|
||||
"settings": {
|
||||
"timelineMonths": 18
|
||||
},
|
||||
|
||||
60
example/template-proto.svg
Normal file
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 800">
|
||||
<defs>
|
||||
<!-- Styles that can be edited in Inkscape -->
|
||||
<style>
|
||||
.month-label { font-family: Arial, sans-serif; font-size: 11px; fill: #666; font-weight: 600; }
|
||||
.month-grid { stroke: #ddd; stroke-width: 1; }
|
||||
.lane-bg { fill: #f8f9fa; stroke: #e9ecef; stroke-width: 1; }
|
||||
.lane-label { font-family: Arial, sans-serif; font-size: 12px; fill: #495057; font-weight: 600; }
|
||||
.item-marker { fill: #007bff; }
|
||||
.item-title { font-family: Arial, sans-serif; font-size: 10px; fill: #212529; }
|
||||
.item-id { font-family: monospace; font-size: 9px; fill: #6c757d; }
|
||||
</style>
|
||||
|
||||
<!-- Gradients, filters, etc. can be added here and edited in Inkscape -->
|
||||
<linearGradient id="laneGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#f8f9fa;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Title and metadata (visible in Inkscape) -->
|
||||
<text x="20" y="30" style="font-family:Arial; font-size:20px; font-weight:bold; fill:#212529;">
|
||||
Timeline Prototype
|
||||
</text>
|
||||
<text x="20" y="50" style="font-family:Arial; font-size:12px; fill:#6c757d;">
|
||||
Edit this SVG in Inkscape - prototypes will be cloned for each data item
|
||||
</text>
|
||||
|
||||
<!-- Prototypes layer (will be hidden after cloning) -->
|
||||
<g id="prototypes" style="opacity:0.5">
|
||||
<text x="20" y="80" style="font-family:Arial; font-size:11px; fill:#fd7e14; font-weight:bold;">
|
||||
PROTOTYPES (will be cloned):
|
||||
</text>
|
||||
|
||||
<!-- Month prototype -->
|
||||
<g id="month-proto" transform="translate(220, 0)">
|
||||
<line x1="0" y1="120" x2="0" y2="600" class="month-grid" />
|
||||
<text x="4" y="90" class="month-label">Jan 25</text>
|
||||
</g>
|
||||
|
||||
<!-- Lane prototype -->
|
||||
<g id="lane-proto" transform="translate(0, 140)">
|
||||
<rect x="40" y="-24" width="1000" height="80" class="lane-bg" rx="4" ry="4" style="fill:url(#laneGradient)" />
|
||||
<text x="56" y="-4" class="lane-label">Lane Name</text>
|
||||
</g>
|
||||
|
||||
<!-- Item prototype -->
|
||||
<g id="item-proto" transform="translate(280, 150)">
|
||||
<circle cx="0" cy="0" r="5" class="item-marker" />
|
||||
<text x="12" y="4" class="item-title">Task Title</text>
|
||||
<text x="12" y="-8" class="item-id">T-123</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Containers where clones will be placed -->
|
||||
<g id="months-container"></g>
|
||||
<g id="lanes-container"></g>
|
||||
<g id="items-container"></g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -36,12 +36,12 @@
|
||||
font-size="14" font-weight="700">{{LANE_NAME}}</text>
|
||||
</g>
|
||||
|
||||
<g id="task-template" style="display: none;">
|
||||
<circle cx="{{TASK_X}}" cy="{{TASK_Y}}" r="6"
|
||||
<g id="item-template" style="display: none;">
|
||||
<circle cx="{{ITEM_X}}" cy="{{ITEM_Y}}" r="6"
|
||||
fill="#2d8659" stroke="#4a9b6b" stroke-width="2"/>
|
||||
<text x="{{TEXT_X}}" y="{{TEXT_Y}}" font-size="12" fill="#2d8659" font-weight="500">
|
||||
<tspan class="item-id">{{TASK_ID}}: </tspan>
|
||||
<tspan class="item-title" fill="#1e5a3d">{{TASK_TITLE}}</tspan>
|
||||
<tspan class="item-id">{{ITEM_ID}}: </tspan>
|
||||
<tspan class="item-title" fill="#1e5a3d">{{ITEM_TITLE}}</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</defs>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
@@ -1,50 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="background: linear-gradient(135deg, #f0f9f4 0%, #e6f7ea 100%);">
|
||||
<defs>
|
||||
<!-- Enhanced month indicator styling -->
|
||||
<linearGradient id="monthHeaderGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2d8659;stop-opacity:0.15"/>
|
||||
<stop offset="100%" style="stop-color:#4a9b6b;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="#2d8659" 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="#4a9b6b" 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="#2d8659" stroke-width="1" opacity="0.6"/>
|
||||
|
||||
<!-- Title area with visual indicator -->
|
||||
<rect x="10" y="10" width="300" height="60" fill="#ffffff" stroke="#2d8659" stroke-width="2" rx="8" opacity="0.9"/>
|
||||
<text x="20" y="35" fill="#2d8659" font-size="16" font-weight="bold" filter="url(#textShadow)">
|
||||
📊 External Template Active
|
||||
</text>
|
||||
<text x="20" y="55" fill="#4a9b6b" font-size="11" font-weight="500">
|
||||
Enhanced 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(45, 134, 89, 0.05)" stroke="#4a9b6b" 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="#2d8659" stroke-width="2" rx="4" opacity="0.7"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
281
file-editor.js
Normal file
@@ -0,0 +1,281 @@
|
||||
// File Editor Module
|
||||
window.fileEditor = {
|
||||
currentFile: null,
|
||||
currentContent: null,
|
||||
modifiedFiles: new Set(),
|
||||
|
||||
init() {
|
||||
// Set up edit button handlers
|
||||
document.getElementById('editProjectBtn').addEventListener('click', () => {
|
||||
this.openEditor('project', 'Project Configuration (project.json)',
|
||||
JSON.stringify(window.timelineEngine.config, null, 2));
|
||||
});
|
||||
|
||||
document.getElementById('editSvgBtn').addEventListener('click', () => {
|
||||
this.openEditor('svg', 'SVG Template', window.timelineEngine.template);
|
||||
});
|
||||
|
||||
document.getElementById('editCssBtn').addEventListener('click', () => {
|
||||
this.openEditor('css', 'Stylesheet', window.timelineEngine.cssData);
|
||||
});
|
||||
|
||||
document.getElementById('editCsvBtn').addEventListener('click', () => {
|
||||
this.openEditor('csv', 'CSV Data', window.timelineEngine.csvData);
|
||||
});
|
||||
|
||||
// Set up save changes button
|
||||
document.getElementById('saveChanges').addEventListener('click', () => {
|
||||
this.saveAllChanges();
|
||||
});
|
||||
|
||||
// Close modal on background click
|
||||
document.getElementById('editorModal').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'editorModal') {
|
||||
this.closeEditor();
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard shortcut: Escape to close
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && document.getElementById('editorModal').style.display === 'block') {
|
||||
this.closeEditor();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('File editor initialized');
|
||||
},
|
||||
|
||||
openEditor(fileType, title, content) {
|
||||
if (!content) {
|
||||
alert(`No ${title} loaded. Please load a file first.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentFile = fileType;
|
||||
this.currentContent = content;
|
||||
|
||||
document.getElementById('editorTitle').textContent = `Edit ${title}`;
|
||||
document.getElementById('editorTextarea').value = content;
|
||||
document.getElementById('editorModal').style.display = 'block';
|
||||
|
||||
console.log(`Opened editor for ${fileType}`);
|
||||
},
|
||||
|
||||
closeEditor() {
|
||||
document.getElementById('editorModal').style.display = 'none';
|
||||
this.currentFile = null;
|
||||
this.currentContent = null;
|
||||
},
|
||||
|
||||
applyChanges() {
|
||||
const newContent = document.getElementById('editorTextarea').value;
|
||||
|
||||
// Validate based on file type
|
||||
if (this.currentFile === 'project' || this.currentFile === 'json') {
|
||||
try {
|
||||
JSON.parse(newContent);
|
||||
} catch (e) {
|
||||
alert(`Invalid JSON: ${e.message}\n\nPlease fix the syntax errors before applying.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if content actually changed
|
||||
if (newContent === this.currentContent) {
|
||||
console.log('No changes detected');
|
||||
this.closeEditor();
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply changes based on file type
|
||||
console.log(`Applying changes to ${this.currentFile}`);
|
||||
|
||||
try {
|
||||
switch (this.currentFile) {
|
||||
case 'project':
|
||||
const cfg = JSON.parse(newContent);
|
||||
window.timelineEngine.config = cfg;
|
||||
|
||||
// Update UI
|
||||
document.getElementById("projectName").textContent = cfg.name || "Timeline";
|
||||
document.getElementById("projectSubtitle").textContent =
|
||||
cfg.description || "Project configuration updated.";
|
||||
|
||||
// Show field mappings
|
||||
window.timelineEngine.showFieldMappings();
|
||||
|
||||
// Regenerate timeline if we have CSV data
|
||||
if (window.timelineEngine.csvData) {
|
||||
window.timelineEngine.processCsv(window.timelineEngine.csvData);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'svg':
|
||||
window.timelineEngine.template = newContent;
|
||||
window.timelineEngine.showTemplateFields();
|
||||
|
||||
// Regenerate timeline if we have CSV data
|
||||
if (window.timelineEngine.csvData) {
|
||||
window.timelineEngine.processCsv(window.timelineEngine.csvData);
|
||||
} else {
|
||||
window.timelineEngine.showTemplatePreview();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'css':
|
||||
window.timelineEngine.cssData = newContent;
|
||||
const blob = new Blob([newContent], { type: "text/css" });
|
||||
document.getElementById("dynamicCss").href = URL.createObjectURL(blob);
|
||||
break;
|
||||
|
||||
case 'csv':
|
||||
window.timelineEngine.csvData = newContent;
|
||||
window.timelineEngine.processCsv(newContent);
|
||||
break;
|
||||
}
|
||||
|
||||
// Mark file as modified
|
||||
this.modifiedFiles.add(this.currentFile);
|
||||
this.updateModifiedBadges();
|
||||
this.updateSaveButton();
|
||||
|
||||
console.log(`✅ Changes applied to ${this.currentFile}`);
|
||||
this.closeEditor();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error applying changes:', error);
|
||||
alert(`Error applying changes: ${error.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
updateModifiedBadges() {
|
||||
// Remove all existing badges
|
||||
document.querySelectorAll('.modified-badge').forEach(badge => badge.remove());
|
||||
|
||||
// Add badges to modified files
|
||||
this.modifiedFiles.forEach(fileType => {
|
||||
let statusElement;
|
||||
switch (fileType) {
|
||||
case 'project':
|
||||
statusElement = document.getElementById('projectFile');
|
||||
break;
|
||||
case 'svg':
|
||||
statusElement = document.getElementById('svgFile');
|
||||
break;
|
||||
case 'css':
|
||||
statusElement = document.getElementById('cssFile');
|
||||
break;
|
||||
case 'csv':
|
||||
statusElement = document.getElementById('csvFile');
|
||||
break;
|
||||
}
|
||||
|
||||
if (statusElement && !statusElement.querySelector('.modified-badge')) {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'modified-badge';
|
||||
badge.textContent = 'MODIFIED';
|
||||
statusElement.appendChild(badge);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updateSaveButton() {
|
||||
const saveBtn = document.getElementById('saveChanges');
|
||||
if (this.modifiedFiles.size > 0) {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.style.opacity = '1';
|
||||
} else {
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.style.opacity = '0.6';
|
||||
}
|
||||
},
|
||||
|
||||
saveAllChanges() {
|
||||
if (this.modifiedFiles.size === 0) {
|
||||
alert('No files have been modified.');
|
||||
return;
|
||||
}
|
||||
|
||||
const filesToSave = Array.from(this.modifiedFiles);
|
||||
console.log('Saving modified files:', filesToSave);
|
||||
|
||||
filesToSave.forEach(fileType => {
|
||||
let content, filename, mimeType;
|
||||
|
||||
switch (fileType) {
|
||||
case 'project':
|
||||
content = JSON.stringify(window.timelineEngine.config, null, 2);
|
||||
filename = 'project.json';
|
||||
mimeType = 'application/json';
|
||||
break;
|
||||
|
||||
case 'svg':
|
||||
content = window.timelineEngine.template;
|
||||
filename = 'template-v2.svg';
|
||||
mimeType = 'image/svg+xml';
|
||||
break;
|
||||
|
||||
case 'css':
|
||||
content = window.timelineEngine.cssData;
|
||||
filename = 'style.css';
|
||||
mimeType = 'text/css';
|
||||
break;
|
||||
|
||||
case 'csv':
|
||||
content = window.timelineEngine.csvData;
|
||||
filename = 'sample.csv';
|
||||
mimeType = 'text/csv';
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`Unknown file type: ${fileType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and trigger download
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
console.log(`✅ Saved ${filename}`);
|
||||
});
|
||||
|
||||
// Clear modified state after saving
|
||||
setTimeout(() => {
|
||||
if (confirm(`${filesToSave.length} file(s) downloaded.\n\nClear modified state?`)) {
|
||||
this.modifiedFiles.clear();
|
||||
this.updateModifiedBadges();
|
||||
this.updateSaveButton();
|
||||
}
|
||||
}, 500);
|
||||
},
|
||||
|
||||
enableEditButton(fileType) {
|
||||
let btnId;
|
||||
switch (fileType) {
|
||||
case 'project':
|
||||
btnId = 'editProjectBtn';
|
||||
break;
|
||||
case 'svg':
|
||||
btnId = 'editSvgBtn';
|
||||
break;
|
||||
case 'css':
|
||||
btnId = 'editCssBtn';
|
||||
break;
|
||||
case 'csv':
|
||||
btnId = 'editCsvBtn';
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById(btnId);
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
264
generator-dom.js
Normal file
@@ -0,0 +1,264 @@
|
||||
// DOM-based Timeline Generator
|
||||
// Uses proper DOM manipulation instead of string templates
|
||||
// SVG remains 100% valid and Inkscape-editable
|
||||
|
||||
window.timelineGeneratorDOM = {
|
||||
generate(items, cfg, templateString) {
|
||||
if (!templateString) {
|
||||
throw new Error('Template is required. Please provide an SVG file with prototype elements.');
|
||||
}
|
||||
|
||||
// Parse SVG as DOM
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(templateString, 'image/svg+xml');
|
||||
|
||||
// Check for parse errors
|
||||
const parserError = doc.querySelector('parsererror');
|
||||
if (parserError) {
|
||||
throw new Error('Failed to parse SVG template: ' + parserError.textContent);
|
||||
}
|
||||
|
||||
const svg = doc.documentElement;
|
||||
|
||||
// Validate required prototypes exist
|
||||
this.validatePrototypes(svg);
|
||||
|
||||
// Calculate layout
|
||||
const layout = this.calculateLayout(items, cfg);
|
||||
|
||||
// Generate timeline using DOM cloning
|
||||
this.generateFromPrototypes(svg, items, cfg, layout);
|
||||
|
||||
// Set SVG dimensions
|
||||
svg.setAttribute('width', layout.width);
|
||||
svg.setAttribute('height', layout.height);
|
||||
svg.setAttribute('viewBox', `0 0 ${layout.width} ${layout.height}`);
|
||||
|
||||
// Serialize back to string
|
||||
const serializer = new XMLSerializer();
|
||||
return serializer.serializeToString(svg);
|
||||
},
|
||||
|
||||
validatePrototypes(svg) {
|
||||
const requiredIds = ['month-proto', 'lane-proto', 'item-proto'];
|
||||
const missing = [];
|
||||
|
||||
for (const id of requiredIds) {
|
||||
if (!svg.querySelector(`#${id}`)) {
|
||||
missing.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
throw new Error(
|
||||
`Template is missing required prototype elements: ${missing.join(', ')}. ` +
|
||||
`Please create prototype groups with these IDs in your SVG.`
|
||||
);
|
||||
}
|
||||
|
||||
// Check for target containers (create if missing)
|
||||
const containers = ['months-container', 'lanes-container', 'items-container'];
|
||||
for (const id of containers) {
|
||||
if (!svg.querySelector(`#${id}`)) {
|
||||
console.warn(`Container #${id} not found, will create it`);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
calculateLayout(items, cfg) {
|
||||
const monthsRange = (cfg.settings && cfg.settings.timelineMonths) || 18;
|
||||
|
||||
// Determine time window
|
||||
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 (can be overridden in cfg.settings)
|
||||
const left = (cfg.settings && cfg.settings.marginLeft) || 220;
|
||||
const top = (cfg.settings && cfg.settings.marginTop) || 140;
|
||||
const monthWidth = (cfg.settings && cfg.settings.monthWidth) || 120;
|
||||
const laneHeight = (cfg.settings && cfg.settings.laneHeight) || 80;
|
||||
const laneGap = (cfg.settings && cfg.settings.laneGap) || 16;
|
||||
|
||||
const height = top + laneNames.length * (laneHeight + laneGap) + 80;
|
||||
const width = left + months.length * monthWidth + 100;
|
||||
|
||||
return {
|
||||
months, start, laneMap, laneNames,
|
||||
left, top, monthWidth, laneHeight, laneGap,
|
||||
width, height
|
||||
};
|
||||
},
|
||||
|
||||
generateFromPrototypes(svg, items, cfg, layout) {
|
||||
const { months, start, laneMap, laneNames, left, top, monthWidth, laneHeight, laneGap } = layout;
|
||||
|
||||
// Find prototypes
|
||||
const monthProto = svg.querySelector('#month-proto');
|
||||
const laneProto = svg.querySelector('#lane-proto');
|
||||
const itemProto = svg.querySelector('#item-proto');
|
||||
|
||||
// Find or create containers
|
||||
let monthsContainer = svg.querySelector('#months-container');
|
||||
let lanesContainer = svg.querySelector('#lanes-container');
|
||||
let itemsContainer = svg.querySelector('#items-container');
|
||||
|
||||
if (!monthsContainer) {
|
||||
monthsContainer = svg.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
monthsContainer.setAttribute('id', 'months-container');
|
||||
svg.appendChild(monthsContainer);
|
||||
}
|
||||
|
||||
if (!lanesContainer) {
|
||||
lanesContainer = svg.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
lanesContainer.setAttribute('id', 'lanes-container');
|
||||
svg.appendChild(lanesContainer);
|
||||
}
|
||||
|
||||
if (!itemsContainer) {
|
||||
itemsContainer = svg.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
itemsContainer.setAttribute('id', 'items-container');
|
||||
svg.appendChild(itemsContainer);
|
||||
}
|
||||
|
||||
// Generate months
|
||||
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" });
|
||||
|
||||
const clone = monthProto.cloneNode(true);
|
||||
clone.removeAttribute('id'); // Remove id to avoid duplicates
|
||||
clone.setAttribute('transform', `translate(${x}, 0)`);
|
||||
|
||||
// Update text content
|
||||
const labelElement = clone.querySelector('[id*="label"], text');
|
||||
if (labelElement) {
|
||||
labelElement.textContent = label;
|
||||
}
|
||||
|
||||
// Update grid line positions (if present)
|
||||
const gridLine = clone.querySelector('line');
|
||||
if (gridLine) {
|
||||
gridLine.setAttribute('y1', gridTop);
|
||||
gridLine.setAttribute('y2', gridBottom);
|
||||
}
|
||||
|
||||
monthsContainer.appendChild(clone);
|
||||
});
|
||||
|
||||
// Helper function
|
||||
function monthIndexForDate(d) {
|
||||
return (d.getFullYear() - start.getFullYear()) * 12
|
||||
+ (d.getMonth() - start.getMonth());
|
||||
}
|
||||
|
||||
// Generate lanes
|
||||
laneNames.forEach((laneName, laneIdx) => {
|
||||
const laneY = top + laneIdx * (laneHeight + laneGap);
|
||||
|
||||
const clone = laneProto.cloneNode(true);
|
||||
clone.removeAttribute('id');
|
||||
clone.setAttribute('transform', `translate(0, ${laneY})`);
|
||||
|
||||
// Update lane label
|
||||
const labelElement = clone.querySelector('[id*="label"], text');
|
||||
if (labelElement) {
|
||||
labelElement.textContent = laneName;
|
||||
}
|
||||
|
||||
// Update lane background width (if rect present)
|
||||
const bgRect = clone.querySelector('rect');
|
||||
if (bgRect) {
|
||||
const laneWidth = left + months.length * monthWidth;
|
||||
bgRect.setAttribute('width', laneWidth);
|
||||
bgRect.setAttribute('height', laneHeight);
|
||||
}
|
||||
|
||||
lanesContainer.appendChild(clone);
|
||||
|
||||
// Generate items for this lane
|
||||
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;
|
||||
|
||||
const itemClone = itemProto.cloneNode(true);
|
||||
itemClone.removeAttribute('id');
|
||||
itemClone.setAttribute('transform', `translate(${cx}, ${cy})`);
|
||||
|
||||
// Update text elements based on field mapping
|
||||
const fieldMapping = cfg.fieldMapping || {
|
||||
id: 'ID',
|
||||
title: 'Title',
|
||||
lane: 'Lane',
|
||||
due: 'Due'
|
||||
};
|
||||
|
||||
// Map data to text elements
|
||||
for (const [fieldName, csvColumn] of Object.entries(fieldMapping)) {
|
||||
if (fieldName === 'due') continue; // Skip due, used for positioning
|
||||
|
||||
const value = it[fieldName] || '';
|
||||
// Try to find text element with matching id
|
||||
const textElement = itemClone.querySelector(`#item-${fieldName}, [id*="${fieldName}"]`);
|
||||
if (textElement) {
|
||||
textElement.textContent = value;
|
||||
textElement.removeAttribute('id'); // Avoid duplicate IDs
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for generic text element if no specific mapping found
|
||||
if (!itemClone.querySelector('text[id]')) {
|
||||
const anyText = itemClone.querySelector('text');
|
||||
if (anyText) {
|
||||
anyText.textContent = it.title || it.id || '';
|
||||
}
|
||||
}
|
||||
|
||||
itemsContainer.appendChild(itemClone);
|
||||
});
|
||||
});
|
||||
|
||||
// Hide or remove prototypes
|
||||
monthProto.style.display = 'none';
|
||||
laneProto.style.display = 'none';
|
||||
itemProto.style.display = 'none';
|
||||
},
|
||||
|
||||
escapeXml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
};
|
||||
233
generator.js
@@ -1,5 +1,13 @@
|
||||
window.timelineGenerator = {
|
||||
generate(items, cfg, template) {
|
||||
// Validate template is provided
|
||||
if (!template) {
|
||||
throw new Error('Template is required. Please provide a template-v2.svg file with template elements.');
|
||||
}
|
||||
|
||||
// Validate template has required elements
|
||||
this.validateTemplate(template);
|
||||
|
||||
const monthsRange = (cfg.settings && cfg.settings.timelineMonths) || 18;
|
||||
|
||||
// Determine time window from earliest due date
|
||||
@@ -36,42 +44,41 @@ window.timelineGenerator = {
|
||||
const laneHeight = 80;
|
||||
const laneGap = 16;
|
||||
|
||||
// Check if template uses new template-based approach
|
||||
if (template && this.hasTemplateElements(template)) {
|
||||
return this.generateFromTemplates(items, cfg, template, {
|
||||
months, start, laneMap, laneNames,
|
||||
left, top, monthWidth, laneHeight, laneGap
|
||||
});
|
||||
}
|
||||
|
||||
// Use hardcoded generation for backward compatibility
|
||||
return this.generateHardcoded(items, cfg, template, {
|
||||
return this.generateFromTemplates(items, cfg, template, {
|
||||
months, start, laneMap, laneNames,
|
||||
left, top, monthWidth, laneHeight, laneGap
|
||||
});
|
||||
},
|
||||
|
||||
escapeXml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
},
|
||||
// Validate template has required elements
|
||||
validateTemplate(template) {
|
||||
const hasMonthTemplate = template.includes('id="month-template"');
|
||||
const hasLaneTemplate = template.includes('id="lane-template"');
|
||||
const hasItemTemplate = template.includes('id="item-template"');
|
||||
|
||||
// Check if template has template elements
|
||||
hasTemplateElements(template) {
|
||||
return template.includes('id="month-template"') &&
|
||||
template.includes('id="lane-template"') &&
|
||||
template.includes('id="task-template"');
|
||||
if (!hasMonthTemplate || !hasLaneTemplate || !hasItemTemplate) {
|
||||
const missing = [];
|
||||
if (!hasMonthTemplate) missing.push('month-template');
|
||||
if (!hasLaneTemplate) missing.push('lane-template');
|
||||
if (!hasItemTemplate) missing.push('item-template');
|
||||
|
||||
throw new Error(
|
||||
`Template is missing required elements: ${missing.join(', ')}. ` +
|
||||
`Please use a template-v2.svg file with proper template elements in the <defs> section.`
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
// Extract a template element from SVG
|
||||
extractTemplate(template, id) {
|
||||
const regex = new RegExp(`<g id="${id}"[^>]*>([\\s\\S]*?)</g>`, 'i');
|
||||
const match = template.match(regex);
|
||||
return match ? match[0] : null;
|
||||
|
||||
if (!match) {
|
||||
throw new Error(`Failed to extract template element: ${id}`);
|
||||
}
|
||||
|
||||
return match[0];
|
||||
},
|
||||
|
||||
// Replace placeholders in template string
|
||||
@@ -87,15 +94,10 @@ window.timelineGenerator = {
|
||||
generateFromTemplates(items, cfg, template, layout) {
|
||||
const { months, start, laneMap, laneNames, left, top, monthWidth, laneHeight, laneGap } = layout;
|
||||
|
||||
// Extract template elements
|
||||
// Extract template elements (will throw if not found)
|
||||
const monthTemplate = this.extractTemplate(template, 'month-template');
|
||||
const laneTemplate = this.extractTemplate(template, 'lane-template');
|
||||
const taskTemplate = this.extractTemplate(template, 'task-template');
|
||||
|
||||
if (!monthTemplate || !laneTemplate || !taskTemplate) {
|
||||
console.warn('Template elements not found, falling back to hardcoded generation');
|
||||
return this.generateHardcoded(items, cfg, template, layout);
|
||||
}
|
||||
const itemTemplate = this.extractTemplate(template, 'item-template');
|
||||
|
||||
const monthLabelY = 90;
|
||||
const gridTop = top - 20;
|
||||
@@ -154,7 +156,7 @@ window.timelineGenerator = {
|
||||
laneElement = laneElement.replace(/id="lane-template"/, '');
|
||||
laneBlocks += laneElement;
|
||||
|
||||
// Generate tasks for this lane
|
||||
// Generate items for this lane
|
||||
const laneItems = laneMap.get(laneName).sort((a, b) => a.due - b.due);
|
||||
laneItems.forEach((it, idx) => {
|
||||
const mi = monthIndexForDate(it.due);
|
||||
@@ -162,19 +164,28 @@ window.timelineGenerator = {
|
||||
const cx = left + clampedMi * monthWidth + monthWidth * 0.5;
|
||||
const cy = laneY + 10 + idx * 18;
|
||||
|
||||
const taskValues = {
|
||||
TASK_X: cx,
|
||||
TASK_Y: cy,
|
||||
// Start with layout placeholders
|
||||
const itemValues = {
|
||||
ITEM_X: cx,
|
||||
ITEM_Y: cy,
|
||||
TEXT_X: cx + 12,
|
||||
TEXT_Y: cy + 4,
|
||||
TASK_ID: this.escapeXml(it.id || ""),
|
||||
TASK_TITLE: this.escapeXml(it.title || "")
|
||||
TEXT_Y: cy + 4
|
||||
};
|
||||
|
||||
let taskElement = this.replacePlaceholders(taskTemplate, taskValues);
|
||||
taskElement = taskElement.replace(/style="display:\s*none;?"/, '');
|
||||
taskElement = taskElement.replace(/id="task-template"/, '');
|
||||
laneBlocks += taskElement;
|
||||
// Dynamically add data placeholders from all item properties
|
||||
const placeholderMapping = cfg.placeholderMapping || {};
|
||||
for (const [key, value] of Object.entries(it)) {
|
||||
if (key !== 'due') { // Skip due date as it's used for positioning
|
||||
// Use custom placeholder name if defined, otherwise use convention
|
||||
const placeholderName = placeholderMapping[key] || `ITEM_${key.toUpperCase()}`;
|
||||
itemValues[placeholderName] = this.escapeXml(value || "");
|
||||
}
|
||||
}
|
||||
|
||||
let itemElement = this.replacePlaceholders(itemTemplate, itemValues);
|
||||
itemElement = itemElement.replace(/style="display:\s*none;?"/, '');
|
||||
itemElement = itemElement.replace(/id="item-template"/, '');
|
||||
laneBlocks += itemElement;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -187,6 +198,12 @@ window.timelineGenerator = {
|
||||
.replace("{{MONTHS}}", monthGraphics)
|
||||
.replace("{{LANES}}", laneBlocks);
|
||||
|
||||
// Remove template elements from defs (they should not appear in final output)
|
||||
processedTemplate = processedTemplate
|
||||
.replace(/<g id="month-template"[^>]*>[\s\S]*?<\/g>/, '')
|
||||
.replace(/<g id="lane-template"[^>]*>[\s\S]*?<\/g>/, '')
|
||||
.replace(/<g id="item-template"[^>]*>[\s\S]*?<\/g>/, '');
|
||||
|
||||
// Add width and height attributes to the SVG element
|
||||
processedTemplate = processedTemplate.replace(
|
||||
/<svg([^>]*?)>/,
|
||||
@@ -196,130 +213,12 @@ window.timelineGenerator = {
|
||||
return processedTemplate;
|
||||
},
|
||||
|
||||
// Original hardcoded generation (for backward compatibility)
|
||||
generateHardcoded(items, cfg, template, layout) {
|
||||
const { months, start, laneMap, laneNames, left, top, monthWidth, laneHeight, laneGap } = layout;
|
||||
|
||||
// This is the original hardcoded generation that was in the generate() method
|
||||
// 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, "'");
|
||||
}
|
||||
};
|
||||
|
||||
186
index.html
@@ -49,6 +49,96 @@
|
||||
background: #343a40;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
border: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.edit-btn:hover:not(:disabled) {
|
||||
background: #545b62;
|
||||
}
|
||||
|
||||
.edit-btn:disabled {
|
||||
background: #e9ecef;
|
||||
color: #adb5bd;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.modified-badge {
|
||||
display: inline-block;
|
||||
background: #fd7e14;
|
||||
color: white;
|
||||
font-size: 9px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
margin-left: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.editor-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.6);
|
||||
z-index: 10000;
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
background: white;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.editor-body {
|
||||
padding: 20px;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.editor-textarea {
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
padding: 12px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.editor-footer {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #dee2e6;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.file-status {
|
||||
border-top: 1px solid #f1f3f4;
|
||||
padding-top: 8px;
|
||||
@@ -225,7 +315,9 @@
|
||||
}
|
||||
</style>
|
||||
<script src="generator.js"></script>
|
||||
<script src="generator-dom.js"></script>
|
||||
<script src="engine.js"></script>
|
||||
<script src="file-editor.js"></script>
|
||||
</head>
|
||||
<body class="internal-mode" style="font-family: Inter, Arial, sans-serif; background:#f5f7fa; margin:20px;">
|
||||
|
||||
@@ -239,14 +331,29 @@
|
||||
<div id="fileManager" style="margin-bottom:16px; padding:16px; background:#f8f9fa; border:1px solid #e9ecef; border-radius:8px;">
|
||||
<h3 style="margin:0 0 12px 0; font-size:14px; font-weight:600; color:#495057;">Project Files</h3>
|
||||
|
||||
<!-- Project Folder Picker -->
|
||||
<div style="margin-bottom:16px; padding:16px; background:#e7f3ff; border:2px solid #0066cc; border-radius:6px;">
|
||||
<div style="text-align:center; margin-bottom:8px;">
|
||||
<label class="upload-btn" style="background:#0066cc; font-size:14px; padding:10px 20px;">
|
||||
<input type="file" id="folderInput" webkitdirectory directory multiple style="display:none;" />
|
||||
📂 Load Project Folder
|
||||
</label>
|
||||
</div>
|
||||
<div style="text-align:center;">
|
||||
<span style="font-size:13px; color:#004080; font-weight:500;">
|
||||
Select your project folder to load all files automatically
|
||||
</span><br>
|
||||
<span style="font-size:11px; color:#0066cc;">
|
||||
(project.json, template-v2.svg, style.css, sample.csv)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-grid" style="display:grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap:12px; margin-bottom:16px;">
|
||||
<div class="file-item">
|
||||
<div class="file-header">
|
||||
<span class="file-label">Project Configuration</span>
|
||||
<label class="upload-btn">
|
||||
<input type="file" id="projectInput" accept=".json" style="display:none;" />
|
||||
📁 Load
|
||||
</label>
|
||||
<button class="edit-btn" id="editProjectBtn" disabled>✏️ Edit</button>
|
||||
</div>
|
||||
<div class="file-status">
|
||||
<span id="projectFile" class="file-name">Not loaded</span>
|
||||
@@ -256,10 +363,7 @@
|
||||
<div class="file-item">
|
||||
<div class="file-header">
|
||||
<span class="file-label">SVG Template</span>
|
||||
<label class="upload-btn">
|
||||
<input type="file" id="svgInput" accept=".svg" style="display:none;" />
|
||||
🖼️ Load
|
||||
</label>
|
||||
<button class="edit-btn" id="editSvgBtn" disabled>✏️ Edit</button>
|
||||
</div>
|
||||
<div class="file-status">
|
||||
<span id="svgFile" class="file-name">Not loaded</span>
|
||||
@@ -269,10 +373,7 @@
|
||||
<div class="file-item">
|
||||
<div class="file-header">
|
||||
<span class="file-label">Stylesheet</span>
|
||||
<label class="upload-btn">
|
||||
<input type="file" id="cssInput" accept=".css" style="display:none;" />
|
||||
🎨 Load
|
||||
</label>
|
||||
<button class="edit-btn" id="editCssBtn" disabled>✏️ Edit</button>
|
||||
</div>
|
||||
<div class="file-status">
|
||||
<span id="cssFile" class="file-name">Not loaded</span>
|
||||
@@ -282,10 +383,7 @@
|
||||
<div class="file-item">
|
||||
<div class="file-header">
|
||||
<span class="file-label">CSV Data</span>
|
||||
<label class="upload-btn">
|
||||
<input type="file" id="csvInput" accept=".csv" style="display:none;" />
|
||||
📊 Load
|
||||
</label>
|
||||
<button class="edit-btn" id="editCsvBtn" disabled>✏️ Edit</button>
|
||||
</div>
|
||||
<div class="file-status">
|
||||
<span id="csvFile" class="file-name">Not loaded</span>
|
||||
@@ -297,9 +395,13 @@
|
||||
<button id="toggleView" style="padding:8px 16px; background:#495057; color:white; border:none; border-radius:6px; cursor:pointer; font-size:12px;">
|
||||
🔄 Switch View (Internal / External)
|
||||
</button>
|
||||
<button id="saveChanges" disabled
|
||||
style="padding:8px 16px; background:#28a745; color:white; border:none; border-radius:6px; cursor:pointer; opacity:0.6; font-size:12px;">
|
||||
💾 Save Changes
|
||||
</button>
|
||||
<button id="downloadSvg" disabled
|
||||
style="padding:8px 16px; background:#495057; color:white; border:none; border-radius:6px; cursor:pointer; opacity:0.6; font-size:12px;">
|
||||
💾 Download SVG
|
||||
📥 Download SVG
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -325,6 +427,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Debug Information Panel -->
|
||||
<div id="debugInfo" style="margin-top:16px; padding:16px; background:#f8f9fa; border:1px solid #e9ecef; border-radius:8px; display:none;">
|
||||
<h3 style="margin:0 0 12px 0; font-size:14px; font-weight:600; color:#495057;">🔍 Debug Information</h3>
|
||||
|
||||
<!-- Field Mappings -->
|
||||
<div id="fieldMappingInfo" style="display:none; margin-bottom:12px;">
|
||||
<h4 style="margin:0 0 8px 0; font-size:13px; color:#6c757d; font-weight:600;">📋 Configured Field Mappings</h4>
|
||||
<pre id="fieldMappingDisplay" style="background:#fff; padding:12px; border-radius:4px; border:1px solid #dee2e6; font-size:12px; margin:0; overflow-x:auto; font-family:'Courier New', monospace;"></pre>
|
||||
</div>
|
||||
|
||||
<!-- Template Fields -->
|
||||
<div id="templateFieldsInfo" style="display:none; margin-bottom:12px;">
|
||||
<h4 style="margin:0 0 8px 0; font-size:13px; color:#6c757d; font-weight:600;">🖼️ Template Placeholders</h4>
|
||||
<pre id="templateFieldsDisplay" style="background:#fff; padding:12px; border-radius:4px; border:1px solid #dee2e6; font-size:12px; margin:0; overflow-x:auto; font-family:'Courier New', monospace;"></pre>
|
||||
</div>
|
||||
|
||||
<!-- CSV Data Preview -->
|
||||
<div id="csvDataInfo" style="display:none;">
|
||||
<h4 style="margin:0 0 8px 0; font-size:13px; color:#6c757d; font-weight:600;">📊 CSV Data Preview</h4>
|
||||
<pre id="csvDataDisplay" style="background:#fff; padding:12px; border-radius:4px; border:1px solid #dee2e6; font-size:12px; margin:0; overflow-x:auto; font-family:'Courier New', monospace;"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Setup event handlers first
|
||||
@@ -333,6 +458,12 @@
|
||||
console.log("Event handlers set up");
|
||||
}
|
||||
|
||||
// Initialize file editor
|
||||
if (window.fileEditor && typeof window.fileEditor.init === 'function') {
|
||||
window.fileEditor.init();
|
||||
console.log("File editor initialized");
|
||||
}
|
||||
|
||||
// Initialize zoom functionality
|
||||
if (window.svgViewer && typeof window.svgViewer.initializeZoom === 'function') {
|
||||
window.svgViewer.initializeZoom();
|
||||
@@ -369,5 +500,26 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Editor Modal -->
|
||||
<div id="editorModal" class="editor-modal">
|
||||
<div class="editor-content">
|
||||
<div class="editor-header">
|
||||
<h3 id="editorTitle" style="margin:0; font-size:16px; font-weight:600;">Edit File</h3>
|
||||
<button onclick="window.fileEditor.closeEditor()" style="background:none; border:none; font-size:24px; cursor:pointer; color:#6c757d;">×</button>
|
||||
</div>
|
||||
<div class="editor-body">
|
||||
<textarea id="editorTextarea" class="editor-textarea" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div class="editor-footer">
|
||||
<button onclick="window.fileEditor.closeEditor()" style="padding:8px 16px; background:#6c757d; color:white; border:none; border-radius:4px; cursor:pointer;">
|
||||
Cancel
|
||||
</button>
|
||||
<button onclick="window.fileEditor.applyChanges()" style="padding:8px 16px; background:#007bff; color:white; border:none; border-radius:4px; cursor:pointer;">
|
||||
Apply Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "Roadmap-Timeline for My Project",
|
||||
"dataSource": "sample.csv",
|
||||
"stylesheet": "style.css",
|
||||
"svgTemplate": "template.svg",
|
||||
"svgTemplate": "template-v2.svg",
|
||||
"settings": {
|
||||
"timelineMonths": 18
|
||||
},
|
||||
|
||||
@@ -36,12 +36,12 @@
|
||||
font-size="14" font-weight="700">{{LANE_NAME}}</text>
|
||||
</g>
|
||||
|
||||
<g id="task-template" style="display: none;">
|
||||
<circle cx="{{TASK_X}}" cy="{{TASK_Y}}" r="6"
|
||||
<g id="item-template" style="display: none;">
|
||||
<circle cx="{{ITEM_X}}" cy="{{ITEM_Y}}" r="6"
|
||||
fill="#3b82f6" stroke="#60a5fa" stroke-width="2"/>
|
||||
<text x="{{TEXT_X}}" y="{{TEXT_Y}}" font-size="12" fill="#3b82f6" font-weight="500">
|
||||
<tspan class="item-id">{{TASK_ID}}: </tspan>
|
||||
<tspan class="item-title" fill="#1e40af">{{TASK_TITLE}}</tspan>
|
||||
<tspan class="item-id">{{ITEM_ID}}: </tspan>
|
||||
<tspan class="item-title" fill="#1e40af">{{ITEM_TITLE}}</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</defs>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
@@ -1,50 +0,0 @@
|
||||
<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: 2.1 KiB |
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { createSampleItems, createSampleProject, createSampleTemplate } from './testHelpers.js'
|
||||
import { createSampleItems, createSampleProject, createSampleTemplate, createMalformedTemplate } from './testHelpers.js'
|
||||
|
||||
// Import generator by loading it as text and evaluating
|
||||
const fs = await import('fs/promises')
|
||||
@@ -36,7 +36,7 @@ describe('Timeline Generator', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('generate', () => {
|
||||
describe('Template validation', () => {
|
||||
let items, config
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -44,88 +44,166 @@ describe('Timeline Generator', () => {
|
||||
config = createSampleProject()
|
||||
})
|
||||
|
||||
it('should generate SVG with template placeholders', () => {
|
||||
it('should throw error when template is not provided', () => {
|
||||
expect(() => {
|
||||
timelineGenerator.generate(items, config, null)
|
||||
}).toThrow('Template is required')
|
||||
})
|
||||
|
||||
it('should throw error when template is missing month-template', () => {
|
||||
const malformedTemplate = createMalformedTemplate('month-template')
|
||||
|
||||
expect(() => {
|
||||
timelineGenerator.generate(items, config, malformedTemplate)
|
||||
}).toThrow('Template is missing required elements: month-template')
|
||||
})
|
||||
|
||||
it('should throw error when template is missing lane-template', () => {
|
||||
const malformedTemplate = createMalformedTemplate('lane-template')
|
||||
|
||||
expect(() => {
|
||||
timelineGenerator.generate(items, config, malformedTemplate)
|
||||
}).toThrow('Template is missing required elements: lane-template')
|
||||
})
|
||||
|
||||
it('should throw error when template is missing item-template', () => {
|
||||
const malformedTemplate = createMalformedTemplate('item-template')
|
||||
|
||||
expect(() => {
|
||||
timelineGenerator.generate(items, config, malformedTemplate)
|
||||
}).toThrow('Template is missing required elements: item-template')
|
||||
})
|
||||
|
||||
it('should validate template with proper error message', () => {
|
||||
const emptyTemplate = '<svg></svg>'
|
||||
|
||||
expect(() => {
|
||||
timelineGenerator.generate(items, config, emptyTemplate)
|
||||
}).toThrow('Please use a template-v2.svg file with proper template elements')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Template extraction', () => {
|
||||
it('should extract template element by id', () => {
|
||||
const svg = `
|
||||
<svg>
|
||||
<g id="month-template">
|
||||
<line x1="{{X}}" y1="0"/>
|
||||
<text>{{LABEL}}</text>
|
||||
</g>
|
||||
</svg>
|
||||
`
|
||||
const result = timelineGenerator.extractTemplate(svg, 'month-template')
|
||||
expect(result).toContain('id="month-template"')
|
||||
expect(result).toContain('{{X}}')
|
||||
expect(result).toContain('{{LABEL}}')
|
||||
})
|
||||
|
||||
it('should throw error when template not found', () => {
|
||||
const svg = '<svg><g id="other"></g></svg>'
|
||||
|
||||
expect(() => {
|
||||
timelineGenerator.extractTemplate(svg, 'month-template')
|
||||
}).toThrow('Failed to extract template element: month-template')
|
||||
})
|
||||
|
||||
it('should extract nested elements within template', () => {
|
||||
const svg = `
|
||||
<svg>
|
||||
<g id="lane-template">
|
||||
<rect fill="#FFF"/>
|
||||
<text>Label</text>
|
||||
</g>
|
||||
</svg>
|
||||
`
|
||||
const result = timelineGenerator.extractTemplate(svg, 'lane-template')
|
||||
expect(result).toContain('<rect fill="#FFF"/>')
|
||||
expect(result).toContain('<text>Label</text>')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Placeholder replacement', () => {
|
||||
it('should replace all placeholders with values', () => {
|
||||
const template = '<text x="{{X}}" y="{{Y}}">{{LABEL}}</text>'
|
||||
const values = { X: 100, Y: 200, LABEL: 'Test' }
|
||||
const result = timelineGenerator.replacePlaceholders(template, values)
|
||||
expect(result).toBe('<text x="100" y="200">Test</text>')
|
||||
})
|
||||
|
||||
it('should handle multiple occurrences of same placeholder', () => {
|
||||
const template = '<g><rect x="{{X}}"/><circle cx="{{X}}"/></g>'
|
||||
const values = { X: 50 }
|
||||
const result = timelineGenerator.replacePlaceholders(template, values)
|
||||
expect(result).toBe('<g><rect x="50"/><circle cx="50"/></g>')
|
||||
})
|
||||
|
||||
it('should leave unmatched placeholders unchanged', () => {
|
||||
const template = '<text>{{LABEL}} {{OTHER}}</text>'
|
||||
const values = { LABEL: 'Test' }
|
||||
const result = timelineGenerator.replacePlaceholders(template, values)
|
||||
expect(result).toContain('Test')
|
||||
expect(result).toContain('{{OTHER}}')
|
||||
})
|
||||
|
||||
it('should handle numeric and boolean values', () => {
|
||||
const template = '<rect x="{{X}}" visible="{{VISIBLE}}"/>'
|
||||
const values = { X: 42, VISIBLE: true }
|
||||
const result = timelineGenerator.replacePlaceholders(template, values)
|
||||
expect(result).toBe('<rect x="42" visible="true"/>')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Template-based SVG generation', () => {
|
||||
let items, config
|
||||
|
||||
beforeEach(() => {
|
||||
items = createSampleItems()
|
||||
config = createSampleProject()
|
||||
})
|
||||
|
||||
it('should generate SVG with template-v2 format', () => {
|
||||
const template = createSampleTemplate()
|
||||
const result = timelineGenerator.generate(items, config, template)
|
||||
|
||||
expect(result).toContain('<svg xmlns="http://www.w3.org/2000/svg"')
|
||||
expect(result).toContain('width=')
|
||||
expect(result).toContain('height=')
|
||||
expect(result).toContain('<rect width="100%" height="100%" fill="#FFFFFF"/>')
|
||||
expect(result).not.toContain('{{MONTHS}}')
|
||||
expect(result).not.toContain('{{LANES}}')
|
||||
})
|
||||
|
||||
it('should generate fallback SVG when no template provided', () => {
|
||||
const result = timelineGenerator.generate(items, config, null)
|
||||
it('should not contain template placeholders in output', () => {
|
||||
const template = createSampleTemplate()
|
||||
const result = timelineGenerator.generate(items, config, template)
|
||||
|
||||
expect(result).toContain('<svg xmlns="http://www.w3.org/2000/svg"')
|
||||
expect(result).toContain('width=')
|
||||
expect(result).toContain('height=')
|
||||
expect(result).toContain('<rect width="100%" height="100%" fill="#FFFFFF"')
|
||||
expect(result).not.toContain('{{MONTH_X}}')
|
||||
expect(result).not.toContain('{{LANE_Y}}')
|
||||
expect(result).not.toContain('{{ITEM_X}}')
|
||||
expect(result).not.toContain('{{ITEM_TITLE}}')
|
||||
})
|
||||
|
||||
it('should create month labels and grid lines', () => {
|
||||
const result = timelineGenerator.generate(items, config, null)
|
||||
it('should contain task data in generated SVG', () => {
|
||||
const template = createSampleTemplate()
|
||||
const result = timelineGenerator.generate(items, config, template)
|
||||
|
||||
expect(result).toContain('<line')
|
||||
expect(result).toContain('stroke="#E3E8EF"')
|
||||
expect(result).toContain('<text')
|
||||
expect(result).toContain('fill="#5C6B7A"')
|
||||
expect(result).toContain('T-1')
|
||||
expect(result).toContain('First Task')
|
||||
expect(result).toContain('T-2')
|
||||
expect(result).toContain('Second Task')
|
||||
expect(result).toContain('T-3')
|
||||
expect(result).toContain('Third Task')
|
||||
})
|
||||
|
||||
it('should create lane backgrounds and labels', () => {
|
||||
const result = timelineGenerator.generate(items, config, null)
|
||||
it('should contain lane names in generated SVG', () => {
|
||||
const template = createSampleTemplate()
|
||||
const result = timelineGenerator.generate(items, config, template)
|
||||
|
||||
expect(result).toContain('Development')
|
||||
expect(result).toContain('Testing')
|
||||
expect(result).toContain('<rect')
|
||||
expect(result).toContain('fill="#FFFFFF"')
|
||||
})
|
||||
|
||||
it('should position items correctly in lanes', () => {
|
||||
const result = timelineGenerator.generate(items, config, null)
|
||||
|
||||
expect(result).toContain('<circle')
|
||||
expect(result).toContain('fill="#0A4D8C"')
|
||||
expect(result).toContain('T-1')
|
||||
expect(result).toContain('First Task')
|
||||
})
|
||||
|
||||
it('should sort items by due date within lanes', () => {
|
||||
// Add items with same lane but different dates
|
||||
const unsortedItems = [
|
||||
{ id: 'T-3', title: 'Third', lane: 'Dev', due: new Date('2025-03-01') },
|
||||
{ id: 'T-1', title: 'First', lane: 'Dev', due: new Date('2025-01-01') },
|
||||
{ id: 'T-2', title: 'Second', lane: 'Dev', due: new Date('2025-02-01') }
|
||||
]
|
||||
|
||||
const result = timelineGenerator.generate(unsortedItems, config, null)
|
||||
const firstIndex = result.indexOf('First')
|
||||
const secondIndex = result.indexOf('Second')
|
||||
const thirdIndex = result.indexOf('Third')
|
||||
|
||||
expect(firstIndex).toBeLessThan(secondIndex)
|
||||
expect(secondIndex).toBeLessThan(thirdIndex)
|
||||
})
|
||||
|
||||
it('should handle items without lanes', () => {
|
||||
const itemsNoLane = [
|
||||
{ id: 'T-1', title: 'No Lane Task', lane: null, due: new Date('2025-01-01') }
|
||||
]
|
||||
|
||||
const result = timelineGenerator.generate(itemsNoLane, config, null)
|
||||
expect(result).toContain('Ohne Epic')
|
||||
})
|
||||
|
||||
it('should respect timelineMonths setting', () => {
|
||||
const shortConfig = { ...config, settings: { timelineMonths: 6 } }
|
||||
const result = timelineGenerator.generate(items, shortConfig, null)
|
||||
|
||||
// Should create 6 months worth of grid lines
|
||||
const lineCount = (result.match(/<line/g) || []).length
|
||||
expect(lineCount).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should escape special characters in item text', () => {
|
||||
it('should escape special characters in task data', () => {
|
||||
const itemsWithSpecialChars = [{
|
||||
id: 'T&1',
|
||||
title: 'Task with <special> & "characters"',
|
||||
@@ -133,172 +211,119 @@ describe('Timeline Generator', () => {
|
||||
due: new Date('2025-01-01')
|
||||
}]
|
||||
|
||||
const result = timelineGenerator.generate(itemsWithSpecialChars, config, null)
|
||||
const template = createSampleTemplate()
|
||||
const result = timelineGenerator.generate(itemsWithSpecialChars, config, template)
|
||||
|
||||
expect(result).toContain('T&1')
|
||||
expect(result).toContain('<special> & "characters"')
|
||||
})
|
||||
|
||||
it('should determine start date from earliest item', () => {
|
||||
const itemsWithEarlyDate = [
|
||||
{ id: 'T-1', title: 'Early', lane: 'Dev', due: new Date('2024-06-15') },
|
||||
{ id: 'T-2', title: 'Late', lane: 'Dev', due: new Date('2025-12-01') }
|
||||
it('should handle items without lanes', () => {
|
||||
const itemsNoLane = [
|
||||
{ id: 'T-1', title: 'No Lane Task', lane: null, due: new Date('2025-01-01') }
|
||||
]
|
||||
|
||||
const result = timelineGenerator.generate(itemsWithEarlyDate, config, null)
|
||||
|
||||
// Should start from June 2024 (first day of month) - German month name
|
||||
expect(result).toContain('Juni 24')
|
||||
const template = createSampleTemplate()
|
||||
const result = timelineGenerator.generate(itemsNoLane, config, template)
|
||||
expect(result).toContain('Ohne Epic')
|
||||
})
|
||||
|
||||
it('should clamp item positions to timeline bounds', () => {
|
||||
const itemsOutOfRange = [
|
||||
{ id: 'T-1', title: 'In Range', lane: 'Dev', due: new Date('2025-01-01') },
|
||||
{ id: 'T-2', title: 'Way Future', lane: 'Dev', due: new Date('2030-01-01') }
|
||||
]
|
||||
it('should respect timelineMonths setting', () => {
|
||||
const template = createSampleTemplate()
|
||||
const shortConfig = { ...config, settings: { timelineMonths: 6 } }
|
||||
const longConfig = { ...config, settings: { timelineMonths: 24 } }
|
||||
|
||||
// Should not throw and should generate valid SVG
|
||||
const result = timelineGenerator.generate(itemsOutOfRange, config, null)
|
||||
expect(result).toContain('<svg')
|
||||
expect(result).toContain('T-1')
|
||||
expect(result).toContain('T-2')
|
||||
const shortResult = timelineGenerator.generate(items, shortConfig, template)
|
||||
const longResult = timelineGenerator.generate(items, longConfig, template)
|
||||
|
||||
// Longer timeline should produce larger SVG
|
||||
const shortWidth = shortResult.match(/width="(\d+)"/)?.[1] || 0
|
||||
const longWidth = longResult.match(/width="(\d+)"/)?.[1] || 0
|
||||
expect(parseInt(longWidth)).toBeGreaterThan(parseInt(shortWidth))
|
||||
})
|
||||
|
||||
it('should not contain template elements with display:none in output', () => {
|
||||
const template = createSampleTemplate()
|
||||
const result = timelineGenerator.generate(items, config, template)
|
||||
|
||||
expect(result).not.toContain('id="month-template"')
|
||||
expect(result).not.toContain('id="lane-template"')
|
||||
expect(result).not.toContain('id="item-template"')
|
||||
expect(result).not.toContain('style="display:none"')
|
||||
})
|
||||
|
||||
it('should preserve template styling in generated output', async () => {
|
||||
const templateV2 = await fs.readFile('./example/template-v2.svg', 'utf-8')
|
||||
const result = timelineGenerator.generate(items, config, templateV2)
|
||||
|
||||
// Should preserve gradient and filter definitions
|
||||
expect(result).toContain('monthHeaderGrad')
|
||||
expect(result).toContain('textShadow')
|
||||
expect(result).toContain('bgGrid')
|
||||
})
|
||||
|
||||
it('should set viewBox dimensions correctly', () => {
|
||||
const template = createSampleTemplate()
|
||||
const result = timelineGenerator.generate(items, config, template)
|
||||
|
||||
const widthMatch = result.match(/width="(\d+)"/)
|
||||
const heightMatch = result.match(/height="(\d+)"/)
|
||||
const viewBoxMatch = result.match(/viewBox="0 0 (\d+) (\d+)"/)
|
||||
|
||||
expect(widthMatch).toBeTruthy()
|
||||
expect(heightMatch).toBeTruthy()
|
||||
expect(viewBoxMatch).toBeTruthy()
|
||||
|
||||
// viewBox should match width and height
|
||||
expect(viewBoxMatch[1]).toBe(widthMatch[1])
|
||||
expect(viewBoxMatch[2]).toBe(heightMatch[1])
|
||||
})
|
||||
|
||||
it('should support custom placeholder mapping', () => {
|
||||
const customConfig = {
|
||||
...config,
|
||||
placeholderMapping: {
|
||||
id: 'TASK_ID', // Map item.id to {{TASK_ID}} instead of {{ITEM_ID}}
|
||||
title: 'TASK_NAME' // Map item.title to {{TASK_NAME}} instead of {{ITEM_TITLE}}
|
||||
}
|
||||
}
|
||||
|
||||
// Create template with custom placeholders
|
||||
const customTemplate = `<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<g id="month-template" style="display:none">
|
||||
<text x="{{MONTH_TEXT_X}}" y="{{MONTH_LABEL_Y}}">{{MONTH_LABEL}}</text>
|
||||
</g>
|
||||
<g id="lane-template" style="display:none">
|
||||
<text>{{LANE_NAME}}</text>
|
||||
</g>
|
||||
<g id="item-template" style="display:none">
|
||||
<text x="{{TEXT_X}}" y="{{TEXT_Y}}">{{TASK_ID}}: {{TASK_NAME}}</text>
|
||||
</g>
|
||||
</defs>
|
||||
{{MONTHS}}
|
||||
{{LANES}}
|
||||
</svg>`
|
||||
|
||||
const result = timelineGenerator.generate(items, customConfig, customTemplate)
|
||||
|
||||
// Should use custom placeholder names
|
||||
expect(result).toContain('T-1: First Task') // TASK_ID: TASK_NAME
|
||||
expect(result).toContain('T-2: Second Task')
|
||||
|
||||
// Should not contain default placeholder syntax
|
||||
expect(result).not.toContain('{{ITEM_ID}}')
|
||||
expect(result).not.toContain('{{ITEM_TITLE}}')
|
||||
})
|
||||
|
||||
it('should use default convention when placeholderMapping is not provided', () => {
|
||||
const template = createSampleTemplate()
|
||||
const result = timelineGenerator.generate(items, config, template)
|
||||
|
||||
// Should use default ITEM_* convention
|
||||
expect(result).toContain('T-1 First Task') // Default template has space, not colon
|
||||
expect(result).not.toContain('{{ITEM_ID}}')
|
||||
expect(result).not.toContain('{{ITEM_TITLE}}')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Template-based rendering', () => {
|
||||
let items, config
|
||||
|
||||
beforeEach(() => {
|
||||
items = createSampleItems()
|
||||
config = createSampleProject()
|
||||
})
|
||||
|
||||
describe('hasTemplateElements', () => {
|
||||
it('should detect template elements', () => {
|
||||
const templateWithElements = `
|
||||
<svg>
|
||||
<defs>
|
||||
<g id="month-template"></g>
|
||||
<g id="lane-template"></g>
|
||||
<g id="task-template"></g>
|
||||
</defs>
|
||||
</svg>
|
||||
`
|
||||
expect(timelineGenerator.hasTemplateElements(templateWithElements)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when template elements are missing', () => {
|
||||
const templateWithoutElements = '<svg><g>{{MONTHS}}</g></svg>'
|
||||
expect(timelineGenerator.hasTemplateElements(templateWithoutElements)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false when only some template elements exist', () => {
|
||||
const partialTemplate = '<svg><g id="month-template"></g></svg>'
|
||||
expect(timelineGenerator.hasTemplateElements(partialTemplate)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractTemplate', () => {
|
||||
it('should extract template element by id', () => {
|
||||
const svg = `
|
||||
<svg>
|
||||
<g id="month-template">
|
||||
<line x1="{{X}}" y1="0"/>
|
||||
<text>{{LABEL}}</text>
|
||||
</g>
|
||||
</svg>
|
||||
`
|
||||
const result = timelineGenerator.extractTemplate(svg, 'month-template')
|
||||
expect(result).toContain('id="month-template"')
|
||||
expect(result).toContain('{{X}}')
|
||||
expect(result).toContain('{{LABEL}}')
|
||||
})
|
||||
|
||||
it('should return null when template not found', () => {
|
||||
const svg = '<svg><g id="other"></g></svg>'
|
||||
const result = timelineGenerator.extractTemplate(svg, 'month-template')
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('replacePlaceholders', () => {
|
||||
it('should replace all placeholders with values', () => {
|
||||
const template = '<text x="{{X}}" y="{{Y}}">{{LABEL}}</text>'
|
||||
const values = { X: 100, Y: 200, LABEL: 'Test' }
|
||||
const result = timelineGenerator.replacePlaceholders(template, values)
|
||||
expect(result).toBe('<text x="100" y="200">Test</text>')
|
||||
})
|
||||
|
||||
it('should handle multiple occurrences of same placeholder', () => {
|
||||
const template = '<g><rect x="{{X}}"/><circle cx="{{X}}"/></g>'
|
||||
const values = { X: 50 }
|
||||
const result = timelineGenerator.replacePlaceholders(template, values)
|
||||
expect(result).toBe('<g><rect x="50"/><circle cx="50"/></g>')
|
||||
})
|
||||
|
||||
it('should leave unmatched placeholders unchanged', () => {
|
||||
const template = '<text>{{LABEL}} {{OTHER}}</text>'
|
||||
const values = { LABEL: 'Test' }
|
||||
const result = timelineGenerator.replacePlaceholders(template, values)
|
||||
expect(result).toContain('Test')
|
||||
expect(result).toContain('{{OTHER}}')
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateFromTemplates', () => {
|
||||
it('should generate SVG using template elements', async () => {
|
||||
// Read actual template-v2.svg
|
||||
const fs = await import('fs/promises')
|
||||
const templateV2 = await fs.readFile('./example/template-v2.svg', 'utf-8')
|
||||
|
||||
const result = timelineGenerator.generate(items, config, templateV2)
|
||||
|
||||
// Should contain SVG structure
|
||||
expect(result).toContain('<svg')
|
||||
expect(result).toContain('width=')
|
||||
expect(result).toContain('height=')
|
||||
|
||||
// Should not contain template placeholders
|
||||
expect(result).not.toContain('{{MONTH_X}}')
|
||||
expect(result).not.toContain('{{LANE_Y}}')
|
||||
expect(result).not.toContain('{{TASK_X}}')
|
||||
|
||||
// Should contain actual data
|
||||
expect(result).toContain('T-1')
|
||||
expect(result).toContain('First Task')
|
||||
expect(result).toContain('Development')
|
||||
})
|
||||
|
||||
it('should fall back to hardcoded generation when templates incomplete', () => {
|
||||
const incompleteTemplate = `
|
||||
<svg>
|
||||
<defs>
|
||||
<g id="month-template"></g>
|
||||
</defs>
|
||||
{{MONTHS}}
|
||||
{{LANES}}
|
||||
</svg>
|
||||
`
|
||||
|
||||
const result = timelineGenerator.generate(items, config, incompleteTemplate)
|
||||
|
||||
// Should still generate valid SVG
|
||||
expect(result).toContain('<svg')
|
||||
expect(result).toContain('T-1')
|
||||
expect(result).toContain('Development')
|
||||
})
|
||||
|
||||
it('should preserve template styling in generated output', async () => {
|
||||
const fs = await import('fs/promises')
|
||||
const templateV2 = await fs.readFile('./example/template-v2.svg', 'utf-8')
|
||||
|
||||
const result = timelineGenerator.generate(items, config, templateV2)
|
||||
|
||||
// Should preserve gradient and filter definitions
|
||||
expect(result).toContain('monthHeaderGrad')
|
||||
expect(result).toContain('textShadow')
|
||||
expect(result).toContain('bgGrid')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { setupBasicDOM } from './setup.js'
|
||||
import { createSampleProject, createSampleCSV, createSampleTemplate, mockFetch, mockPapaParse } from './testHelpers.js'
|
||||
import {
|
||||
createSampleProject,
|
||||
createSampleCSV,
|
||||
createSampleTemplate,
|
||||
createMalformedTemplate,
|
||||
createLargeDataset,
|
||||
mockFetch,
|
||||
mockPapaParse
|
||||
} from './testHelpers.js'
|
||||
|
||||
// Import both engine and generator
|
||||
const fs = await import('fs/promises')
|
||||
@@ -25,14 +33,15 @@ describe('Timeline Integration', () => {
|
||||
})
|
||||
|
||||
describe('End-to-End Timeline Generation', () => {
|
||||
it('should load project, process CSV, and generate timeline', async () => {
|
||||
it('should load project, process CSV, and generate timeline with template-v2', async () => {
|
||||
const config = createSampleProject()
|
||||
const csvData = createSampleCSV()
|
||||
const template = createSampleTemplate()
|
||||
|
||||
// Mock fetch calls in order: template, CSV
|
||||
mockFetch(template)
|
||||
mockFetch(csvData)
|
||||
// Mock fetch calls in order: CSS, SVG template, CSV
|
||||
mockFetch('/* test css */') // CSS
|
||||
mockFetch(template) // SVG
|
||||
mockFetch(csvData) // CSV
|
||||
mockPapaParse()
|
||||
|
||||
await timelineEngine.loadProjectConfigObject(config)
|
||||
@@ -41,12 +50,17 @@ describe('Timeline Integration', () => {
|
||||
expect(document.getElementById('projectName').textContent).toBe('Test Project')
|
||||
expect(document.getElementById('projectSubtitle').textContent).toBe('A test project for unit testing')
|
||||
|
||||
// Verify timeline generated
|
||||
// Verify timeline generated with template-v2 format
|
||||
const viewer = document.getElementById('viewer')
|
||||
expect(viewer.innerHTML).toContain('<svg')
|
||||
expect(viewer.innerHTML).toContain('First Task')
|
||||
expect(viewer.innerHTML).toContain('Development')
|
||||
|
||||
// Verify no template placeholders remain
|
||||
expect(viewer.innerHTML).not.toContain('{{MONTH_X}}')
|
||||
expect(viewer.innerHTML).not.toContain('{{LANE_Y}}')
|
||||
expect(viewer.innerHTML).not.toContain('{{ITEM_X}}')
|
||||
|
||||
// Verify download button enabled
|
||||
const downloadBtn = document.getElementById('downloadSvg')
|
||||
expect(downloadBtn.disabled).toBe(false)
|
||||
@@ -59,6 +73,7 @@ describe('Timeline Integration', () => {
|
||||
const overrideCSV = 'ID,Title,Lane,Due\nO-1,Override Task,Override Lane,2025-06-01'
|
||||
|
||||
// First load project with original CSV
|
||||
mockFetch('/* test css */') // CSS
|
||||
mockFetch(createSampleTemplate())
|
||||
mockFetch(originalCSV)
|
||||
|
||||
@@ -79,33 +94,6 @@ describe('Timeline Integration', () => {
|
||||
expect(document.getElementById('viewer').innerHTML).toContain('Override Task')
|
||||
})
|
||||
|
||||
it('should handle template with custom macros', async () => {
|
||||
const config = createSampleProject()
|
||||
const customTemplate = `<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600">
|
||||
<g class="timeline-months">{{MONTHS}}</g>
|
||||
<g class="timeline-lanes">{{LANES}}</g>
|
||||
</svg>`
|
||||
|
||||
mockFetch(customTemplate)
|
||||
mockFetch(createSampleCSV())
|
||||
|
||||
global.Papa.parse.mockImplementation((text, options) => {
|
||||
options.complete({
|
||||
data: [{ ID: 'T-1', Title: 'Test Task', Lane: 'Test Lane', Due: '2025-01-15' }]
|
||||
})
|
||||
})
|
||||
|
||||
await timelineEngine.loadProjectConfigObject(config)
|
||||
|
||||
const viewer = document.getElementById('viewer')
|
||||
const svg = viewer.innerHTML
|
||||
|
||||
expect(svg).toContain('<g class="timeline-months">')
|
||||
expect(svg).toContain('<g class="timeline-lanes">')
|
||||
expect(svg).not.toContain('{{MONTHS}}')
|
||||
expect(svg).not.toContain('{{LANES}}')
|
||||
})
|
||||
|
||||
it('should handle project load failures gracefully', async () => {
|
||||
// Mock fetch failures
|
||||
global.fetch.mockRejectedValue(new Error('Network error'))
|
||||
@@ -119,83 +107,216 @@ describe('Timeline Integration', () => {
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('DOM Event Handling', () => {
|
||||
it('should handle project file upload', async () => {
|
||||
it('should generate timeline with large dataset (60+ items)', async () => {
|
||||
const config = createSampleProject()
|
||||
const projectInput = document.createElement('input')
|
||||
projectInput.id = 'projectInput'
|
||||
document.body.appendChild(projectInput)
|
||||
const largeItems = createLargeDataset(60)
|
||||
|
||||
// Setup event handlers
|
||||
window.setupEventHandlers()
|
||||
|
||||
// Mock file reading
|
||||
const mockFile = new File([JSON.stringify(config)], 'project.json', { type: 'application/json' })
|
||||
mockFile.text = vi.fn().mockResolvedValue(JSON.stringify(config))
|
||||
|
||||
// Mock the fetch calls that loadProjectConfigObject will make
|
||||
mockFetch('/* test css */') // CSS
|
||||
mockFetch(createSampleTemplate())
|
||||
mockFetch('') // Mock CSV fetch
|
||||
|
||||
global.Papa.parse.mockImplementation((text, options) => {
|
||||
// Create mock CSV data from large dataset
|
||||
const mockData = largeItems.map((item, idx) => ({
|
||||
ID: item.id,
|
||||
Title: item.title,
|
||||
Lane: item.lane,
|
||||
Due: item.due.toISOString().split('T')[0]
|
||||
}))
|
||||
options.complete({ data: mockData })
|
||||
})
|
||||
|
||||
await timelineEngine.loadProjectConfigObject(config)
|
||||
|
||||
const viewer = document.getElementById('viewer')
|
||||
const svg = viewer.innerHTML
|
||||
|
||||
// Verify SVG contains multiple lanes
|
||||
expect(svg).toContain('Development')
|
||||
expect(svg).toContain('Testing')
|
||||
expect(svg).toContain('Design')
|
||||
expect(svg).toContain('DevOps')
|
||||
expect(svg).toContain('Documentation')
|
||||
|
||||
// Verify SVG contains task data
|
||||
expect(svg).toContain('TASK-1')
|
||||
expect(svg).toContain('TASK-60')
|
||||
|
||||
// Verify SVG has reasonable dimensions
|
||||
const widthMatch = svg.match(/width="(\d+)"/)
|
||||
const heightMatch = svg.match(/height="(\d+)"/)
|
||||
expect(parseInt(widthMatch[1])).toBeGreaterThan(500)
|
||||
expect(parseInt(heightMatch[1])).toBeGreaterThan(300)
|
||||
})
|
||||
|
||||
it('should handle date range edge cases (24+ months)', async () => {
|
||||
const config = { ...createSampleProject(), settings: { timelineMonths: 30 } }
|
||||
|
||||
const edgeCaseItems = [
|
||||
{ id: 'EARLY-1', title: 'Very Early Task', lane: 'Dev', due: new Date('2024-01-01') },
|
||||
{ id: 'MID-1', title: 'Middle Task', lane: 'Dev', due: new Date('2025-06-15') },
|
||||
{ id: 'LATE-1', title: 'Future Task', lane: 'Dev', due: new Date('2026-12-31') }
|
||||
]
|
||||
|
||||
mockFetch('/* test css */') // CSS
|
||||
mockFetch(createSampleTemplate())
|
||||
mockFetch('')
|
||||
|
||||
global.Papa.parse.mockImplementation((text, options) => {
|
||||
const mockData = edgeCaseItems.map(item => ({
|
||||
ID: item.id,
|
||||
Title: item.title,
|
||||
Lane: item.lane,
|
||||
Due: item.due.toISOString().split('T')[0]
|
||||
}))
|
||||
options.complete({ data: mockData })
|
||||
})
|
||||
|
||||
await timelineEngine.loadProjectConfigObject(config)
|
||||
|
||||
const svg = document.getElementById('viewer').innerHTML
|
||||
|
||||
// All tasks should be rendered
|
||||
expect(svg).toContain('EARLY-1')
|
||||
expect(svg).toContain('MID-1')
|
||||
expect(svg).toContain('LATE-1')
|
||||
|
||||
// Should have wide SVG for 30 months
|
||||
const widthMatch = svg.match(/width="(\d+)"/)
|
||||
expect(parseInt(widthMatch[1])).toBeGreaterThan(2000)
|
||||
})
|
||||
|
||||
it('should handle special characters in lane names and task titles', async () => {
|
||||
const config = createSampleProject()
|
||||
|
||||
mockFetch('/* test css */') // CSS
|
||||
mockFetch(createSampleTemplate())
|
||||
mockFetch('')
|
||||
|
||||
global.Papa.parse.mockImplementation((text, options) => {
|
||||
const mockData = [
|
||||
{ ID: 'T&1', Title: 'Task with <special> & "quotes"', Lane: 'Dev & Test', Due: '2025-01-15' },
|
||||
{ ID: 'T\'2', Title: 'L\'importance de l\'échappement', Lane: 'Développement', Due: '2025-02-01' }
|
||||
]
|
||||
options.complete({ data: mockData })
|
||||
})
|
||||
|
||||
// Should not throw error when processing special characters
|
||||
await expect(timelineEngine.loadProjectConfigObject(config)).resolves.not.toThrow()
|
||||
|
||||
const svg = document.getElementById('viewer').innerHTML
|
||||
|
||||
// Should generate valid SVG with escaped content (basic check)
|
||||
expect(svg).toContain('<svg')
|
||||
expect(svg).toContain('viewBox')
|
||||
// Characters are XML-escaped by escapeXml function (tested in generator.test.js)
|
||||
})
|
||||
|
||||
it('should handle empty CSV gracefully', async () => {
|
||||
const config = createSampleProject()
|
||||
|
||||
mockFetch('/* test css */') // CSS
|
||||
mockFetch(createSampleTemplate())
|
||||
mockFetch('')
|
||||
|
||||
global.Papa.parse.mockImplementation((text, options) => {
|
||||
options.complete({ data: [] })
|
||||
})
|
||||
|
||||
// Should not throw error
|
||||
await expect(timelineEngine.loadProjectConfigObject(config)).resolves.not.toThrow()
|
||||
|
||||
// Should show message when no valid items found (not crash)
|
||||
const viewer = document.getElementById('viewer').innerHTML
|
||||
expect(viewer).toContain('Keine gültigen Items gefunden')
|
||||
})
|
||||
|
||||
it('should reject malformed template-v2 (missing month-template)', async () => {
|
||||
const config = createSampleProject()
|
||||
const malformedTemplate = createMalformedTemplate('month-template')
|
||||
|
||||
mockFetch('/* test css */') // CSS
|
||||
mockFetch(malformedTemplate)
|
||||
mockFetch(createSampleCSV())
|
||||
mockPapaParse()
|
||||
|
||||
// Simulate file selection
|
||||
Object.defineProperty(projectInput, 'files', {
|
||||
value: [mockFile],
|
||||
writable: false
|
||||
})
|
||||
// Should handle gracefully without crashing
|
||||
await timelineEngine.loadProjectConfigObject(config)
|
||||
|
||||
// Trigger the event
|
||||
const event = new Event('change')
|
||||
projectInput.dispatchEvent(event)
|
||||
|
||||
// Wait for async operations
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
|
||||
expect(document.getElementById('projectName').textContent).toBe('Test Project')
|
||||
const viewer = document.getElementById('viewer').innerHTML
|
||||
// Either shows error message or handles gracefully
|
||||
expect(viewer).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should handle CSV file upload', async () => {
|
||||
it('should reject malformed template-v2 (missing lane-template)', async () => {
|
||||
const config = createSampleProject()
|
||||
timelineEngine.config = config
|
||||
const malformedTemplate = createMalformedTemplate('lane-template')
|
||||
|
||||
const csvInput = document.createElement('input')
|
||||
csvInput.id = 'csvInput'
|
||||
document.body.appendChild(csvInput)
|
||||
mockFetch('/* test css */') // CSS
|
||||
mockFetch(malformedTemplate)
|
||||
mockFetch(createSampleCSV())
|
||||
mockPapaParse()
|
||||
|
||||
// Setup event handlers
|
||||
window.setupEventHandlers()
|
||||
// Should handle gracefully without crashing
|
||||
await timelineEngine.loadProjectConfigObject(config)
|
||||
|
||||
const csvContent = createSampleCSV()
|
||||
const mockFile = new File([csvContent], 'data.csv', { type: 'text/csv' })
|
||||
mockFile.text = vi.fn().mockResolvedValue(csvContent)
|
||||
const viewer = document.getElementById('viewer').innerHTML
|
||||
expect(viewer).toBeTruthy()
|
||||
})
|
||||
|
||||
global.Papa.parse.mockImplementation((text, options) => {
|
||||
options.complete({
|
||||
data: [{ ID: 'T-1', Title: 'Uploaded Task', Lane: 'Upload Lane', Due: '2025-01-15' }]
|
||||
})
|
||||
})
|
||||
it('should reject malformed template-v2 (missing item-template)', async () => {
|
||||
const config = createSampleProject()
|
||||
const malformedTemplate = createMalformedTemplate('item-template')
|
||||
|
||||
Object.defineProperty(csvInput, 'files', {
|
||||
value: [mockFile],
|
||||
writable: false
|
||||
})
|
||||
mockFetch('/* test css */') // CSS
|
||||
mockFetch(malformedTemplate)
|
||||
mockFetch(createSampleCSV())
|
||||
mockPapaParse()
|
||||
|
||||
const event = new Event('change')
|
||||
csvInput.dispatchEvent(event)
|
||||
// Should handle gracefully without crashing
|
||||
await timelineEngine.loadProjectConfigObject(config)
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
const viewer = document.getElementById('viewer').innerHTML
|
||||
expect(viewer).toBeTruthy()
|
||||
})
|
||||
|
||||
expect(timelineEngine.csvOverride).toBe(true)
|
||||
expect(document.getElementById('viewer').innerHTML).toContain('Uploaded Task')
|
||||
it('should preserve template styling and definitions', async () => {
|
||||
const config = createSampleProject()
|
||||
|
||||
// Read actual template-v2.svg to test real styling preservation
|
||||
const templateV2 = await fs.readFile('./example/template-v2.svg', 'utf-8')
|
||||
|
||||
mockFetch('/* test css */') // CSS
|
||||
mockFetch(templateV2)
|
||||
mockFetch(createSampleCSV())
|
||||
mockPapaParse()
|
||||
|
||||
await timelineEngine.loadProjectConfigObject(config)
|
||||
|
||||
const svg = document.getElementById('viewer').innerHTML
|
||||
|
||||
// Template elements are removed during generation (generator.js:202-205)
|
||||
// Instead verify that the template was successfully used to generate content
|
||||
expect(svg).toContain('<svg')
|
||||
expect(svg).toContain('viewBox')
|
||||
|
||||
// Should have generated content from the template
|
||||
expect(svg).toContain('First Task') // Item from CSV
|
||||
expect(svg).toContain('Development') // Lane from CSV
|
||||
|
||||
// Should have <defs> section (for gradients, filters, etc. but not template elements)
|
||||
expect(svg).toContain('<defs')
|
||||
})
|
||||
})
|
||||
|
||||
// DOM Event Handling tests removed - individual file uploads replaced with folder picker
|
||||
|
||||
describe('SVG Export', () => {
|
||||
it('should hide IDs in external view for export', async () => {
|
||||
// Setup timeline with items
|
||||
const config = createSampleProject()
|
||||
mockFetch('/* test css */') // CSS
|
||||
mockFetch(createSampleTemplate())
|
||||
mockFetch(createSampleCSV())
|
||||
|
||||
@@ -231,5 +352,29 @@ describe('Timeline Integration', () => {
|
||||
|
||||
mockCreateElement.mockRestore()
|
||||
})
|
||||
|
||||
it('should generate downloadable SVG with proper dimensions', async () => {
|
||||
const config = createSampleProject()
|
||||
mockFetch('/* test css */') // CSS
|
||||
mockFetch(createSampleTemplate())
|
||||
mockFetch(createSampleCSV())
|
||||
mockPapaParse()
|
||||
|
||||
await timelineEngine.loadProjectConfigObject(config)
|
||||
|
||||
const svg = document.querySelector('#viewer svg')
|
||||
|
||||
// Verify SVG has width and height attributes
|
||||
expect(svg.getAttribute('width')).toBeTruthy()
|
||||
expect(svg.getAttribute('height')).toBeTruthy()
|
||||
expect(svg.getAttribute('viewBox')).toBeTruthy()
|
||||
|
||||
// Verify viewBox matches dimensions
|
||||
const width = svg.getAttribute('width')
|
||||
const height = svg.getAttribute('height')
|
||||
const viewBox = svg.getAttribute('viewBox')
|
||||
expect(viewBox).toContain(width)
|
||||
expect(viewBox).toContain(height)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -43,12 +43,100 @@ T-1,First Task,Development,2025-01-15
|
||||
T-2,Second Task,Testing,2025-02-20
|
||||
T-3,Third Task,Development,2025-03-10`
|
||||
|
||||
// Create a proper template-v2.svg format with template elements in defs
|
||||
export const createSampleTemplate = () => `<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<!-- Month template -->
|
||||
<g id="month-template" style="display:none">
|
||||
<line x1="{{MONTH_X}}" y1="{{GRID_TOP}}" x2="{{MONTH_X}}" y2="{{GRID_BOTTOM}}" stroke="#E0E0E0" stroke-width="1"/>
|
||||
<rect x="{{MONTH_X_OFFSET}}" y="{{MONTH_LABEL_Y_OFFSET}}" width="60" height="24" fill="#F5F5F5" rx="4"/>
|
||||
<text x="{{MONTH_TEXT_X}}" y="{{MONTH_LABEL_Y}}" font-family="Arial" font-size="12" fill="#424242">{{MONTH_LABEL}}</text>
|
||||
<line x1="{{MONTH_SEP_X}}" y1="{{GRID_TOP}}" x2="{{MONTH_SEP_X}}" y2="{{GRID_BOTTOM}}" stroke="#BDBDBD" stroke-width="2"/>
|
||||
</g>
|
||||
|
||||
<!-- Lane template -->
|
||||
<g id="lane-template" style="display:none">
|
||||
<rect x="{{LANE_X}}" y="{{LANE_Y}}" width="{{LANE_WIDTH}}" height="{{LANE_HEIGHT}}" fill="#FAFAFA" stroke="#E0E0E0" rx="8"/>
|
||||
<text x="{{LABEL_X}}" y="{{LABEL_Y}}" font-family="Arial" font-size="14" font-weight="bold" fill="#212121">{{LANE_NAME}}</text>
|
||||
</g>
|
||||
|
||||
<!-- Task template -->
|
||||
<g id="item-template" style="display:none">
|
||||
<circle cx="{{ITEM_X}}" cy="{{ITEM_Y}}" r="6" fill="#1976D2"/>
|
||||
<text x="{{TEXT_X}}" y="{{TEXT_Y}}" font-family="Arial" font-size="11" fill="#424242">{{ITEM_ID}} {{ITEM_TITLE}}</text>
|
||||
</g>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#FFFFFF"/>
|
||||
{{MONTHS}}
|
||||
{{LANES}}
|
||||
</svg>`
|
||||
|
||||
// Create a malformed template missing required elements (for error testing)
|
||||
export const createMalformedTemplate = (missingElement = 'month-template') => {
|
||||
const templates = {
|
||||
'month-template': `<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<g id="lane-template" style="display:none">
|
||||
<rect x="{{LANE_X}}" y="{{LANE_Y}}" width="{{LANE_WIDTH}}" height="{{LANE_HEIGHT}}" fill="#FAFAFA"/>
|
||||
</g>
|
||||
<g id="item-template" style="display:none">
|
||||
<circle cx="{{ITEM_X}}" cy="{{ITEM_Y}}" r="6" fill="#1976D2"/>
|
||||
</g>
|
||||
</defs>
|
||||
{{MONTHS}}
|
||||
{{LANES}}
|
||||
</svg>`,
|
||||
'lane-template': `<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<g id="month-template" style="display:none">
|
||||
<line x1="{{MONTH_X}}" y1="{{GRID_TOP}}" x2="{{MONTH_X}}" y2="{{GRID_BOTTOM}}" stroke="#E0E0E0"/>
|
||||
</g>
|
||||
<g id="item-template" style="display:none">
|
||||
<circle cx="{{ITEM_X}}" cy="{{ITEM_Y}}" r="6" fill="#1976D2"/>
|
||||
</g>
|
||||
</defs>
|
||||
{{MONTHS}}
|
||||
{{LANES}}
|
||||
</svg>`,
|
||||
'item-template': `<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<g id="month-template" style="display:none">
|
||||
<line x1="{{MONTH_X}}" y1="{{GRID_TOP}}" x2="{{MONTH_X}}" y2="{{GRID_BOTTOM}}" stroke="#E0E0E0"/>
|
||||
</g>
|
||||
<g id="lane-template" style="display:none">
|
||||
<rect x="{{LANE_X}}" y="{{LANE_Y}}" width="{{LANE_WIDTH}}" height="{{LANE_HEIGHT}}" fill="#FAFAFA"/>
|
||||
</g>
|
||||
</defs>
|
||||
{{MONTHS}}
|
||||
{{LANES}}
|
||||
</svg>`
|
||||
}
|
||||
return templates[missingElement] || templates['month-template']
|
||||
}
|
||||
|
||||
// Create a large dataset for stress testing (50+ items)
|
||||
export const createLargeDataset = (itemCount = 60) => {
|
||||
const lanes = ['Development', 'Testing', 'Design', 'DevOps', 'Documentation']
|
||||
const startDate = new Date('2025-01-01')
|
||||
const items = []
|
||||
|
||||
for (let i = 1; i <= itemCount; i++) {
|
||||
const daysOffset = Math.floor(i * 8) // Spread items across ~480 days (16 months)
|
||||
const dueDate = new Date(startDate)
|
||||
dueDate.setDate(dueDate.getDate() + daysOffset)
|
||||
|
||||
items.push({
|
||||
id: `TASK-${i}`,
|
||||
title: `Task ${i}: Implementation Item`,
|
||||
lane: lanes[i % lanes.length],
|
||||
due: dueDate
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
export const mockFetch = (data, ok = true) => {
|
||||
global.fetch.mockResolvedValueOnce({
|
||||
ok,
|
||||
|
||||