lifecycle-timeline 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +101 -0
- package/dist/timeline.css +1 -0
- package/dist/timeline.js +249 -0
- package/dist/timeline.umd.cjs +25 -0
- package/package.json +51 -0
- package/src/index.d.ts +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Eric REBOISSON
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# 🕒 Lifecycle Timeline
|
|
2
|
+
|
|
3
|
+
A premium, interactive Vanilla JS component for visualizing product lifecycles, including OSS support, Enterprise support, and EOL (End-Of-Life) dates.
|
|
4
|
+
|
|
5
|
+
## ✨ Features
|
|
6
|
+
|
|
7
|
+
- **Smart Filtering**: Real-time search to filter versions.
|
|
8
|
+
- **Dark Mode**: Native support with a persistent toggle.
|
|
9
|
+
- **Rich Legend**: Detailed explanation of support states.
|
|
10
|
+
- **Responsive Design**: Works on all screen sizes with horizontal scroll support.
|
|
11
|
+
- **Sticky Labels**: Version names stay visible while scrolling through time.
|
|
12
|
+
- **Interactive Tooltips**: Detailed date information on hover.
|
|
13
|
+
- **Live Indicator**: Pulsing badge showing the current date line.
|
|
14
|
+
- **Fully Typed**: Includes TypeScript definitions out of the box.
|
|
15
|
+
|
|
16
|
+
## 🚀 Installation
|
|
17
|
+
|
|
18
|
+
### Via NPM
|
|
19
|
+
```bash
|
|
20
|
+
npm install lifecycle-timeline
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Manual Installation
|
|
24
|
+
Download the files from the `dist` folder: `timeline.js` (ESM), `timeline.umd.cjs` (UMD), and `timeline.css`.
|
|
25
|
+
|
|
26
|
+
## 🛠 Usage
|
|
27
|
+
|
|
28
|
+
### Modern JavaScript (ESM)
|
|
29
|
+
```javascript
|
|
30
|
+
import Timeline from 'lifecycle-timeline';
|
|
31
|
+
import 'lifecycle-timeline/style.css';
|
|
32
|
+
|
|
33
|
+
const data = [
|
|
34
|
+
{
|
|
35
|
+
version: "6.0.x",
|
|
36
|
+
ossStart: "2025-01-01",
|
|
37
|
+
ossEnd: "2026-08-20",
|
|
38
|
+
enterpriseEnd: "2027-02-15",
|
|
39
|
+
releaseNotesUrl: "https://example.com/notes"
|
|
40
|
+
}
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
new Timeline('timeline-root', data, { visibleCount: 3 });
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Browser (UMD)
|
|
47
|
+
```html
|
|
48
|
+
<link rel="stylesheet" href="https://unpkg.com/lifecycle-timeline/dist/timeline.css">
|
|
49
|
+
<div id="timeline-root"></div>
|
|
50
|
+
|
|
51
|
+
<script src="https://unpkg.com/lifecycle-timeline/dist/timeline.umd.cjs"></script>
|
|
52
|
+
<script>
|
|
53
|
+
const data = [...];
|
|
54
|
+
new Timeline('timeline-root', data);
|
|
55
|
+
</script>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## ⚙️ Configuration
|
|
59
|
+
|
|
60
|
+
### Constructor
|
|
61
|
+
`new Timeline(elementId, data, options)`
|
|
62
|
+
|
|
63
|
+
| Argument | Type | Description |
|
|
64
|
+
| :--- | :--- | :--- |
|
|
65
|
+
| `elementId` | `string` | The ID of the container element. |
|
|
66
|
+
| `data` | `Array` | List of version objects. |
|
|
67
|
+
| `options` | `Object` | Optional configuration. |
|
|
68
|
+
|
|
69
|
+
### Data Schema
|
|
70
|
+
Each version object in the `data` array should follow this structure:
|
|
71
|
+
- `version`: (String) Version name/number.
|
|
72
|
+
- `ossStart`: (String) Start date of OSS support (YYYY-MM-DD).
|
|
73
|
+
- `ossEnd`: (String) End date of OSS support (YYYY-MM-DD).
|
|
74
|
+
- `enterpriseEnd`: (String) End date of Enterprise support (YYYY-MM-DD).
|
|
75
|
+
- `releaseNotesUrl`: (String, Optional) Link to release notes.
|
|
76
|
+
|
|
77
|
+
### Options
|
|
78
|
+
- `visibleCount`: (Number, default: `3`) Number of versions to show before the "Show More" button appears.
|
|
79
|
+
- `locale`: (String, default: browser detect) Preferred language for the UI. Supported: `'en'`, `'fr'`.
|
|
80
|
+
|
|
81
|
+
## 🎨 Theming
|
|
82
|
+
The component uses CSS variables for easy customization. You can override them in your own CSS:
|
|
83
|
+
|
|
84
|
+
```css
|
|
85
|
+
:root {
|
|
86
|
+
--accent-oss: #99e67d; /* Color for OSS support bars */
|
|
87
|
+
--accent-ent: #ffe88e; /* Color for Enterprise support bars */
|
|
88
|
+
--current-date: #086dc3; /* Color for the current date line */
|
|
89
|
+
--bg-primary: #ffffff; /* Light mode background */
|
|
90
|
+
--text-primary: #1e293b; /* Light mode text */
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 🛠 Development
|
|
95
|
+
|
|
96
|
+
1. Install dependencies: `npm install`
|
|
97
|
+
2. Start development server: `npm run dev`
|
|
98
|
+
3. Build for production: `npm run build`
|
|
99
|
+
|
|
100
|
+
## 📄 License
|
|
101
|
+
MIT © Eric REBOISSON
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--bg-page: #f9fafb;--bg-card: #ffffff;--bg-track: #e5e7eb;--text-primary: #1f2937;--text-secondary: #6b7280;--text-inverse: #ffffff;--border-color: #e5e7eb;--line-color: #e5e7eb;--grid-line: #9ca3af;--accent-oss: #99e67d;--accent-ent: #ffe88e;--current-date: #086dc3;--font-family: "Inter", system-ui, -apple-system, sans-serif;--tooltip-bg: #ffffff;--tooltip-shadow: rgba(0, 0, 0, .1)}[data-theme=dark]{--bg-page: #111827;--bg-card: #1f2937;--bg-track: #374151;--text-primary: #f9fafb;--text-secondary: #9ca3af;--border-color: #374151;--line-color: #374151;--grid-line: #4b5563;--tooltip-bg: #1f2937;--tooltip-shadow: rgba(0, 0, 0, .5);--accent-oss: #78bd5d;--accent-ent: #e3c456}body{margin:0;padding:0;background-color:var(--bg-page);font-family:var(--font-family);color:var(--text-primary);display:flex;justify-content:center;align-items:center;min-height:100vh;transition:background-color .3s ease,color .3s ease}.page-container{width:100%;max-width:1200px;padding:2rem;background:var(--bg-card);border-radius:12px;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;transition:background-color .3s ease}.timeline-wrapper{position:relative;padding-top:40px;padding-bottom:60px;overflow-x:auto;scrollbar-width:thin}.timeline-header{display:flex;position:sticky;left:0;margin-bottom:20px}.timeline-tracks{position:relative;padding-top:20px}.timeline-axis{display:flex;border-bottom:1px solid var(--border-color);margin-bottom:10px;padding-bottom:8px;position:relative}.timeline-year{flex:1;font-size:.875rem;font-weight:600;color:var(--text-secondary);border-left:1px dashed var(--border-color);padding-left:0;text-align:center;box-sizing:border-box}.timeline-year:first-child{border-left:none}.timeline-row{display:flex;align-items:center;margin-bottom:1.5rem;position:relative;height:36px;z-index:1}.version-label{width:80px;min-width:80px;box-sizing:border-box;padding:4px;border-radius:12px;flex-shrink:0;font-weight:600;font-size:.85rem;color:var(--text-primary);margin-right:16px;text-align:center;background-color:transparent;transition:background-color .3s ease,color .3s ease;position:sticky;left:0;z-index:10}.version-label a.version-link{text-decoration:none;color:inherit;display:block;width:100%;height:100%}.version-label a.version-link:hover{text-decoration:underline;opacity:.8}.status-oss{background-color:var(--accent-oss);color:#000}.status-ent{background-color:var(--accent-ent);color:#000}.status-expired{background-color:var(--bg-track);color:var(--text-secondary)}.track-container{flex-grow:1;position:relative;height:100%;background:var(--bg-track);border-radius:0;overflow:hidden;transition:background-color .3s ease}.bar-segment{position:absolute;height:100%;top:0;transition:opacity .3s ease;display:flex;align-items:center;justify-content:center;color:#fff;font-size:.75rem;font-weight:500;overflow:hidden;white-space:nowrap;box-shadow:0 1px 2px #0000001a}.bar-oss{background:linear-gradient(to bottom,#dcfce7 0%,var(--accent-oss) 100%);z-index:2;border-radius:0;height:66%;top:auto;bottom:0}[data-theme=dark] .bar-oss{background:linear-gradient(to bottom,#a7f3d0 0%,var(--accent-oss) 100%)}.bar-ent{background:linear-gradient(to bottom,#fef9c3 0%,var(--accent-ent) 100%);z-index:1;border-radius:0;opacity:.9}.custom-tooltip{position:fixed;background:var(--tooltip-bg);color:var(--text-primary);padding:12px;border-radius:8px;font-size:.85rem;pointer-events:none;z-index:1000;opacity:0;transition:opacity .1s ease;box-shadow:0 10px 15px -3px var(--tooltip-shadow),0 4px 6px -2px var(--tooltip-shadow);border:1px solid var(--border-color);min-width:200px}.tooltip-header{font-weight:700;color:var(--text-primary);font-size:.9rem;border-bottom:1px solid var(--border-color);padding-bottom:4px;margin-bottom:8px}.tooltip-row{display:flex;justify-content:space-between;margin-bottom:4px;font-size:.8rem}.tooltip-label{color:var(--text-secondary)}.tooltip-value{font-weight:500}.grid-lines-container{position:absolute;inset:0 0 0 96px;pointer-events:none;z-index:5}.current-date-indicator{position:absolute;top:-30px;bottom:0;width:3px;background-image:linear-gradient(to bottom,var(--current-date) 60%,transparent 60%);background-size:3px 20px;background-repeat:repeat-y;border-left:none;z-index:20;pointer-events:none}@keyframes pulse-blue{0%{box-shadow:0 0 #086dc3b3}70%{box-shadow:0 0 0 6px #086dc300}to{box-shadow:0 0 #086dc300}}.current-date-badge{position:absolute;top:100%;margin-top:8px;left:50%;transform:translate(-50%);background-color:var(--current-date);color:#fff;padding:4px 12px;border-radius:12px;font-size:.75rem;font-weight:600;white-space:nowrap;box-shadow:0 2px 4px #086dc333;z-index:25;animation:pulse-blue 2s infinite}.year-grid-line{position:absolute;top:0;bottom:0;border-left:1px solid var(--grid-line);opacity:.4;z-index:0;pointer-events:none}[title]{cursor:help}@media(max-width:768px){.page-container{padding:1rem}.timeline-wrapper{overflow-x:scroll}.track-container{min-width:600px}}.release-legend{margin:40px auto 0;padding:24px;background-color:var(--bg-card);border:1px solid var(--border-color);border-radius:8px;display:flex;flex-direction:column;gap:16px;max-width:800px}.legend-block{display:flex;align-items:flex-start;gap:12px}.legend-icon{width:20px;height:20px;border-radius:4px;flex-shrink:0;margin-top:2px}.legend-block h3{margin:0 0 4px;font-size:1rem;font-weight:700;color:var(--text-primary)}.legend-block p{margin:0;font-size:.9rem;color:var(--text-secondary);line-height:1.5}.legend-block.oss .legend-icon{background-color:var(--accent-oss)}.legend-block.commercial .legend-icon{background-color:var(--accent-ent)}.legend-block.expired .legend-icon{background-color:var(--bg-track)}.timeline-toggle-btn{display:flex;align-items:center;gap:8px;background:var(--bg-card);border:1px solid var(--border-color);padding:8px 16px;border-radius:20px;cursor:pointer;font-size:.875rem;font-weight:500;color:var(--text-primary);box-shadow:0 1px 2px #0000000d;transition:all .2s ease}.timeline-toggle-btn:hover{background:var(--bg-page);border-color:var(--text-secondary)}.timeline-toggle-btn svg{display:block}.timeline-toolbar{display:flex;justify-content:flex-start;align-items:center;margin-bottom:20px;gap:16px}.timeline-filter-container{display:flex;align-items:center;gap:12px;background:var(--bg-card);border:1px solid var(--border-color);padding:8px 16px;border-radius:24px;max-width:320px;flex-grow:1;transition:all .2s ease;box-shadow:0 2px 4px #0000000d}.timeline-filter-container:focus-within{border-color:var(--current-date);box-shadow:0 4px 12px #086dc31a}.search-icon{color:var(--text-secondary);display:flex;align-items:center;opacity:.7}.timeline-filter-input{border:none;outline:none;font-size:.95rem;color:var(--text-primary);background:transparent;width:100%;min-height:24px}.theme-toggle-btn{position:absolute;top:0;right:0;background:var(--bg-card);border:1px solid var(--border-color);color:var(--text-primary);width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .2s ease;z-index:1000;box-shadow:0 2px 5px #0000001a}#timeline-root{position:relative}.theme-toggle-btn:hover{background-color:var(--bg-track);transform:scale(1.05)}
|
package/dist/timeline.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
class g {
|
|
2
|
+
constructor(e, n, i = {}) {
|
|
3
|
+
if (this.root = document.getElementById(e), this.data = (n || []).filter((t) => t), this.visibleCount = i.visibleCount || 3, this.isExpanded = this.safeStorageGet("timeline_isExpanded") === "true", this.currentDate = /* @__PURE__ */ new Date(), this.minYear = (/* @__PURE__ */ new Date()).getFullYear() - 1, this.maxYear = (/* @__PURE__ */ new Date()).getFullYear() + 3, this.data.length > 0) {
|
|
4
|
+
const t = this.data.flatMap((s) => [new Date(s.ossStart), new Date(s.enterpriseEnd)]);
|
|
5
|
+
this.minYear = new Date(Math.min(...t)).getFullYear(), this.maxYear = new Date(Math.max(...t)).getFullYear();
|
|
6
|
+
}
|
|
7
|
+
this.filterText = "", this.translations = {
|
|
8
|
+
en: {
|
|
9
|
+
filterPlaceholder: "Filter versions...",
|
|
10
|
+
legendOssTitle: "OSS support",
|
|
11
|
+
legendOssDesc: "Free security updates and bugfixes.",
|
|
12
|
+
legendEntTitle: "Enterprise support",
|
|
13
|
+
legendEntDesc: "Enterprise support from experts during the OSS timeline, plus extended support after OSS End-Of-Life.",
|
|
14
|
+
legendEolTitle: "Out of Support",
|
|
15
|
+
legendEolDesc: "Generation has reached end of life. No further updates are provided.",
|
|
16
|
+
showLess: "Show Less",
|
|
17
|
+
showMore: "Show {n} more versions",
|
|
18
|
+
ossSupport: "OSS Support",
|
|
19
|
+
entSupport: "Enterprise Support",
|
|
20
|
+
toggleDarkMode: "Toggle Dark Mode",
|
|
21
|
+
viewReleaseNotes: "View Release Notes for {v}"
|
|
22
|
+
},
|
|
23
|
+
fr: {
|
|
24
|
+
filterPlaceholder: "Filtrer les versions...",
|
|
25
|
+
legendOssTitle: "Support OSS",
|
|
26
|
+
legendOssDesc: "Mises à jour de sécurité et corrections de bugs gratuites.",
|
|
27
|
+
legendEntTitle: "Support Entreprise",
|
|
28
|
+
legendEntDesc: "Support entreprise par des experts pendant la période OSS, plus support étendu après la fin de vie OSS.",
|
|
29
|
+
legendEolTitle: "Fin de support",
|
|
30
|
+
legendEolDesc: "Cette version a atteint sa fin de vie. Plus aucune mise à jour n'est fournie.",
|
|
31
|
+
showLess: "Voir moins",
|
|
32
|
+
showMore: "Voir {n} versions supplémentaires",
|
|
33
|
+
ossSupport: "Support OSS",
|
|
34
|
+
entSupport: "Support Entreprise",
|
|
35
|
+
toggleDarkMode: "Changer le mode sombre",
|
|
36
|
+
viewReleaseNotes: "Voir les notes de version pour {v}"
|
|
37
|
+
}
|
|
38
|
+
}, this.locale = i.locale || this.detectLanguage();
|
|
39
|
+
try {
|
|
40
|
+
this.init();
|
|
41
|
+
} catch (t) {
|
|
42
|
+
console.error("Timeline Init Error:", t);
|
|
43
|
+
const s = document.createElement("div");
|
|
44
|
+
s.style.color = "red", s.style.padding = "20px", s.textContent = `Error initializing timeline: ${t.message}`, this.root && this.root.prepend(s);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
detectLanguage() {
|
|
48
|
+
const e = (navigator.language || navigator.userLanguage || "en").split("-")[0];
|
|
49
|
+
return this.translations[e] ? e : "en";
|
|
50
|
+
}
|
|
51
|
+
t(e, n = {}) {
|
|
52
|
+
let i = (this.translations[this.locale] || this.translations.en)[e] || e;
|
|
53
|
+
return Object.keys(n).forEach((t) => {
|
|
54
|
+
i = i.replace(`{${t}}`, n[t]);
|
|
55
|
+
}), i;
|
|
56
|
+
}
|
|
57
|
+
init() {
|
|
58
|
+
this.root && (this.renderToolbar(), this.wrapper = document.createElement("div"), this.wrapper.className = "timeline-wrapper", this.root.appendChild(this.wrapper), this.axisContainer = document.createElement("div"), this.axisContainer.className = "timeline-axis", this.wrapper.appendChild(this.axisContainer), this.renderAxis(), this.tracksContainer = document.createElement("div"), this.tracksContainer.className = "timeline-tracks", this.wrapper.appendChild(this.tracksContainer), this.createRowElements(), this.updateVisibility(), this.renderLegend(), document.querySelector(".custom-tooltip") ? this.tooltip = document.querySelector(".custom-tooltip") : (this.tooltip = document.createElement("div"), this.tooltip.className = "custom-tooltip", document.body.appendChild(this.tooltip)), this.theme = this.safeStorageGet("timeline_theme") || "light", document.documentElement.setAttribute("data-theme", this.theme), this.startAutoUpdate(), this.renderThemeToggle());
|
|
59
|
+
}
|
|
60
|
+
renderToolbar() {
|
|
61
|
+
const e = document.createElement("div");
|
|
62
|
+
e.className = "timeline-toolbar";
|
|
63
|
+
const n = document.createElement("div");
|
|
64
|
+
n.className = "timeline-filter-container";
|
|
65
|
+
const i = document.createElement("div");
|
|
66
|
+
i.className = "search-icon", i.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>';
|
|
67
|
+
const t = document.createElement("div");
|
|
68
|
+
t.className = "timeline-filter-input", t.contentEditable = "true", t.setAttribute("spellcheck", "false"), t.style.cursor = "text", t.style.minWidth = "150px", t.style.display = "inline-block", this.filterText || (t.textContent = this.t("filterPlaceholder"), t.style.color = "var(--text-secondary)"), t.onfocus = () => {
|
|
69
|
+
t.textContent === this.t("filterPlaceholder") && (t.textContent = "", t.style.color = "var(--text-primary)");
|
|
70
|
+
}, t.onblur = () => {
|
|
71
|
+
t.textContent.trim() === "" && (t.textContent = this.t("filterPlaceholder"), t.style.color = "var(--text-secondary)");
|
|
72
|
+
}, t.onkeydown = (r) => {
|
|
73
|
+
if (r.key === "Enter")
|
|
74
|
+
return r.preventDefault(), !1;
|
|
75
|
+
};
|
|
76
|
+
let s;
|
|
77
|
+
t.oninput = (r) => {
|
|
78
|
+
r.stopPropagation(), t.style.color = "var(--text-primary)", clearTimeout(s), s = setTimeout(() => {
|
|
79
|
+
const o = t.textContent === this.t("filterPlaceholder") ? "" : t.textContent;
|
|
80
|
+
this.filterText = o.toLowerCase().trim(), this.updateVisibility();
|
|
81
|
+
}, 300);
|
|
82
|
+
}, n.appendChild(i), n.appendChild(t), e.appendChild(n), this.root.appendChild(e);
|
|
83
|
+
}
|
|
84
|
+
renderThemeToggle() {
|
|
85
|
+
if (document.querySelector(".theme-toggle-btn")) return;
|
|
86
|
+
const e = document.createElement("button");
|
|
87
|
+
e.className = "theme-toggle-btn", e.type = "button", e.title = this.t("toggleDarkMode");
|
|
88
|
+
const n = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>', i = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>';
|
|
89
|
+
e.innerHTML = this.theme === "dark" ? i : n, e.onclick = () => {
|
|
90
|
+
this.theme = this.theme === "light" ? "dark" : "light", document.documentElement.setAttribute("data-theme", this.theme), this.safeStorageSet("timeline_theme", this.theme), e.innerHTML = this.theme === "dark" ? i : n;
|
|
91
|
+
}, this.root.appendChild(e);
|
|
92
|
+
}
|
|
93
|
+
startAutoUpdate() {
|
|
94
|
+
this.autoUpdateInterval && clearInterval(this.autoUpdateInterval), this.renderCurrentDateLine();
|
|
95
|
+
}
|
|
96
|
+
renderLegend() {
|
|
97
|
+
const e = `
|
|
98
|
+
<div class="release-legend">
|
|
99
|
+
<div class="legend-block oss">
|
|
100
|
+
<div class="legend-icon"></div>
|
|
101
|
+
<div>
|
|
102
|
+
<h3>${this.t("legendOssTitle")}</h3>
|
|
103
|
+
<p>${this.t("legendOssDesc")}</p>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="legend-block commercial">
|
|
107
|
+
<div class="legend-icon"></div>
|
|
108
|
+
<div>
|
|
109
|
+
<h3>${this.t("legendEntTitle")}</h3>
|
|
110
|
+
<p>${this.t("legendEntDesc")}</p>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="legend-block expired">
|
|
114
|
+
<div class="legend-icon"></div>
|
|
115
|
+
<div>
|
|
116
|
+
<h3>${this.t("legendEolTitle")}</h3>
|
|
117
|
+
<p>${this.t("legendEolDesc")}</p>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
`, n = document.createElement("div");
|
|
122
|
+
n.innerHTML = e, this.wrapper.appendChild(n.firstElementChild);
|
|
123
|
+
}
|
|
124
|
+
renderAxis() {
|
|
125
|
+
this.axisContainer.innerHTML = "";
|
|
126
|
+
const e = document.createElement("div");
|
|
127
|
+
e.style.width = "96px", e.style.flexShrink = "0", this.axisContainer.appendChild(e);
|
|
128
|
+
const n = this.maxYear - this.minYear + 1;
|
|
129
|
+
for (let i = 0; i < n; i++) {
|
|
130
|
+
const t = this.minYear + i, s = document.createElement("div");
|
|
131
|
+
s.className = "timeline-year", s.textContent = t, this.axisContainer.appendChild(s);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
renderGrid() {
|
|
135
|
+
if (!this.gridContainer) return;
|
|
136
|
+
const e = this.getTimelineDuration(), n = this.maxYear - this.minYear + 1;
|
|
137
|
+
for (let i = 0; i < n; i++) {
|
|
138
|
+
const t = this.minYear + i, r = new Date(t, 0, 1).getTime() - this.getTimelineStart();
|
|
139
|
+
if (r >= 0 && r <= e) {
|
|
140
|
+
const o = r / e * 100, a = document.createElement("div");
|
|
141
|
+
a.className = "year-grid-line", a.style.left = `${o}%`, this.gridContainer.appendChild(a);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
createRowElements() {
|
|
146
|
+
this.tracksContainer.innerHTML = "", this.gridContainer = document.createElement("div"), this.gridContainer.className = "grid-lines-container", this.tracksContainer.appendChild(this.gridContainer), this.renderGrid(), this.renderCurrentDateLine(), this.rowElements = this.data.map((e) => {
|
|
147
|
+
const n = document.createElement("div");
|
|
148
|
+
n.className = "timeline-row";
|
|
149
|
+
const i = document.createElement("div");
|
|
150
|
+
if (i.className = "version-label", e.releaseNotesUrl) {
|
|
151
|
+
const l = document.createElement("a");
|
|
152
|
+
l.href = e.releaseNotesUrl, l.className = "version-link", l.target = "_blank", l.textContent = e.version, l.title = this.t("viewReleaseNotes", { v: e.version }), i.appendChild(l);
|
|
153
|
+
} else
|
|
154
|
+
i.textContent = e.version;
|
|
155
|
+
const t = (/* @__PURE__ */ new Date()).getTime(), s = new Date(e.ossStart).getTime(), r = new Date(e.ossEnd).getTime(), o = new Date(e.enterpriseEnd).getTime();
|
|
156
|
+
t >= s && t <= r ? i.classList.add("status-oss") : t > r && t <= o ? i.classList.add("status-ent") : t > o && i.classList.add("status-expired"), n.appendChild(i);
|
|
157
|
+
const a = document.createElement("div");
|
|
158
|
+
a.className = "track-container";
|
|
159
|
+
const c = this.createBarSegment(e.ossStart, e.ossEnd, "bar-oss", this.t("ossSupport")), h = this.createBarSegment(e.ossStart, e.enterpriseEnd, "bar-ent", this.t("entSupport"));
|
|
160
|
+
return a.appendChild(h), a.appendChild(c), n.appendChild(a), this.tracksContainer.appendChild(n), { element: n, data: e };
|
|
161
|
+
}), this.toggleBtnContainer = document.createElement("div"), this.toggleBtnContainer.style.display = "flex", this.toggleBtnContainer.style.justifyContent = "center", this.toggleBtnContainer.style.paddingTop = "10px", this.toggleBtnContainer.style.zIndex = "10", this.toggleBtnContainer.style.position = "relative", this.tracksContainer.appendChild(this.toggleBtnContainer);
|
|
162
|
+
}
|
|
163
|
+
updateVisibility() {
|
|
164
|
+
const e = (this.filterText || "").toLowerCase().trim(), n = e.length > 0, i = this.rowElements.filter((r) => !n || r.data.version.toLowerCase().includes(e));
|
|
165
|
+
this.rowElements.forEach((r) => r.element.style.display = "none");
|
|
166
|
+
let t = i;
|
|
167
|
+
const s = !n && i.length > this.visibleCount;
|
|
168
|
+
s && !this.isExpanded && (t = i.slice(0, this.visibleCount)), t.forEach((r) => {
|
|
169
|
+
r.element.style.display = "flex";
|
|
170
|
+
}), this.renderToggleButton(s, i.length - this.visibleCount);
|
|
171
|
+
}
|
|
172
|
+
renderRows() {
|
|
173
|
+
this.updateVisibility();
|
|
174
|
+
}
|
|
175
|
+
renderToggleButton(e, n) {
|
|
176
|
+
if (this.toggleBtnContainer.innerHTML = "", !e) {
|
|
177
|
+
this.toggleBtnContainer.style.display = "none";
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
this.toggleBtnContainer.style.display = "flex";
|
|
181
|
+
const i = document.createElement("button");
|
|
182
|
+
i.type = "button", i.className = "timeline-toggle-btn";
|
|
183
|
+
const t = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>', s = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"></polyline></svg>';
|
|
184
|
+
this.isExpanded ? i.innerHTML = `${this.t("showLess")} ${s}` : i.innerHTML = `${this.t("showMore", { n })} ${t}`, i.onclick = () => {
|
|
185
|
+
this.isExpanded = !this.isExpanded, this.safeStorageSet("timeline_isExpanded", this.isExpanded), this.updateVisibility();
|
|
186
|
+
}, this.toggleBtnContainer.appendChild(i);
|
|
187
|
+
}
|
|
188
|
+
createBarSegment(e, n, i, t) {
|
|
189
|
+
const s = new Date(e), r = new Date(n), o = this.getTimelineDuration(), a = s - this.getTimelineStart(), c = r - s, h = a / o * 100, l = c / o * 100, d = document.createElement("div");
|
|
190
|
+
d.className = `bar-segment ${i}`, d.style.left = `${h}%`, d.style.width = `${l}%`;
|
|
191
|
+
const u = `${t}: ${e} to ${n}`;
|
|
192
|
+
return d.addEventListener("mouseenter", (p) => {
|
|
193
|
+
this.tooltip && (this.tooltip.textContent = u, this.tooltip.style.opacity = "1", this.updateTooltipPosition(p));
|
|
194
|
+
}), d.addEventListener("mousemove", (p) => {
|
|
195
|
+
this.updateTooltipPosition(p);
|
|
196
|
+
}), d.addEventListener("mouseleave", () => {
|
|
197
|
+
this.tooltip && (this.tooltip.style.opacity = "0");
|
|
198
|
+
}), d;
|
|
199
|
+
}
|
|
200
|
+
updateTooltipPosition(e) {
|
|
201
|
+
if (!this.tooltip) return;
|
|
202
|
+
const n = e.clientX + 12, i = e.clientY + 12;
|
|
203
|
+
this.tooltip.style.left = `${n}px`, this.tooltip.style.top = `${i}px`;
|
|
204
|
+
}
|
|
205
|
+
renderCurrentDateLine() {
|
|
206
|
+
if (!this.gridContainer) return;
|
|
207
|
+
const e = /* @__PURE__ */ new Date(), n = this.getTimelineDuration(), i = e - this.getTimelineStart();
|
|
208
|
+
if (i < 0 || i > n) return;
|
|
209
|
+
const t = i / n * 100;
|
|
210
|
+
if (this.currentDateLine && this.gridContainer.contains(this.currentDateLine)) {
|
|
211
|
+
this.currentDateLine.style.left = `${t}%`;
|
|
212
|
+
const o = this.currentDateLine.querySelector(".current-date-badge");
|
|
213
|
+
o && (o.textContent = e.toISOString().split("T")[0]);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const s = document.createElement("div");
|
|
217
|
+
s.className = "current-date-indicator", s.style.left = `${t}%`;
|
|
218
|
+
const r = document.createElement("div");
|
|
219
|
+
r.className = "current-date-badge", r.textContent = e.toISOString().split("T")[0], s.appendChild(r), this.currentDateLine = s, this.gridContainer.appendChild(s);
|
|
220
|
+
}
|
|
221
|
+
// Helpers
|
|
222
|
+
getTimelineStart() {
|
|
223
|
+
return new Date(this.minYear, 0, 1).getTime();
|
|
224
|
+
}
|
|
225
|
+
getTimelineEnd() {
|
|
226
|
+
return new Date(this.maxYear, 11, 31).getTime();
|
|
227
|
+
}
|
|
228
|
+
getTimelineDuration() {
|
|
229
|
+
return this.getTimelineEnd() - this.getTimelineStart();
|
|
230
|
+
}
|
|
231
|
+
// Safe LocalStorage Helpers
|
|
232
|
+
safeStorageGet(e) {
|
|
233
|
+
try {
|
|
234
|
+
return localStorage.getItem(e);
|
|
235
|
+
} catch {
|
|
236
|
+
return console.warn("LocalStorage access denied via safeStorageGet"), null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
safeStorageSet(e, n) {
|
|
240
|
+
try {
|
|
241
|
+
localStorage.setItem(e, n);
|
|
242
|
+
} catch {
|
|
243
|
+
console.warn("LocalStorage access denied via safeStorageSet");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export {
|
|
248
|
+
g as default
|
|
249
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
(function(c,h){typeof exports=="object"&&typeof module<"u"?module.exports=h():typeof define=="function"&&define.amd?define(h):(c=typeof globalThis<"u"?globalThis:c||self,c.Timeline=h())})(this,(function(){"use strict";class c{constructor(e,n,i={}){if(this.root=document.getElementById(e),this.data=(n||[]).filter(t=>t),this.visibleCount=i.visibleCount||3,this.isExpanded=this.safeStorageGet("timeline_isExpanded")==="true",this.currentDate=new Date,this.minYear=new Date().getFullYear()-1,this.maxYear=new Date().getFullYear()+3,this.data.length>0){const t=this.data.flatMap(s=>[new Date(s.ossStart),new Date(s.enterpriseEnd)]);this.minYear=new Date(Math.min(...t)).getFullYear(),this.maxYear=new Date(Math.max(...t)).getFullYear()}this.filterText="",this.translations={en:{filterPlaceholder:"Filter versions...",legendOssTitle:"OSS support",legendOssDesc:"Free security updates and bugfixes.",legendEntTitle:"Enterprise support",legendEntDesc:"Enterprise support from experts during the OSS timeline, plus extended support after OSS End-Of-Life.",legendEolTitle:"Out of Support",legendEolDesc:"Generation has reached end of life. No further updates are provided.",showLess:"Show Less",showMore:"Show {n} more versions",ossSupport:"OSS Support",entSupport:"Enterprise Support",toggleDarkMode:"Toggle Dark Mode",viewReleaseNotes:"View Release Notes for {v}"},fr:{filterPlaceholder:"Filtrer les versions...",legendOssTitle:"Support OSS",legendOssDesc:"Mises à jour de sécurité et corrections de bugs gratuites.",legendEntTitle:"Support Entreprise",legendEntDesc:"Support entreprise par des experts pendant la période OSS, plus support étendu après la fin de vie OSS.",legendEolTitle:"Fin de support",legendEolDesc:"Cette version a atteint sa fin de vie. Plus aucune mise à jour n'est fournie.",showLess:"Voir moins",showMore:"Voir {n} versions supplémentaires",ossSupport:"Support OSS",entSupport:"Support Entreprise",toggleDarkMode:"Changer le mode sombre",viewReleaseNotes:"Voir les notes de version pour {v}"}},this.locale=i.locale||this.detectLanguage();try{this.init()}catch(t){console.error("Timeline Init Error:",t);const s=document.createElement("div");s.style.color="red",s.style.padding="20px",s.textContent=`Error initializing timeline: ${t.message}`,this.root&&this.root.prepend(s)}}detectLanguage(){const e=(navigator.language||navigator.userLanguage||"en").split("-")[0];return this.translations[e]?e:"en"}t(e,n={}){let i=(this.translations[this.locale]||this.translations.en)[e]||e;return Object.keys(n).forEach(t=>{i=i.replace(`{${t}}`,n[t])}),i}init(){this.root&&(this.renderToolbar(),this.wrapper=document.createElement("div"),this.wrapper.className="timeline-wrapper",this.root.appendChild(this.wrapper),this.axisContainer=document.createElement("div"),this.axisContainer.className="timeline-axis",this.wrapper.appendChild(this.axisContainer),this.renderAxis(),this.tracksContainer=document.createElement("div"),this.tracksContainer.className="timeline-tracks",this.wrapper.appendChild(this.tracksContainer),this.createRowElements(),this.updateVisibility(),this.renderLegend(),document.querySelector(".custom-tooltip")?this.tooltip=document.querySelector(".custom-tooltip"):(this.tooltip=document.createElement("div"),this.tooltip.className="custom-tooltip",document.body.appendChild(this.tooltip)),this.theme=this.safeStorageGet("timeline_theme")||"light",document.documentElement.setAttribute("data-theme",this.theme),this.startAutoUpdate(),this.renderThemeToggle())}renderToolbar(){const e=document.createElement("div");e.className="timeline-toolbar";const n=document.createElement("div");n.className="timeline-filter-container";const i=document.createElement("div");i.className="search-icon",i.innerHTML='<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>';const t=document.createElement("div");t.className="timeline-filter-input",t.contentEditable="true",t.setAttribute("spellcheck","false"),t.style.cursor="text",t.style.minWidth="150px",t.style.display="inline-block",this.filterText||(t.textContent=this.t("filterPlaceholder"),t.style.color="var(--text-secondary)"),t.onfocus=()=>{t.textContent===this.t("filterPlaceholder")&&(t.textContent="",t.style.color="var(--text-primary)")},t.onblur=()=>{t.textContent.trim()===""&&(t.textContent=this.t("filterPlaceholder"),t.style.color="var(--text-secondary)")},t.onkeydown=r=>{if(r.key==="Enter")return r.preventDefault(),!1};let s;t.oninput=r=>{r.stopPropagation(),t.style.color="var(--text-primary)",clearTimeout(s),s=setTimeout(()=>{const o=t.textContent===this.t("filterPlaceholder")?"":t.textContent;this.filterText=o.toLowerCase().trim(),this.updateVisibility()},300)},n.appendChild(i),n.appendChild(t),e.appendChild(n),this.root.appendChild(e)}renderThemeToggle(){if(document.querySelector(".theme-toggle-btn"))return;const e=document.createElement("button");e.className="theme-toggle-btn",e.type="button",e.title=this.t("toggleDarkMode");const n='<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>',i='<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>';e.innerHTML=this.theme==="dark"?i:n,e.onclick=()=>{this.theme=this.theme==="light"?"dark":"light",document.documentElement.setAttribute("data-theme",this.theme),this.safeStorageSet("timeline_theme",this.theme),e.innerHTML=this.theme==="dark"?i:n},this.root.appendChild(e)}startAutoUpdate(){this.autoUpdateInterval&&clearInterval(this.autoUpdateInterval),this.renderCurrentDateLine()}renderLegend(){const e=`
|
|
2
|
+
<div class="release-legend">
|
|
3
|
+
<div class="legend-block oss">
|
|
4
|
+
<div class="legend-icon"></div>
|
|
5
|
+
<div>
|
|
6
|
+
<h3>${this.t("legendOssTitle")}</h3>
|
|
7
|
+
<p>${this.t("legendOssDesc")}</p>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="legend-block commercial">
|
|
11
|
+
<div class="legend-icon"></div>
|
|
12
|
+
<div>
|
|
13
|
+
<h3>${this.t("legendEntTitle")}</h3>
|
|
14
|
+
<p>${this.t("legendEntDesc")}</p>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="legend-block expired">
|
|
18
|
+
<div class="legend-icon"></div>
|
|
19
|
+
<div>
|
|
20
|
+
<h3>${this.t("legendEolTitle")}</h3>
|
|
21
|
+
<p>${this.t("legendEolDesc")}</p>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
`,n=document.createElement("div");n.innerHTML=e,this.wrapper.appendChild(n.firstElementChild)}renderAxis(){this.axisContainer.innerHTML="";const e=document.createElement("div");e.style.width="96px",e.style.flexShrink="0",this.axisContainer.appendChild(e);const n=this.maxYear-this.minYear+1;for(let i=0;i<n;i++){const t=this.minYear+i,s=document.createElement("div");s.className="timeline-year",s.textContent=t,this.axisContainer.appendChild(s)}}renderGrid(){if(!this.gridContainer)return;const e=this.getTimelineDuration(),n=this.maxYear-this.minYear+1;for(let i=0;i<n;i++){const t=this.minYear+i,r=new Date(t,0,1).getTime()-this.getTimelineStart();if(r>=0&&r<=e){const o=r/e*100,l=document.createElement("div");l.className="year-grid-line",l.style.left=`${o}%`,this.gridContainer.appendChild(l)}}}createRowElements(){this.tracksContainer.innerHTML="",this.gridContainer=document.createElement("div"),this.gridContainer.className="grid-lines-container",this.tracksContainer.appendChild(this.gridContainer),this.renderGrid(),this.renderCurrentDateLine(),this.rowElements=this.data.map(e=>{const n=document.createElement("div");n.className="timeline-row";const i=document.createElement("div");if(i.className="version-label",e.releaseNotesUrl){const a=document.createElement("a");a.href=e.releaseNotesUrl,a.className="version-link",a.target="_blank",a.textContent=e.version,a.title=this.t("viewReleaseNotes",{v:e.version}),i.appendChild(a)}else i.textContent=e.version;const t=new Date().getTime(),s=new Date(e.ossStart).getTime(),r=new Date(e.ossEnd).getTime(),o=new Date(e.enterpriseEnd).getTime();t>=s&&t<=r?i.classList.add("status-oss"):t>r&&t<=o?i.classList.add("status-ent"):t>o&&i.classList.add("status-expired"),n.appendChild(i);const l=document.createElement("div");l.className="track-container";const p=this.createBarSegment(e.ossStart,e.ossEnd,"bar-oss",this.t("ossSupport")),u=this.createBarSegment(e.ossStart,e.enterpriseEnd,"bar-ent",this.t("entSupport"));return l.appendChild(u),l.appendChild(p),n.appendChild(l),this.tracksContainer.appendChild(n),{element:n,data:e}}),this.toggleBtnContainer=document.createElement("div"),this.toggleBtnContainer.style.display="flex",this.toggleBtnContainer.style.justifyContent="center",this.toggleBtnContainer.style.paddingTop="10px",this.toggleBtnContainer.style.zIndex="10",this.toggleBtnContainer.style.position="relative",this.tracksContainer.appendChild(this.toggleBtnContainer)}updateVisibility(){const e=(this.filterText||"").toLowerCase().trim(),n=e.length>0,i=this.rowElements.filter(r=>!n||r.data.version.toLowerCase().includes(e));this.rowElements.forEach(r=>r.element.style.display="none");let t=i;const s=!n&&i.length>this.visibleCount;s&&!this.isExpanded&&(t=i.slice(0,this.visibleCount)),t.forEach(r=>{r.element.style.display="flex"}),this.renderToggleButton(s,i.length-this.visibleCount)}renderRows(){this.updateVisibility()}renderToggleButton(e,n){if(this.toggleBtnContainer.innerHTML="",!e){this.toggleBtnContainer.style.display="none";return}this.toggleBtnContainer.style.display="flex";const i=document.createElement("button");i.type="button",i.className="timeline-toggle-btn";const t='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>',s='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"></polyline></svg>';this.isExpanded?i.innerHTML=`${this.t("showLess")} ${s}`:i.innerHTML=`${this.t("showMore",{n})} ${t}`,i.onclick=()=>{this.isExpanded=!this.isExpanded,this.safeStorageSet("timeline_isExpanded",this.isExpanded),this.updateVisibility()},this.toggleBtnContainer.appendChild(i)}createBarSegment(e,n,i,t){const s=new Date(e),r=new Date(n),o=this.getTimelineDuration(),l=s-this.getTimelineStart(),p=r-s,u=l/o*100,a=p/o*100,d=document.createElement("div");d.className=`bar-segment ${i}`,d.style.left=`${u}%`,d.style.width=`${a}%`;const g=`${t}: ${e} to ${n}`;return d.addEventListener("mouseenter",m=>{this.tooltip&&(this.tooltip.textContent=g,this.tooltip.style.opacity="1",this.updateTooltipPosition(m))}),d.addEventListener("mousemove",m=>{this.updateTooltipPosition(m)}),d.addEventListener("mouseleave",()=>{this.tooltip&&(this.tooltip.style.opacity="0")}),d}updateTooltipPosition(e){if(!this.tooltip)return;const n=e.clientX+12,i=e.clientY+12;this.tooltip.style.left=`${n}px`,this.tooltip.style.top=`${i}px`}renderCurrentDateLine(){if(!this.gridContainer)return;const e=new Date,n=this.getTimelineDuration(),i=e-this.getTimelineStart();if(i<0||i>n)return;const t=i/n*100;if(this.currentDateLine&&this.gridContainer.contains(this.currentDateLine)){this.currentDateLine.style.left=`${t}%`;const o=this.currentDateLine.querySelector(".current-date-badge");o&&(o.textContent=e.toISOString().split("T")[0]);return}const s=document.createElement("div");s.className="current-date-indicator",s.style.left=`${t}%`;const r=document.createElement("div");r.className="current-date-badge",r.textContent=e.toISOString().split("T")[0],s.appendChild(r),this.currentDateLine=s,this.gridContainer.appendChild(s)}getTimelineStart(){return new Date(this.minYear,0,1).getTime()}getTimelineEnd(){return new Date(this.maxYear,11,31).getTime()}getTimelineDuration(){return this.getTimelineEnd()-this.getTimelineStart()}safeStorageGet(e){try{return localStorage.getItem(e)}catch{return console.warn("LocalStorage access denied via safeStorageGet"),null}}safeStorageSet(e,n){try{localStorage.setItem(e,n)}catch{console.warn("LocalStorage access denied via safeStorageSet")}}}return c}));
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lifecycle-timeline",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A beautiful, premium JS timeline component for visualizing product lifecycles, OSS support, and enterprise support dates.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/timeline.umd.cjs",
|
|
7
|
+
"module": "./dist/timeline.js",
|
|
8
|
+
"types": "./src/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/index.d.ts",
|
|
12
|
+
"import": "./dist/timeline.js",
|
|
13
|
+
"require": "./dist/timeline.umd.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./style.css": "./dist/timeline.css"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src/index.d.ts"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"dev": "vite",
|
|
23
|
+
"build": "vite build",
|
|
24
|
+
"preview": "vite preview",
|
|
25
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"timeline",
|
|
29
|
+
"lifecycle",
|
|
30
|
+
"roadmap",
|
|
31
|
+
"release",
|
|
32
|
+
"support",
|
|
33
|
+
"oss",
|
|
34
|
+
"enterprise",
|
|
35
|
+
"vanilla-js",
|
|
36
|
+
"premium-ui"
|
|
37
|
+
],
|
|
38
|
+
"author": "Eric REBOISSON",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/ericreboisson/lifecycle-timeline.git"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/ericreboisson/lifecycle-timeline/issues"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/ericreboisson/lifecycle-timeline#readme",
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"vite": "^7.3.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface TimelineVersion {
|
|
2
|
+
version: string;
|
|
3
|
+
ossStart: string;
|
|
4
|
+
ossEnd: string;
|
|
5
|
+
enterpriseEnd: string;
|
|
6
|
+
releaseNotesUrl?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TimelineOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Number of versions to show before "Show more" button appears.
|
|
12
|
+
* @default 3
|
|
13
|
+
*/
|
|
14
|
+
visibleCount?: number;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Preferred locale for translations ('en' or 'fr').
|
|
18
|
+
* Defaults to browser language.
|
|
19
|
+
*/
|
|
20
|
+
locale?: 'en' | 'fr' | string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default class Timeline {
|
|
24
|
+
/**
|
|
25
|
+
* Creates a new Lifecycle Timeline.
|
|
26
|
+
* @param elementId The ID of the container element.
|
|
27
|
+
* @param data Array of version data.
|
|
28
|
+
* @param options Configuration options.
|
|
29
|
+
*/
|
|
30
|
+
constructor(elementId: string, data: TimelineVersion[], options?: TimelineOptions);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Initializes the timeline. Called automatically by constructor.
|
|
34
|
+
*/
|
|
35
|
+
init(): void;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Renders the toolbar (search/filter).
|
|
39
|
+
*/
|
|
40
|
+
renderToolbar(): void;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Renders the theme toggle button.
|
|
44
|
+
*/
|
|
45
|
+
renderThemeToggle(): void;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Updates the visibility of rows based on filtering and expansion state.
|
|
49
|
+
*/
|
|
50
|
+
updateVisibility(): void;
|
|
51
|
+
}
|