lifecycle-timeline 1.2.2 → 1.2.3
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/README.md +2 -42
- package/dist/timeline.js +125 -119
- package/dist/timeline.umd.cjs +7 -13
- package/package.json +6 -4
- package/dist/timeline.css +0 -1
package/README.md
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A premium, interactive Vanilla JS component for visualizing product lifecycles, including OSS support, Enterprise support, and EOL (End-Of-Life) dates.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
[**🚀 View Live Demo**](https://ericreboisson.github.io/lifecycle-timeline/)
|
|
6
7
|
|
|
7
8
|

|
|
8
9
|
|
|
@@ -61,47 +62,6 @@ new Timeline('timeline-root', data, { visibleCount: 3 });
|
|
|
61
62
|
</script>
|
|
62
63
|
```
|
|
63
64
|
|
|
64
|
-
### Angular Integration
|
|
65
|
-
|
|
66
|
-
To use this component in an Angular application, you can initialize it in the `ngAfterViewInit` lifecycle hook to ensure the DOM is ready.
|
|
67
|
-
|
|
68
|
-
**Component Template (`app.component.html`):**
|
|
69
|
-
```html
|
|
70
|
-
<div id="timeline-container"></div>
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
**Component Logic (`app.component.ts`):**
|
|
74
|
-
```typescript
|
|
75
|
-
import { Component, AfterViewInit } from '@angular/core';
|
|
76
|
-
import Timeline from 'lifecycle-timeline';
|
|
77
|
-
|
|
78
|
-
@Component({
|
|
79
|
-
selector: 'app-root',
|
|
80
|
-
templateUrl: './app.component.html',
|
|
81
|
-
styleUrls: ['./app.component.css']
|
|
82
|
-
})
|
|
83
|
-
export class AppComponent implements AfterViewInit {
|
|
84
|
-
ngAfterViewInit() {
|
|
85
|
-
const data = [
|
|
86
|
-
{
|
|
87
|
-
version: "6.0.x",
|
|
88
|
-
ossStart: "2025-01-01",
|
|
89
|
-
ossEnd: "2026-08-20",
|
|
90
|
-
enterpriseEnd: "2027-02-15"
|
|
91
|
-
}
|
|
92
|
-
];
|
|
93
|
-
|
|
94
|
-
// Initialize the timeline
|
|
95
|
-
new Timeline('timeline-container', data, { locale: 'fr' });
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
> [!TIP]
|
|
101
|
-
> Don't forget to include the CSS! You can:
|
|
102
|
-
> - Import it in your `styles.css`: `@import 'lifecycle-timeline/style.css';`
|
|
103
|
-
> - Or in your `angular.json`: add `"node_modules/lifecycle-timeline/dist/timeline.css"` to the `styles` array.
|
|
104
|
-
|
|
105
65
|
## ⚙️ Configuration & API
|
|
106
66
|
|
|
107
67
|
### Constructor
|
package/dist/timeline.js
CHANGED
|
@@ -36,7 +36,7 @@ const u = {
|
|
|
36
36
|
entEnd: "Fin Entreprise *"
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
|
-
class
|
|
39
|
+
class v {
|
|
40
40
|
/**
|
|
41
41
|
* Creates an instance of Timeline.
|
|
42
42
|
* @param {string} elementId - The ID of the root element.
|
|
@@ -46,16 +46,16 @@ class b {
|
|
|
46
46
|
* @param {string} [options.locale] - Language code (e.g., 'en', 'fr').
|
|
47
47
|
* @param {Object} [options.i18n] - Custom translations to merge or override.
|
|
48
48
|
*/
|
|
49
|
-
constructor(
|
|
50
|
-
this.root = document.getElementById(
|
|
49
|
+
constructor(t, e, s = {}) {
|
|
50
|
+
this.root = document.getElementById(t), this.root && (this.options = s, this.visibleCount = s.visibleCount || 3, this.showTable = s.showTable !== !1, this.isExpanded = !1, this.theme = "light", this.filterText = "", this.activeHighlight = null, this.translations = { ...u, ...s.i18n || {} }, this.locale = s.locale || this.detectLanguage(), this.setupBaseLayout(), this.updateData(e));
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
53
|
* Detects the browser language.
|
|
54
54
|
* @returns {string} The detected language code or 'en'.
|
|
55
55
|
*/
|
|
56
56
|
detectLanguage() {
|
|
57
|
-
const
|
|
58
|
-
return this.translations[
|
|
57
|
+
const t = (navigator.language || "en").split("-")[0];
|
|
58
|
+
return this.translations[t] ? t : "en";
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
61
|
* Translates a key based on the current locale.
|
|
@@ -63,9 +63,9 @@ class b {
|
|
|
63
63
|
* @param {Object} [params={}] - Parameters to replace in the translation string.
|
|
64
64
|
* @returns {string} The translated string.
|
|
65
65
|
*/
|
|
66
|
-
t(
|
|
67
|
-
let s = (this.translations[this.locale] || this.translations.en)[
|
|
68
|
-
return Object.keys(
|
|
66
|
+
t(t, e = {}) {
|
|
67
|
+
let s = (this.translations[this.locale] || this.translations.en)[t] || t;
|
|
68
|
+
return Object.keys(e).forEach((i) => s = s.replace(`{${i}}`, e[i])), s;
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
71
|
* Sets up the initial layout of the timeline.
|
|
@@ -77,21 +77,21 @@ class b {
|
|
|
77
77
|
* Updates the timeline data and re-renders.
|
|
78
78
|
* @param {Array<Object>} newData - The new data array.
|
|
79
79
|
*/
|
|
80
|
-
updateData(
|
|
81
|
-
this.data = this.validateData(
|
|
80
|
+
updateData(t) {
|
|
81
|
+
this.data = this.validateData(t || []), this.calculateTimeRange(), this.render();
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
84
|
* Validates the input data.
|
|
85
85
|
* @param {Array<Object>} data - The data array to validate.
|
|
86
86
|
* @returns {Array<Object>} The validated and filtered data array.
|
|
87
87
|
*/
|
|
88
|
-
validateData(
|
|
89
|
-
return
|
|
90
|
-
const n = ["version", "ossStart", "ossEnd"].filter((l) => !
|
|
88
|
+
validateData(t) {
|
|
89
|
+
return t.filter((e, s) => {
|
|
90
|
+
const n = ["version", "ossStart", "ossEnd"].filter((l) => !e[l]);
|
|
91
91
|
if (n.length > 0)
|
|
92
92
|
return console.warn(`[Timeline] Missing fields for item at index ${s}: ${n.join(", ")}`), !1;
|
|
93
|
-
const a = ["ossStart", "ossEnd", "enterpriseEnd"].filter((l) =>
|
|
94
|
-
return a.length > 0 ? (console.warn(`[Timeline] Invalid date format for item "${
|
|
93
|
+
const a = ["ossStart", "ossEnd", "enterpriseEnd"].filter((l) => e[l]).filter((l) => isNaN(new Date(e[l]).getTime()));
|
|
94
|
+
return a.length > 0 ? (console.warn(`[Timeline] Invalid date format for item "${e.version}": ${a.join(", ")}`), !1) : !0;
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
/**
|
|
@@ -102,53 +102,53 @@ class b {
|
|
|
102
102
|
this.minYear = this.currentDate.getFullYear() - 1, this.maxYear = this.currentDate.getFullYear() + 3;
|
|
103
103
|
return;
|
|
104
104
|
}
|
|
105
|
-
const
|
|
106
|
-
new Date(
|
|
107
|
-
new Date(
|
|
105
|
+
const t = this.data.flatMap((e) => [
|
|
106
|
+
new Date(e.ossStart),
|
|
107
|
+
new Date(e.enterpriseEnd || e.ossEnd)
|
|
108
108
|
]);
|
|
109
|
-
this.minYear = new Date(Math.min(...
|
|
109
|
+
this.minYear = new Date(Math.min(...t)).getFullYear(), this.maxYear = new Date(Math.max(...t)).getFullYear();
|
|
110
110
|
}
|
|
111
111
|
/**
|
|
112
112
|
* Renders the data table.
|
|
113
113
|
*/
|
|
114
114
|
renderTable() {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
[this.t("branch"), this.t("initial"), this.t("ossEnd"), this.t("entEnd")].forEach((
|
|
119
|
-
this.el("th", "", s).textContent =
|
|
120
|
-
}), this.tableBody = this.el("tbody", "",
|
|
115
|
+
const t = this.el("table", "timeline-table", this.tableContainer);
|
|
116
|
+
t.setAttribute("aria-label", "Project support");
|
|
117
|
+
const e = this.el("thead", "", t), s = this.el("tr", "", e);
|
|
118
|
+
[this.t("branch"), this.t("initial"), this.t("ossEnd"), this.t("entEnd")].forEach((i) => {
|
|
119
|
+
this.el("th", "", s).textContent = i;
|
|
120
|
+
}), this.tableBody = this.el("tbody", "", t), this.tableRows = this.data.map((i) => this.createTableRow(i)), this.tableToggleContainer = this.el("div", "timeline-table-toggle", this.tableContainer), this.updateTableVisibility();
|
|
121
121
|
}
|
|
122
122
|
/**
|
|
123
123
|
* Creates a row for the data table.
|
|
124
124
|
* @param {Object} item - Version data.
|
|
125
125
|
*/
|
|
126
|
-
createTableRow(
|
|
127
|
-
const
|
|
128
|
-
let
|
|
129
|
-
|
|
130
|
-
const r = this.el("td", "",
|
|
131
|
-
r.textContent =
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
return
|
|
126
|
+
createTableRow(t) {
|
|
127
|
+
const e = this.el("tr", "", this.tableBody), s = Date.now(), i = new Date(t.ossStart).getTime(), n = new Date(t.ossEnd).getTime(), o = t.enterpriseEnd ? new Date(t.enterpriseEnd).getTime() : n, a = s >= i && s <= n ? "status-oss" : s > n && s <= o ? "status-ent" : s > o ? "status-expired" : "", l = this.el("td", "", e);
|
|
128
|
+
let d = `<span class="table-badge ${a}">${t.versionOriginal || t.version}</span>`;
|
|
129
|
+
t.releaseNotesUrl ? l.innerHTML = `<a href="${t.releaseNotesUrl}" target="_blank" class="table-version-link">${d}</a>` : l.innerHTML = d;
|
|
130
|
+
const r = this.el("td", "", e);
|
|
131
|
+
r.textContent = t.ossStart, s > i && (r.className = "past-date");
|
|
132
|
+
const h = this.el("td", "", e);
|
|
133
|
+
h.textContent = t.ossEnd, s > n && (h.className = "past-date");
|
|
134
|
+
const c = this.el("td", "", e);
|
|
135
|
+
return c.textContent = t.enterpriseEnd || t.ossEnd, s > o && (c.className = "past-date"), { el: e, version: t.version.toLowerCase(), versionOriginal: t.versionOriginal || t.version };
|
|
136
136
|
}
|
|
137
137
|
/**
|
|
138
138
|
* Updates table visibility based on filter and visibleCount.
|
|
139
139
|
*/
|
|
140
140
|
updateTableVisibility() {
|
|
141
|
-
const
|
|
142
|
-
this.tableRows.forEach((
|
|
143
|
-
const n =
|
|
144
|
-
n.innerHTML = this.highlight(
|
|
141
|
+
const t = this.tableRows.filter((i) => i.version.includes(this.filterText));
|
|
142
|
+
this.tableRows.forEach((i) => {
|
|
143
|
+
const n = i.el.querySelector(".table-badge");
|
|
144
|
+
n.innerHTML = this.highlight(i.versionOriginal || i.version), i.el.classList.remove("row-visible"), i.el.classList.add("row-hidden");
|
|
145
145
|
});
|
|
146
|
-
const
|
|
147
|
-
if ((
|
|
148
|
-
|
|
149
|
-
}), this.tableToggleContainer.innerHTML = "",
|
|
150
|
-
const
|
|
151
|
-
|
|
146
|
+
const e = this.filterText === "" && t.length > this.visibleCount;
|
|
147
|
+
if ((e && !this.isExpanded ? t.slice(0, this.visibleCount) : t).forEach((i) => {
|
|
148
|
+
i.el.classList.remove("row-hidden"), i.el.classList.add("row-visible");
|
|
149
|
+
}), this.tableToggleContainer.innerHTML = "", e) {
|
|
150
|
+
const i = this.el("button", "timeline-toggle-btn table-toggle", this.tableToggleContainer), n = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="${this.isExpanded ? "18 15 12 9 6 15" : "6 9 12 15 18 9"}"></polyline></svg>`;
|
|
151
|
+
i.innerHTML = this.isExpanded ? `${this.t("less")} ${n}` : `${this.t("more", { n: t.length - this.visibleCount })} ${n}`, i.onclick = () => {
|
|
152
152
|
this.isExpanded = !this.isExpanded, this.updateVisibility();
|
|
153
153
|
};
|
|
154
154
|
}
|
|
@@ -157,7 +157,7 @@ class b {
|
|
|
157
157
|
* Renders the entire timeline.
|
|
158
158
|
*/
|
|
159
159
|
render() {
|
|
160
|
-
this.showTable && (this.tableContainer.innerHTML = ""), this.axis.innerHTML = "", this.tracks.innerHTML = "", this.legendContainer.innerHTML = "", this.showTable && this.renderTable(), this.renderAxis(), this.grid = this.el("div", "grid-lines-container", this.tracks), this.indicators = this.el("div", "indicators-container", this.tracks), this.renderGrid(), this.renderCurrentDateLine(), this.rows = this.data.map((e) => this.createRow(e)), this.toggleContainer = this.el("div", "timeline-more-toggle", this.tracks, {
|
|
160
|
+
this.showTable && (this.tableContainer.innerHTML = ""), this.axis.innerHTML = "", this.tracks.innerHTML = "", this.legendContainer.innerHTML = "", this.showTable && this.renderTable(), this.renderAxis(), this.grid = this.el("div", "grid-lines-container", this.tracks), this.indicators = this.el("div", "indicators-container", this.tracks), this.renderGrid(), this.renderCurrentDateLine(), this.rows = this.data.map((t, e) => this.createRow(t, e)), this.toggleContainer = this.el("div", "timeline-more-toggle", this.tracks, {
|
|
161
161
|
paddingTop: "10px",
|
|
162
162
|
display: "flex",
|
|
163
163
|
justifyContent: "center",
|
|
@@ -176,17 +176,17 @@ class b {
|
|
|
176
176
|
* @param {MouseEvent} e - The mouse event.
|
|
177
177
|
* @param {string} content - The HTML content for the tooltip.
|
|
178
178
|
*/
|
|
179
|
-
showTooltip(
|
|
180
|
-
this.tooltip.innerHTML =
|
|
179
|
+
showTooltip(t, e) {
|
|
180
|
+
this.tooltip.innerHTML = e, this.tooltip.style.display = "block", this.updateTooltipPos(t);
|
|
181
181
|
}
|
|
182
182
|
/**
|
|
183
183
|
* Updates tooltip position relative to mouse.
|
|
184
184
|
* @param {MouseEvent} e - The mouse event.
|
|
185
185
|
*/
|
|
186
|
-
updateTooltipPos(
|
|
187
|
-
let s =
|
|
186
|
+
updateTooltipPos(t) {
|
|
187
|
+
let s = t.pageX + 12, i = t.pageY + 12;
|
|
188
188
|
const n = this.tooltip.offsetWidth, o = this.tooltip.offsetHeight;
|
|
189
|
-
s + n > window.innerWidth && (s =
|
|
189
|
+
s + n > window.innerWidth && (s = t.pageX - n - 12), i + o > window.innerHeight && (i = t.pageY - o - 12), this.tooltip.style.left = `${s}px`, this.tooltip.style.top = `${i}px`;
|
|
190
190
|
}
|
|
191
191
|
/**
|
|
192
192
|
* Hides the tooltip.
|
|
@@ -199,13 +199,13 @@ class b {
|
|
|
199
199
|
* @param {string} text - The text to process.
|
|
200
200
|
* @returns {string} The HTML with highlighted segments.
|
|
201
201
|
*/
|
|
202
|
-
highlight(
|
|
203
|
-
if (!this.filterText) return
|
|
202
|
+
highlight(t) {
|
|
203
|
+
if (!this.filterText) return t;
|
|
204
204
|
try {
|
|
205
|
-
const
|
|
206
|
-
return
|
|
205
|
+
const e = new RegExp(`(${this.filterText})`, "gi");
|
|
206
|
+
return t.replace(e, '<mark class="highlight-match">$1</mark>');
|
|
207
207
|
} catch {
|
|
208
|
-
return
|
|
208
|
+
return t;
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
/**
|
|
@@ -216,68 +216,74 @@ class b {
|
|
|
216
216
|
* @param {Object} [styles={}] - Inline styles.
|
|
217
217
|
* @returns {HTMLElement} The created element.
|
|
218
218
|
*/
|
|
219
|
-
el(
|
|
220
|
-
const n = document.createElement(
|
|
221
|
-
return
|
|
219
|
+
el(t, e, s, i = {}) {
|
|
220
|
+
const n = document.createElement(t);
|
|
221
|
+
return e && (n.className = e), Object.assign(n.style, i), s && s.appendChild(n), n;
|
|
222
222
|
}
|
|
223
223
|
/**
|
|
224
224
|
* Renders the toolbar with the filter input.
|
|
225
225
|
*/
|
|
226
226
|
renderToolbar() {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
const s = this.el("input", "timeline-filter-input",
|
|
230
|
-
s.placeholder = this.t("filter"), s.value = this.filterText, s.setAttribute("aria-label", this.t("filter")), s.oninput = (
|
|
231
|
-
this.filterText =
|
|
227
|
+
const t = this.el("div", "timeline-toolbar", this.root), e = this.el("div", "timeline-filter-container", t);
|
|
228
|
+
e.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>';
|
|
229
|
+
const s = this.el("input", "timeline-filter-input", e);
|
|
230
|
+
s.placeholder = this.t("filter"), s.value = this.filterText, s.setAttribute("aria-label", this.t("filter")), s.oninput = (i) => {
|
|
231
|
+
this.filterText = i.target.value.toLowerCase().trim(), this.updateVisibility();
|
|
232
232
|
};
|
|
233
233
|
}
|
|
234
234
|
/**
|
|
235
235
|
* Renders the theme toggle button.
|
|
236
236
|
*/
|
|
237
237
|
renderThemeToggle() {
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
const
|
|
238
|
+
const t = this.el("button", "theme-toggle-btn", this.root);
|
|
239
|
+
t.title = this.t("dark"), t.setAttribute("aria-label", this.t("dark"));
|
|
240
|
+
const e = {
|
|
241
241
|
moon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>',
|
|
242
242
|
sun: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><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>'
|
|
243
243
|
};
|
|
244
|
-
|
|
245
|
-
this.theme = this.theme === "light" ? "dark" : "light", document.documentElement.setAttribute("data-theme", this.theme),
|
|
244
|
+
t.innerHTML = this.theme === "dark" ? e.sun : e.moon, t.onclick = () => {
|
|
245
|
+
this.theme = this.theme === "light" ? "dark" : "light", document.documentElement.setAttribute("data-theme", this.theme), t.innerHTML = this.theme === "dark" ? e.sun : e.moon, t.setAttribute("aria-pressed", this.theme === "dark");
|
|
246
246
|
};
|
|
247
247
|
}
|
|
248
248
|
/**
|
|
249
249
|
* Renders the legend section.
|
|
250
250
|
*/
|
|
251
251
|
renderLegend() {
|
|
252
|
-
this.legendContainer.innerHTML =
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
252
|
+
this.legendContainer.innerHTML = "";
|
|
253
|
+
const t = this.el("div", "release-legend", this.legendContainer);
|
|
254
|
+
t.setAttribute("role", "complementary"), t.setAttribute("aria-label", "Support Legend"), ["oss", "ent", "eol"].forEach((e) => {
|
|
255
|
+
const s = this.el("div", `legend-block ${e === "ent" ? "commercial" : e}`, t);
|
|
256
|
+
s.classList.add("legend-item-reactive"), this.activeHighlight === e && s.classList.add("active-highlight"), s.innerHTML = `
|
|
257
|
+
<div class="legend-icon" aria-hidden="true"></div>
|
|
258
|
+
<div><h3>${this.t(e)}</h3><p>${this.t(e + "Desc")}</p></div>
|
|
259
|
+
`, s.onclick = () => this.highlightSegment(e);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Toggles highlighting for a specific segment type.
|
|
264
|
+
* @param {string} type - 'oss', 'ent', or 'eol'.
|
|
265
|
+
*/
|
|
266
|
+
highlightSegment(t) {
|
|
267
|
+
this.activeHighlight = this.activeHighlight === t ? null : t, this.root.classList.remove("highlight-oss", "highlight-ent", "highlight-eol"), this.activeHighlight && this.root.classList.add(`highlight-${this.activeHighlight}`), this.renderLegend();
|
|
262
268
|
}
|
|
263
269
|
/**
|
|
264
270
|
* Renders the year axis.
|
|
265
271
|
*/
|
|
266
272
|
renderAxis() {
|
|
267
273
|
this.el("div", "", this.axis, { width: "96px", flexShrink: "0" }).setAttribute("role", "presentation");
|
|
268
|
-
for (let
|
|
274
|
+
for (let e = this.minYear; e <= this.maxYear; e++) {
|
|
269
275
|
const s = this.el("div", "timeline-year", this.axis);
|
|
270
|
-
s.textContent =
|
|
276
|
+
s.textContent = e, s.setAttribute("role", "columnheader");
|
|
271
277
|
}
|
|
272
278
|
}
|
|
273
279
|
/**
|
|
274
280
|
* Renders the vertical grid lines for years.
|
|
275
281
|
*/
|
|
276
282
|
renderGrid() {
|
|
277
|
-
const
|
|
283
|
+
const t = new Date(this.minYear, 0, 1).getTime(), e = new Date(this.maxYear, 11, 31).getTime() - t;
|
|
278
284
|
for (let s = this.minYear; s <= this.maxYear; s++) {
|
|
279
|
-
const
|
|
280
|
-
|
|
285
|
+
const i = this.el("div", "year-grid-line", this.grid);
|
|
286
|
+
i.style.left = `${(new Date(s, 0, 1).getTime() - t) / e * 100}%`, i.setAttribute("role", "presentation");
|
|
281
287
|
}
|
|
282
288
|
}
|
|
283
289
|
/**
|
|
@@ -285,19 +291,19 @@ class b {
|
|
|
285
291
|
* @param {Object} item - The version data item.
|
|
286
292
|
* @returns {Object} Metadata about the created row.
|
|
287
293
|
*/
|
|
288
|
-
createRow(e) {
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
if (
|
|
295
|
-
const
|
|
296
|
-
|
|
294
|
+
createRow(t, e) {
|
|
295
|
+
const s = this.el("div", "timeline-row row-entrance", this.tracks);
|
|
296
|
+
s.setAttribute("role", "row"), s.style.transitionDelay = `${e * 0.05}s`;
|
|
297
|
+
const i = this.el("div", "version-label", s);
|
|
298
|
+
i.setAttribute("role", "rowheader");
|
|
299
|
+
const n = Date.now(), o = new Date(t.ossStart).getTime(), a = new Date(t.ossEnd).getTime(), l = t.enterpriseEnd ? new Date(t.enterpriseEnd).getTime() : a, d = n >= o && n <= a ? "status-oss" : n > a && n <= l ? "status-ent" : n > l ? "status-expired" : "";
|
|
300
|
+
if (d && i.classList.add(d), t.releaseNotesUrl) {
|
|
301
|
+
const h = this.el("a", "version-link", i);
|
|
302
|
+
h.href = t.releaseNotesUrl, h.target = "_blank", h.innerHTML = this.highlight(t.version), h.title = this.t("notes", { v: t.version }), h.setAttribute("aria-label", this.t("notes", { v: t.version }));
|
|
297
303
|
} else
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
return
|
|
304
|
+
i.innerHTML = this.highlight(t.version);
|
|
305
|
+
const r = this.el("div", "track-container", s);
|
|
306
|
+
return r.setAttribute("role", "gridcell"), r.appendChild(this.createBar(t, t.ossStart, t.enterpriseEnd || t.ossEnd, "bar-ent", this.t("ent"))), r.appendChild(this.createBar(t, t.ossStart, t.ossEnd, "bar-oss", this.t("oss"))), { el: s, version: t.version.toLowerCase(), versionOriginal: t.version };
|
|
301
307
|
}
|
|
302
308
|
/**
|
|
303
309
|
* Creates a colored bar segment for the timeline.
|
|
@@ -305,18 +311,18 @@ class b {
|
|
|
305
311
|
* @param {string} startStr - Start date string.
|
|
306
312
|
* @param {string} endStr - End date string.
|
|
307
313
|
* @param {string} className - CSS class.
|
|
308
|
-
* @param {string}
|
|
314
|
+
* @param {string} label - Support type label for tooltip.
|
|
309
315
|
* @returns {HTMLElement} The created bar element.
|
|
310
316
|
*/
|
|
311
|
-
createBar(
|
|
312
|
-
const o = new Date(
|
|
313
|
-
r.style.left = `${(o - l) /
|
|
317
|
+
createBar(t, e, s, i, n) {
|
|
318
|
+
const o = new Date(e).getTime(), a = new Date(s).getTime(), l = new Date(this.minYear, 0, 1).getTime(), d = new Date(this.maxYear, 11, 31).getTime() - l, r = this.el("div", `bar-segment ${i}`), h = i === "bar-oss" ? "segment-oss" : "segment-ent";
|
|
319
|
+
r.classList.add(h), r.style.left = `${(o - l) / d * 100}%`, r.style.width = `${(a - o) / d * 100}%`, r.setAttribute("role", "img"), r.setAttribute("aria-label", `${n}: ${t.version} (${e} to ${s})`), r.tabIndex = 0;
|
|
314
320
|
const c = `
|
|
315
|
-
<div class="tooltip-header">${n} - ${
|
|
316
|
-
<div class="tooltip-date"><strong>Du:</strong> ${
|
|
321
|
+
<div class="tooltip-header">${n} - ${t.version}</div>
|
|
322
|
+
<div class="tooltip-date"><strong>Du:</strong> ${e}</div>
|
|
317
323
|
<div class="tooltip-date"><strong>Au:</strong> ${s}</div>
|
|
318
324
|
`;
|
|
319
|
-
return r.onmouseenter = (
|
|
325
|
+
return r.onmouseenter = (g) => this.showTooltip(g, c), r.onmousemove = (g) => this.updateTooltipPos(g), r.onmouseleave = () => this.hideTooltip(), r.onfocus = (g) => {
|
|
320
326
|
const p = r.getBoundingClientRect();
|
|
321
327
|
this.showTooltip({ pageX: p.left + window.scrollX, pageY: p.top + window.scrollY - 40 }, c);
|
|
322
328
|
}, r.onblur = () => this.hideTooltip(), r;
|
|
@@ -325,17 +331,17 @@ class b {
|
|
|
325
331
|
* Updates visibility of rows based on filtering and expansion state.
|
|
326
332
|
*/
|
|
327
333
|
updateVisibility() {
|
|
328
|
-
const
|
|
329
|
-
this.rows.forEach((
|
|
330
|
-
const n =
|
|
331
|
-
o ? o.innerHTML = this.highlight(
|
|
334
|
+
const t = this.rows.filter((i) => i.version.includes(this.filterText));
|
|
335
|
+
this.rows.forEach((i) => {
|
|
336
|
+
const n = i.el.querySelector(".version-label"), o = n.querySelector(".version-link");
|
|
337
|
+
o ? o.innerHTML = this.highlight(i.versionOriginal || i.version) : n.innerHTML = this.highlight(i.versionOriginal || i.version), i.el.classList.remove("row-visible"), i.el.classList.add("row-hidden");
|
|
332
338
|
});
|
|
333
|
-
const
|
|
334
|
-
if ((
|
|
335
|
-
|
|
336
|
-
}), this.showTable && this.updateTableVisibility(), this.toggleContainer.innerHTML = "",
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
+
const e = this.filterText === "" && t.length > this.visibleCount;
|
|
340
|
+
if ((e && !this.isExpanded ? t.slice(0, this.visibleCount) : t).forEach((i) => {
|
|
341
|
+
i.el.classList.remove("row-hidden"), i.el.classList.add("row-visible");
|
|
342
|
+
}), this.showTable && this.updateTableVisibility(), this.toggleContainer.innerHTML = "", e) {
|
|
343
|
+
const i = this.el("button", "timeline-toggle-btn", this.toggleContainer), n = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="${this.isExpanded ? "18 15 12 9 6 15" : "6 9 12 15 18 9"}"></polyline></svg>`;
|
|
344
|
+
i.innerHTML = this.isExpanded ? `${this.t("less")} ${n}` : `${this.t("more", { n: t.length - this.visibleCount })} ${n}`, i.setAttribute("aria-expanded", this.isExpanded), i.onclick = () => {
|
|
339
345
|
this.isExpanded = !this.isExpanded, this.updateVisibility();
|
|
340
346
|
};
|
|
341
347
|
}
|
|
@@ -344,14 +350,14 @@ class b {
|
|
|
344
350
|
* Renders the vertical line indicating current date.
|
|
345
351
|
*/
|
|
346
352
|
renderCurrentDateLine() {
|
|
347
|
-
const
|
|
348
|
-
if (s < 0 || s >
|
|
349
|
-
const
|
|
350
|
-
n.style.left = `${s /
|
|
353
|
+
const t = new Date(this.minYear, 0, 1).getTime(), e = new Date(this.maxYear, 11, 31).getTime() - t, s = Date.now() - t;
|
|
354
|
+
if (s < 0 || s > e) return;
|
|
355
|
+
const i = (/* @__PURE__ */ new Date()).toISOString().split("T")[0], n = this.el("div", "current-date-indicator", this.indicators);
|
|
356
|
+
n.style.left = `${s / e * 100}%`, n.setAttribute("role", "separator"), n.setAttribute("aria-label", this.t("today", { date: i }));
|
|
351
357
|
const o = this.el("div", "current-date-badge", n);
|
|
352
|
-
o.textContent =
|
|
358
|
+
o.textContent = i, o.setAttribute("aria-hidden", "true");
|
|
353
359
|
}
|
|
354
360
|
}
|
|
355
361
|
export {
|
|
356
|
-
|
|
362
|
+
v as default
|
|
357
363
|
};
|
package/dist/timeline.umd.cjs
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
(function(c,
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
</div>
|
|
8
|
-
`).join("")}
|
|
9
|
-
</div>
|
|
10
|
-
`}renderAxis(){this.el("div","",this.axis,{width:"96px",flexShrink:"0"}).setAttribute("role","presentation");for(let i=this.minYear;i<=this.maxYear;i++){const s=this.el("div","timeline-year",this.axis);s.textContent=i,s.setAttribute("role","columnheader")}}renderGrid(){const e=new Date(this.minYear,0,1).getTime(),i=new Date(this.maxYear,11,31).getTime()-e;for(let s=this.minYear;s<=this.maxYear;s++){const t=this.el("div","year-grid-line",this.grid);t.style.left=`${(new Date(s,0,1).getTime()-e)/i*100}%`,t.setAttribute("role","presentation")}}createRow(e){const i=this.el("div","timeline-row",this.tracks);i.setAttribute("role","row");const s=this.el("div","version-label",i);s.setAttribute("role","rowheader");const t=Date.now(),n=new Date(e.ossStart).getTime(),o=new Date(e.ossEnd).getTime(),a=e.enterpriseEnd?new Date(e.enterpriseEnd).getTime():o,l=t>=n&&t<=o?"status-oss":t>o&&t<=a?"status-ent":t>a?"status-expired":"";if(l&&s.classList.add(l),e.releaseNotesUrl){const r=this.el("a","version-link",s);r.href=e.releaseNotesUrl,r.target="_blank",r.innerHTML=this.highlight(e.version),r.title=this.t("notes",{v:e.version}),r.setAttribute("aria-label",this.t("notes",{v:e.version}))}else s.innerHTML=this.highlight(e.version);const h=this.el("div","track-container",i);return h.setAttribute("role","gridcell"),h.appendChild(this.createBar(e,e.ossStart,e.enterpriseEnd||e.ossEnd,"bar-ent",this.t("ent"))),h.appendChild(this.createBar(e,e.ossStart,e.ossEnd,"bar-oss",this.t("oss"))),{el:i,version:e.version.toLowerCase(),versionOriginal:e.version}}createBar(e,i,s,t,n){const o=new Date(i).getTime(),a=new Date(s).getTime(),l=new Date(this.minYear,0,1).getTime(),h=new Date(this.maxYear,11,31).getTime()-l,r=this.el("div",`bar-segment ${t}`);r.style.left=`${(o-l)/h*100}%`,r.style.width=`${(a-o)/h*100}%`,r.setAttribute("role","img"),r.setAttribute("aria-label",`${e.version} ${n}: ${i} to ${s}`),r.tabIndex=0;const u=`
|
|
11
|
-
<div class="tooltip-header">${n} - ${e.version}</div>
|
|
12
|
-
<div class="tooltip-date"><strong>Du:</strong> ${i}</div>
|
|
1
|
+
(function(c,g){typeof exports=="object"&&typeof module<"u"?module.exports=g():typeof define=="function"&&define.amd?define(g):(c=typeof globalThis<"u"?globalThis:c||self,c.Timeline=g())})(this,(function(){"use strict";const c={en:{filter:"Filter versions...",oss:"OSS Support",ent:"Enterprise Support",eol:"Out of Support",less:"Show Less",more:"Show {n} more versions",notes:"View Release Notes for {v}",dark:"Toggle Dark Mode",ossDesc:"Free security updates and bugfixes.",entDesc:"Expert support during OSS plus extended support after EOL.",eolDesc:"End of life. No further updates are provided.",today:"Today's date: {date}",branch:"Branch",initial:"Initial Release",ossEnd:"End OSS",entEnd:"End Enterprise *"},fr:{filter:"Filtrer les versions...",oss:"Support OSS",ent:"Support Entreprise",eol:"Fin de support",less:"Voir moins",more:"Voir {n} versions supplémentaires",notes:"Voir les notes de version pour {v}",dark:"Changer le mode sombre",ossDesc:"Mises à jour de sécurité et corrections de bugs gratuites.",entDesc:"Support pendant la période OSS plus support étendu après.",eolDesc:"Version en fin de vie. Plus de mises à jour.",today:"Date du jour : {date}",branch:"Version",initial:"Sortie initiale",ossEnd:"Fin OSS",entEnd:"Fin Entreprise *"}};class g{constructor(t,e,s={}){this.root=document.getElementById(t),this.root&&(this.options=s,this.visibleCount=s.visibleCount||3,this.showTable=s.showTable!==!1,this.isExpanded=!1,this.theme="light",this.filterText="",this.activeHighlight=null,this.translations={...c,...s.i18n||{}},this.locale=s.locale||this.detectLanguage(),this.setupBaseLayout(),this.updateData(e))}detectLanguage(){const t=(navigator.language||"en").split("-")[0];return this.translations[t]?t:"en"}t(t,e={}){let s=(this.translations[this.locale]||this.translations.en)[t]||t;return Object.keys(e).forEach(i=>s=s.replace(`{${i}}`,e[i])),s}setupBaseLayout(){this.root.innerHTML="",this.root.setAttribute("role","application"),this.root.setAttribute("aria-label","Product Lifecycle Timeline"),this.renderToolbar(),this.renderThemeToggle(),this.showTable&&(this.tableContainer=this.el("div","timeline-table-container",this.root)),this.wrapper=this.el("div","timeline-wrapper",this.root),this.wrapper.setAttribute("role","grid"),this.wrapper.setAttribute("aria-readonly","true"),this.axis=this.el("div","timeline-axis",this.wrapper),this.axis.setAttribute("role","row"),this.tracks=this.el("div","timeline-tracks",this.wrapper),this.legendContainer=this.el("div","timeline-legend-container",this.wrapper)}updateData(t){this.data=this.validateData(t||[]),this.calculateTimeRange(),this.render()}validateData(t){return t.filter((e,s)=>{const n=["version","ossStart","ossEnd"].filter(l=>!e[l]);if(n.length>0)return console.warn(`[Timeline] Missing fields for item at index ${s}: ${n.join(", ")}`),!1;const a=["ossStart","ossEnd","enterpriseEnd"].filter(l=>e[l]).filter(l=>isNaN(new Date(e[l]).getTime()));return a.length>0?(console.warn(`[Timeline] Invalid date format for item "${e.version}": ${a.join(", ")}`),!1):!0})}calculateTimeRange(){if(this.currentDate=new Date,!this.data.length){this.minYear=this.currentDate.getFullYear()-1,this.maxYear=this.currentDate.getFullYear()+3;return}const t=this.data.flatMap(e=>[new Date(e.ossStart),new Date(e.enterpriseEnd||e.ossEnd)]);this.minYear=new Date(Math.min(...t)).getFullYear(),this.maxYear=new Date(Math.max(...t)).getFullYear()}renderTable(){const t=this.el("table","timeline-table",this.tableContainer);t.setAttribute("aria-label","Project support");const e=this.el("thead","",t),s=this.el("tr","",e);[this.t("branch"),this.t("initial"),this.t("ossEnd"),this.t("entEnd")].forEach(i=>{this.el("th","",s).textContent=i}),this.tableBody=this.el("tbody","",t),this.tableRows=this.data.map(i=>this.createTableRow(i)),this.tableToggleContainer=this.el("div","timeline-table-toggle",this.tableContainer),this.updateTableVisibility()}createTableRow(t){const e=this.el("tr","",this.tableBody),s=Date.now(),i=new Date(t.ossStart).getTime(),n=new Date(t.ossEnd).getTime(),o=t.enterpriseEnd?new Date(t.enterpriseEnd).getTime():n,a=s>=i&&s<=n?"status-oss":s>n&&s<=o?"status-ent":s>o?"status-expired":"",l=this.el("td","",e);let d=`<span class="table-badge ${a}">${t.versionOriginal||t.version}</span>`;t.releaseNotesUrl?l.innerHTML=`<a href="${t.releaseNotesUrl}" target="_blank" class="table-version-link">${d}</a>`:l.innerHTML=d;const r=this.el("td","",e);r.textContent=t.ossStart,s>i&&(r.className="past-date");const h=this.el("td","",e);h.textContent=t.ossEnd,s>n&&(h.className="past-date");const u=this.el("td","",e);return u.textContent=t.enterpriseEnd||t.ossEnd,s>o&&(u.className="past-date"),{el:e,version:t.version.toLowerCase(),versionOriginal:t.versionOriginal||t.version}}updateTableVisibility(){const t=this.tableRows.filter(i=>i.version.includes(this.filterText));this.tableRows.forEach(i=>{const n=i.el.querySelector(".table-badge");n.innerHTML=this.highlight(i.versionOriginal||i.version),i.el.classList.remove("row-visible"),i.el.classList.add("row-hidden")});const e=this.filterText===""&&t.length>this.visibleCount;if((e&&!this.isExpanded?t.slice(0,this.visibleCount):t).forEach(i=>{i.el.classList.remove("row-hidden"),i.el.classList.add("row-visible")}),this.tableToggleContainer.innerHTML="",e){const i=this.el("button","timeline-toggle-btn table-toggle",this.tableToggleContainer),n=`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="${this.isExpanded?"18 15 12 9 6 15":"6 9 12 15 18 9"}"></polyline></svg>`;i.innerHTML=this.isExpanded?`${this.t("less")} ${n}`:`${this.t("more",{n:t.length-this.visibleCount})} ${n}`,i.onclick=()=>{this.isExpanded=!this.isExpanded,this.updateVisibility()}}}render(){this.showTable&&(this.tableContainer.innerHTML=""),this.axis.innerHTML="",this.tracks.innerHTML="",this.legendContainer.innerHTML="",this.showTable&&this.renderTable(),this.renderAxis(),this.grid=this.el("div","grid-lines-container",this.tracks),this.indicators=this.el("div","indicators-container",this.tracks),this.renderGrid(),this.renderCurrentDateLine(),this.rows=this.data.map((t,e)=>this.createRow(t,e)),this.toggleContainer=this.el("div","timeline-more-toggle",this.tracks,{paddingTop:"10px",display:"flex",justifyContent:"center",position:"relative",zIndex:"10"}),this.updateVisibility(),this.renderLegend(),this.setupTooltip()}setupTooltip(){this.tooltip&&this.tooltip.remove(),this.tooltip=this.el("div","timeline-tooltip-overlay",document.body),this.tooltip.style.display="none",this.tooltip.setAttribute("role","tooltip")}showTooltip(t,e){this.tooltip.innerHTML=e,this.tooltip.style.display="block",this.updateTooltipPos(t)}updateTooltipPos(t){let s=t.pageX+12,i=t.pageY+12;const n=this.tooltip.offsetWidth,o=this.tooltip.offsetHeight;s+n>window.innerWidth&&(s=t.pageX-n-12),i+o>window.innerHeight&&(i=t.pageY-o-12),this.tooltip.style.left=`${s}px`,this.tooltip.style.top=`${i}px`}hideTooltip(){this.tooltip&&(this.tooltip.style.display="none")}highlight(t){if(!this.filterText)return t;try{const e=new RegExp(`(${this.filterText})`,"gi");return t.replace(e,'<mark class="highlight-match">$1</mark>')}catch{return t}}el(t,e,s,i={}){const n=document.createElement(t);return e&&(n.className=e),Object.assign(n.style,i),s&&s.appendChild(n),n}renderToolbar(){const t=this.el("div","timeline-toolbar",this.root),e=this.el("div","timeline-filter-container",t);e.innerHTML='<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>';const s=this.el("input","timeline-filter-input",e);s.placeholder=this.t("filter"),s.value=this.filterText,s.setAttribute("aria-label",this.t("filter")),s.oninput=i=>{this.filterText=i.target.value.toLowerCase().trim(),this.updateVisibility()}}renderThemeToggle(){const t=this.el("button","theme-toggle-btn",this.root);t.title=this.t("dark"),t.setAttribute("aria-label",this.t("dark"));const e={moon:'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>',sun:'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><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>'};t.innerHTML=this.theme==="dark"?e.sun:e.moon,t.onclick=()=>{this.theme=this.theme==="light"?"dark":"light",document.documentElement.setAttribute("data-theme",this.theme),t.innerHTML=this.theme==="dark"?e.sun:e.moon,t.setAttribute("aria-pressed",this.theme==="dark")}}renderLegend(){this.legendContainer.innerHTML="";const t=this.el("div","release-legend",this.legendContainer);t.setAttribute("role","complementary"),t.setAttribute("aria-label","Support Legend"),["oss","ent","eol"].forEach(e=>{const s=this.el("div",`legend-block ${e==="ent"?"commercial":e}`,t);s.classList.add("legend-item-reactive"),this.activeHighlight===e&&s.classList.add("active-highlight"),s.innerHTML=`
|
|
2
|
+
<div class="legend-icon" aria-hidden="true"></div>
|
|
3
|
+
<div><h3>${this.t(e)}</h3><p>${this.t(e+"Desc")}</p></div>
|
|
4
|
+
`,s.onclick=()=>this.highlightSegment(e)})}highlightSegment(t){this.activeHighlight=this.activeHighlight===t?null:t,this.root.classList.remove("highlight-oss","highlight-ent","highlight-eol"),this.activeHighlight&&this.root.classList.add(`highlight-${this.activeHighlight}`),this.renderLegend()}renderAxis(){this.el("div","",this.axis,{width:"96px",flexShrink:"0"}).setAttribute("role","presentation");for(let e=this.minYear;e<=this.maxYear;e++){const s=this.el("div","timeline-year",this.axis);s.textContent=e,s.setAttribute("role","columnheader")}}renderGrid(){const t=new Date(this.minYear,0,1).getTime(),e=new Date(this.maxYear,11,31).getTime()-t;for(let s=this.minYear;s<=this.maxYear;s++){const i=this.el("div","year-grid-line",this.grid);i.style.left=`${(new Date(s,0,1).getTime()-t)/e*100}%`,i.setAttribute("role","presentation")}}createRow(t,e){const s=this.el("div","timeline-row row-entrance",this.tracks);s.setAttribute("role","row"),s.style.transitionDelay=`${e*.05}s`;const i=this.el("div","version-label",s);i.setAttribute("role","rowheader");const n=Date.now(),o=new Date(t.ossStart).getTime(),a=new Date(t.ossEnd).getTime(),l=t.enterpriseEnd?new Date(t.enterpriseEnd).getTime():a,d=n>=o&&n<=a?"status-oss":n>a&&n<=l?"status-ent":n>l?"status-expired":"";if(d&&i.classList.add(d),t.releaseNotesUrl){const h=this.el("a","version-link",i);h.href=t.releaseNotesUrl,h.target="_blank",h.innerHTML=this.highlight(t.version),h.title=this.t("notes",{v:t.version}),h.setAttribute("aria-label",this.t("notes",{v:t.version}))}else i.innerHTML=this.highlight(t.version);const r=this.el("div","track-container",s);return r.setAttribute("role","gridcell"),r.appendChild(this.createBar(t,t.ossStart,t.enterpriseEnd||t.ossEnd,"bar-ent",this.t("ent"))),r.appendChild(this.createBar(t,t.ossStart,t.ossEnd,"bar-oss",this.t("oss"))),{el:s,version:t.version.toLowerCase(),versionOriginal:t.version}}createBar(t,e,s,i,n){const o=new Date(e).getTime(),a=new Date(s).getTime(),l=new Date(this.minYear,0,1).getTime(),d=new Date(this.maxYear,11,31).getTime()-l,r=this.el("div",`bar-segment ${i}`),h=i==="bar-oss"?"segment-oss":"segment-ent";r.classList.add(h),r.style.left=`${(o-l)/d*100}%`,r.style.width=`${(a-o)/d*100}%`,r.setAttribute("role","img"),r.setAttribute("aria-label",`${n}: ${t.version} (${e} to ${s})`),r.tabIndex=0;const u=`
|
|
5
|
+
<div class="tooltip-header">${n} - ${t.version}</div>
|
|
6
|
+
<div class="tooltip-date"><strong>Du:</strong> ${e}</div>
|
|
13
7
|
<div class="tooltip-date"><strong>Au:</strong> ${s}</div>
|
|
14
|
-
`;return r.onmouseenter=
|
|
8
|
+
`;return r.onmouseenter=p=>this.showTooltip(p,u),r.onmousemove=p=>this.updateTooltipPos(p),r.onmouseleave=()=>this.hideTooltip(),r.onfocus=p=>{const b=r.getBoundingClientRect();this.showTooltip({pageX:b.left+window.scrollX,pageY:b.top+window.scrollY-40},u)},r.onblur=()=>this.hideTooltip(),r}updateVisibility(){const t=this.rows.filter(i=>i.version.includes(this.filterText));this.rows.forEach(i=>{const n=i.el.querySelector(".version-label"),o=n.querySelector(".version-link");o?o.innerHTML=this.highlight(i.versionOriginal||i.version):n.innerHTML=this.highlight(i.versionOriginal||i.version),i.el.classList.remove("row-visible"),i.el.classList.add("row-hidden")});const e=this.filterText===""&&t.length>this.visibleCount;if((e&&!this.isExpanded?t.slice(0,this.visibleCount):t).forEach(i=>{i.el.classList.remove("row-hidden"),i.el.classList.add("row-visible")}),this.showTable&&this.updateTableVisibility(),this.toggleContainer.innerHTML="",e){const i=this.el("button","timeline-toggle-btn",this.toggleContainer),n=`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="${this.isExpanded?"18 15 12 9 6 15":"6 9 12 15 18 9"}"></polyline></svg>`;i.innerHTML=this.isExpanded?`${this.t("less")} ${n}`:`${this.t("more",{n:t.length-this.visibleCount})} ${n}`,i.setAttribute("aria-expanded",this.isExpanded),i.onclick=()=>{this.isExpanded=!this.isExpanded,this.updateVisibility()}}}renderCurrentDateLine(){const t=new Date(this.minYear,0,1).getTime(),e=new Date(this.maxYear,11,31).getTime()-t,s=Date.now()-t;if(s<0||s>e)return;const i=new Date().toISOString().split("T")[0],n=this.el("div","current-date-indicator",this.indicators);n.style.left=`${s/e*100}%`,n.setAttribute("role","separator"),n.setAttribute("aria-label",this.t("today",{date:i}));const o=this.el("div","current-date-badge",n);o.textContent=i,o.setAttribute("aria-hidden","true")}}return g}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lifecycle-timeline",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "A beautiful, premium JS timeline component for visualizing product lifecycles, OSS support, and enterprise support dates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/timeline.umd.cjs",
|
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
"import": "./dist/timeline.js",
|
|
13
13
|
"require": "./dist/timeline.umd.cjs"
|
|
14
14
|
},
|
|
15
|
-
"./style.css": "./dist/timeline.css"
|
|
16
|
-
"./dist/timeline.css": "./dist/timeline.css"
|
|
15
|
+
"./style.css": "./dist/timeline.css"
|
|
17
16
|
},
|
|
18
17
|
"files": [
|
|
19
18
|
"dist",
|
|
@@ -22,7 +21,7 @@
|
|
|
22
21
|
"scripts": {
|
|
23
22
|
"dev": "vite",
|
|
24
23
|
"build": "vite build",
|
|
25
|
-
"build:demo": "
|
|
24
|
+
"build:demo": "vite build --config vite.config.demo.js",
|
|
26
25
|
"preview": "vite preview",
|
|
27
26
|
"test": "vitest run",
|
|
28
27
|
"prepublishOnly": "npm run build",
|
|
@@ -63,5 +62,8 @@
|
|
|
63
62
|
"jsdom": "^24.1.3",
|
|
64
63
|
"vite": "^7.3.0",
|
|
65
64
|
"vitest": "^4.0.16"
|
|
65
|
+
},
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"html-to-image": "^1.11.13"
|
|
66
68
|
}
|
|
67
69
|
}
|
package/dist/timeline.css
DELETED
|
@@ -1 +0,0 @@
|
|
|
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: #6b7280;--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}.grid-lines-container{position:absolute;inset:0 0 0 96px;pointer-events:none;z-index:5}.indicators-container{position:absolute;inset:0 0 0 96px;pointer-events:none;z-index:20}.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:2px solid var(--grid-line);opacity:1;z-index:5;pointer-events:none}.timeline-tooltip-overlay{position:absolute;background:var(--tooltip-bg);color:var(--text-primary);padding:10px 14px;border-radius:8px;font-size:.85rem;box-shadow:0 10px 15px -3px var(--tooltip-shadow),0 4px 6px -4px var(--tooltip-shadow);border:1px solid var(--border-color);pointer-events:none;z-index:10000;transition:opacity .15s ease-out;line-height:1.4}.tooltip-header{font-weight:700;margin-bottom:4px;color:var(--current-date);text-transform:uppercase;font-size:.75rem;letter-spacing:.05em}.tooltip-date strong{color:var(--text-secondary)}.timeline-row{transition:opacity .3s ease,transform .3s ease,height .3s ease}.timeline-row[style*="display: none"]{opacity:0;transform:translateY(10px)}.bar-segment{transition:transform .2s ease,box-shadow .2s ease}.bar-segment:hover{box-shadow:0 4px 12px #0000001a}.bar-segment:focus-visible{outline:2px solid var(--text-primary);outline-offset:-2px;z-index:100}.track-container{transition:background-color .3s ease}.release-legend{margin:40px auto 0;padding:24px;background-color:var(--bg-card);border:1px solid var(--border-color);border-radius:12px;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.eol .legend-icon{background-color:var(--bg-track)}.timeline-toolbar{display:flex;justify-content:flex-start;align-items:center;margin-bottom:24px;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:400px;flex-grow:1;transition:all .3s ease;box-shadow:0 2px 4px #0000000d}.timeline-filter-container:focus-within{border-color:var(--current-date);box-shadow:0 8px 20px #086dc31a}.timeline-filter-input{border:none;outline:none;font-size:.95rem;color:var(--text-primary);background:transparent;width:100%}.highlight-match{background-color:#fde68a;color:#92400e;padding:0 2px;border-radius:2px;font-weight:700}.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);transform:translateY(-1px)}.timeline-toggle-btn:focus-visible{outline:2px solid var(--current-date);outline-offset:2px}.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 4px 6px -1px #0000001a}.theme-toggle-btn:focus-visible{outline:2px solid var(--current-date);outline-offset:2px}#timeline-root{position:relative}.theme-toggle-btn:hover{background-color:var(--bg-track);transform:scale(1.1)}@media(max-width:1024px){.timeline-wrapper{padding-left:0}}@media(max-width:768px){.page-container{padding:1rem}.timeline-toolbar{flex-direction:column;align-items:stretch}.timeline-filter-container{max-width:none}}.timeline-row,.timeline-table tr{transition:opacity .4s ease,transform .4s cubic-bezier(.4,0,.2,1)}.row-hidden{display:none!important;opacity:0;transform:translateY(-10px)}.row-visible{display:flex!important;opacity:1;transform:translateY(0)}.timeline-table tr.row-visible{display:table-row!important}.timeline-tracks,.timeline-table tbody{transition:max-height .5s ease-in-out}.timeline-table-container{margin-top:1rem;margin-bottom:2rem;overflow-x:auto;border-radius:12px;border:1px solid var(--border-color);background-color:var(--bg-card);transition:all .3s ease}.timeline-table{width:100%;border-collapse:collapse;font-size:.85rem;text-align:left}.timeline-table th{background-color:var(--bg-page);color:var(--text-secondary);font-weight:700;padding:14px 16px;border-bottom:1px solid var(--border-color);text-transform:uppercase;font-size:.75rem;letter-spacing:.05em}.timeline-table td{padding:12px 16px;border-bottom:1px solid var(--border-color);color:var(--text-primary);vertical-align:middle}.timeline-table tr:hover td{background-color:var(--bg-page)}.timeline-table tr:last-child td{border-bottom:none}.table-badge{display:inline-block;padding:4px 12px;border-radius:12px;font-size:.85rem;font-weight:600;font-family:var(--font-family);text-align:center;min-width:64px;transition:all .3s ease}.table-version-link{text-decoration:none;color:inherit;display:inline-block}.table-version-link:hover .table-badge{transform:translateY(-1px);box-shadow:0 4px 8px #0000001a}.table-badge.status-oss{background-color:var(--accent-oss);color:#000}.table-badge.status-ent{background-color:var(--accent-ent);color:#000}.table-badge.status-expired{background-color:var(--bg-track);color:var(--text-secondary)}.past-date{color:var(--text-secondary);opacity:.7;font-style:italic}.timeline-table-toggle{display:flex;justify-content:center;padding:10px;background-color:var(--bg-card);border-top:1px solid var(--border-color);border-bottom-left-radius:12px;border-bottom-right-radius:12px}.timeline-toggle-btn.table-toggle{margin-top:10px}
|