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 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
- **[🌐 View Live Demo](https://ericreboisson.github.io/lifecycle-timeline/)**
5
+
6
+ [**🚀 View Live Demo**](https://ericreboisson.github.io/lifecycle-timeline/)
6
7
 
7
8
  ![Lifecycle Timeline Screenshot](assets/screenshot.png)
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 b {
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(e, i, s = {}) {
50
- this.root = document.getElementById(e), this.root && (this.options = s, this.visibleCount = s.visibleCount || 3, this.showTable = s.showTable !== !1, this.isExpanded = !1, this.theme = "light", this.filterText = "", this.translations = { ...u, ...s.i18n || {} }, this.locale = s.locale || this.detectLanguage(), this.setupBaseLayout(), this.updateData(i));
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 e = (navigator.language || "en").split("-")[0];
58
- return this.translations[e] ? e : "en";
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(e, i = {}) {
67
- let s = (this.translations[this.locale] || this.translations.en)[e] || e;
68
- return Object.keys(i).forEach((t) => s = s.replace(`{${t}}`, i[t])), s;
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(e) {
81
- this.data = this.validateData(e || []), this.calculateTimeRange(), this.render();
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(e) {
89
- return e.filter((i, s) => {
90
- const n = ["version", "ossStart", "ossEnd"].filter((l) => !i[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) => i[l]).filter((l) => isNaN(new Date(i[l]).getTime()));
94
- return a.length > 0 ? (console.warn(`[Timeline] Invalid date format for item "${i.version}": ${a.join(", ")}`), !1) : !0;
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 e = this.data.flatMap((i) => [
106
- new Date(i.ossStart),
107
- new Date(i.enterpriseEnd || i.ossEnd)
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(...e)).getFullYear(), this.maxYear = new Date(Math.max(...e)).getFullYear();
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 e = this.el("table", "timeline-table", this.tableContainer);
116
- e.setAttribute("aria-label", "Project support");
117
- const i = this.el("thead", "", e), s = this.el("tr", "", i);
118
- [this.t("branch"), this.t("initial"), this.t("ossEnd"), this.t("entEnd")].forEach((t) => {
119
- this.el("th", "", s).textContent = t;
120
- }), this.tableBody = this.el("tbody", "", e), this.tableRows = this.data.map((t) => this.createTableRow(t)), this.tableToggleContainer = this.el("div", "timeline-table-toggle", this.tableContainer), this.updateTableVisibility();
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(e) {
127
- const i = this.el("tr", "", this.tableBody), s = Date.now(), t = new Date(e.ossStart).getTime(), n = new Date(e.ossEnd).getTime(), o = e.enterpriseEnd ? new Date(e.enterpriseEnd).getTime() : n, a = s >= t && s <= n ? "status-oss" : s > n && s <= o ? "status-ent" : s > o ? "status-expired" : "", l = this.el("td", "", i);
128
- let h = `<span class="table-badge ${a}">${e.versionOriginal || e.version}</span>`;
129
- e.releaseNotesUrl ? l.innerHTML = `<a href="${e.releaseNotesUrl}" target="_blank" class="table-version-link">${h}</a>` : l.innerHTML = h;
130
- const r = this.el("td", "", i);
131
- r.textContent = e.ossStart, s > t && (r.className = "past-date");
132
- const c = this.el("td", "", i);
133
- c.textContent = e.ossEnd, s > n && (c.className = "past-date");
134
- const d = this.el("td", "", i);
135
- return d.textContent = e.enterpriseEnd || e.ossEnd, s > o && (d.className = "past-date"), { el: i, version: e.version.toLowerCase(), versionOriginal: e.versionOriginal || e.version };
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 e = this.tableRows.filter((t) => t.version.includes(this.filterText));
142
- this.tableRows.forEach((t) => {
143
- const n = t.el.querySelector(".table-badge");
144
- n.innerHTML = this.highlight(t.versionOriginal || t.version), t.el.classList.remove("row-visible"), t.el.classList.add("row-hidden");
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 i = this.filterText === "" && e.length > this.visibleCount;
147
- if ((i && !this.isExpanded ? e.slice(0, this.visibleCount) : e).forEach((t) => {
148
- t.el.classList.remove("row-hidden"), t.el.classList.add("row-visible");
149
- }), this.tableToggleContainer.innerHTML = "", i) {
150
- const t = 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
- t.innerHTML = this.isExpanded ? `${this.t("less")} ${n}` : `${this.t("more", { n: e.length - this.visibleCount })} ${n}`, t.onclick = () => {
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(e, i) {
180
- this.tooltip.innerHTML = i, this.tooltip.style.display = "block", this.updateTooltipPos(e);
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(e) {
187
- let s = e.pageX + 12, t = e.pageY + 12;
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 = e.pageX - n - 12), t + o > window.innerHeight && (t = e.pageY - o - 12), this.tooltip.style.left = `${s}px`, this.tooltip.style.top = `${t}px`;
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(e) {
203
- if (!this.filterText) return e;
202
+ highlight(t) {
203
+ if (!this.filterText) return t;
204
204
  try {
205
- const i = new RegExp(`(${this.filterText})`, "gi");
206
- return e.replace(i, '<mark class="highlight-match">$1</mark>');
205
+ const e = new RegExp(`(${this.filterText})`, "gi");
206
+ return t.replace(e, '<mark class="highlight-match">$1</mark>');
207
207
  } catch {
208
- return e;
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(e, i, s, t = {}) {
220
- const n = document.createElement(e);
221
- return i && (n.className = i), Object.assign(n.style, t), s && s.appendChild(n), n;
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 e = this.el("div", "timeline-toolbar", this.root), i = this.el("div", "timeline-filter-container", e);
228
- i.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", i);
230
- s.placeholder = this.t("filter"), s.value = this.filterText, s.setAttribute("aria-label", this.t("filter")), s.oninput = (t) => {
231
- this.filterText = t.target.value.toLowerCase().trim(), this.updateVisibility();
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 e = this.el("button", "theme-toggle-btn", this.root);
239
- e.title = this.t("dark"), e.setAttribute("aria-label", this.t("dark"));
240
- const i = {
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
- e.innerHTML = this.theme === "dark" ? i.sun : i.moon, e.onclick = () => {
245
- this.theme = this.theme === "light" ? "dark" : "light", document.documentElement.setAttribute("data-theme", this.theme), e.innerHTML = this.theme === "dark" ? i.sun : i.moon, e.setAttribute("aria-pressed", this.theme === "dark");
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
- <div class="release-legend" role="complementary" aria-label="Support Legend">
254
- ${["oss", "ent", "eol"].map((e) => `
255
- <div class="legend-block ${e === "ent" ? "commercial" : e}">
256
- <div class="legend-icon" aria-hidden="true"></div>
257
- <div><h3>${this.t(e)}</h3><p>${this.t(e + "Desc")}</p></div>
258
- </div>
259
- `).join("")}
260
- </div>
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 i = this.minYear; i <= this.maxYear; i++) {
274
+ for (let e = this.minYear; e <= this.maxYear; e++) {
269
275
  const s = this.el("div", "timeline-year", this.axis);
270
- s.textContent = i, s.setAttribute("role", "columnheader");
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 e = new Date(this.minYear, 0, 1).getTime(), i = new Date(this.maxYear, 11, 31).getTime() - e;
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 t = this.el("div", "year-grid-line", this.grid);
280
- t.style.left = `${(new Date(s, 0, 1).getTime() - e) / i * 100}%`, t.setAttribute("role", "presentation");
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 i = this.el("div", "timeline-row", this.tracks);
290
- i.setAttribute("role", "row");
291
- const s = this.el("div", "version-label", i);
292
- s.setAttribute("role", "rowheader");
293
- 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" : "";
294
- if (l && s.classList.add(l), e.releaseNotesUrl) {
295
- const r = this.el("a", "version-link", s);
296
- 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 }));
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
- s.innerHTML = this.highlight(e.version);
299
- const h = this.el("div", "track-container", i);
300
- 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 };
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} prefix - Support type prefix for tooltip.
314
+ * @param {string} label - Support type label for tooltip.
309
315
  * @returns {HTMLElement} The created bar element.
310
316
  */
311
- createBar(e, i, s, t, n) {
312
- 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}`);
313
- 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;
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} - ${e.version}</div>
316
- <div class="tooltip-date"><strong>Du:</strong> ${i}</div>
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 = (d) => this.showTooltip(d, c), r.onmousemove = (d) => this.updateTooltipPos(d), r.onmouseleave = () => this.hideTooltip(), r.onfocus = (d) => {
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 e = this.rows.filter((t) => t.version.includes(this.filterText));
329
- this.rows.forEach((t) => {
330
- const n = t.el.querySelector(".version-label"), o = n.querySelector(".version-link");
331
- o ? o.innerHTML = this.highlight(t.versionOriginal || t.version) : n.innerHTML = this.highlight(t.versionOriginal || t.version), t.el.classList.remove("row-visible"), t.el.classList.add("row-hidden");
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 i = this.filterText === "" && e.length > this.visibleCount;
334
- if ((i && !this.isExpanded ? e.slice(0, this.visibleCount) : e).forEach((t) => {
335
- t.el.classList.remove("row-hidden"), t.el.classList.add("row-visible");
336
- }), this.showTable && this.updateTableVisibility(), this.toggleContainer.innerHTML = "", i) {
337
- const t = 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>`;
338
- t.innerHTML = this.isExpanded ? `${this.t("less")} ${n}` : `${this.t("more", { n: e.length - this.visibleCount })} ${n}`, t.setAttribute("aria-expanded", this.isExpanded), t.onclick = () => {
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 e = new Date(this.minYear, 0, 1).getTime(), i = new Date(this.maxYear, 11, 31).getTime() - e, s = Date.now() - e;
348
- if (s < 0 || s > i) return;
349
- const t = (/* @__PURE__ */ new Date()).toISOString().split("T")[0], n = this.el("div", "current-date-indicator", this.indicators);
350
- n.style.left = `${s / i * 100}%`, n.setAttribute("role", "separator"), n.setAttribute("aria-label", this.t("today", { date: t }));
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 = t, o.setAttribute("aria-hidden", "true");
358
+ o.textContent = i, o.setAttribute("aria-hidden", "true");
353
359
  }
354
360
  }
355
361
  export {
356
- b as default
362
+ v as default
357
363
  };
@@ -1,14 +1,8 @@
1
- (function(c,p){typeof exports=="object"&&typeof module<"u"?module.exports=p():typeof define=="function"&&define.amd?define(p):(c=typeof globalThis<"u"?globalThis:c||self,c.Timeline=p())})(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 p{constructor(e,i,s={}){this.root=document.getElementById(e),this.root&&(this.options=s,this.visibleCount=s.visibleCount||3,this.showTable=s.showTable!==!1,this.isExpanded=!1,this.theme="light",this.filterText="",this.translations={...c,...s.i18n||{}},this.locale=s.locale||this.detectLanguage(),this.setupBaseLayout(),this.updateData(i))}detectLanguage(){const e=(navigator.language||"en").split("-")[0];return this.translations[e]?e:"en"}t(e,i={}){let s=(this.translations[this.locale]||this.translations.en)[e]||e;return Object.keys(i).forEach(t=>s=s.replace(`{${t}}`,i[t])),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(e){this.data=this.validateData(e||[]),this.calculateTimeRange(),this.render()}validateData(e){return e.filter((i,s)=>{const n=["version","ossStart","ossEnd"].filter(l=>!i[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=>i[l]).filter(l=>isNaN(new Date(i[l]).getTime()));return a.length>0?(console.warn(`[Timeline] Invalid date format for item "${i.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 e=this.data.flatMap(i=>[new Date(i.ossStart),new Date(i.enterpriseEnd||i.ossEnd)]);this.minYear=new Date(Math.min(...e)).getFullYear(),this.maxYear=new Date(Math.max(...e)).getFullYear()}renderTable(){const e=this.el("table","timeline-table",this.tableContainer);e.setAttribute("aria-label","Project support");const i=this.el("thead","",e),s=this.el("tr","",i);[this.t("branch"),this.t("initial"),this.t("ossEnd"),this.t("entEnd")].forEach(t=>{this.el("th","",s).textContent=t}),this.tableBody=this.el("tbody","",e),this.tableRows=this.data.map(t=>this.createTableRow(t)),this.tableToggleContainer=this.el("div","timeline-table-toggle",this.tableContainer),this.updateTableVisibility()}createTableRow(e){const i=this.el("tr","",this.tableBody),s=Date.now(),t=new Date(e.ossStart).getTime(),n=new Date(e.ossEnd).getTime(),o=e.enterpriseEnd?new Date(e.enterpriseEnd).getTime():n,a=s>=t&&s<=n?"status-oss":s>n&&s<=o?"status-ent":s>o?"status-expired":"",l=this.el("td","",i);let h=`<span class="table-badge ${a}">${e.versionOriginal||e.version}</span>`;e.releaseNotesUrl?l.innerHTML=`<a href="${e.releaseNotesUrl}" target="_blank" class="table-version-link">${h}</a>`:l.innerHTML=h;const r=this.el("td","",i);r.textContent=e.ossStart,s>t&&(r.className="past-date");const u=this.el("td","",i);u.textContent=e.ossEnd,s>n&&(u.className="past-date");const d=this.el("td","",i);return d.textContent=e.enterpriseEnd||e.ossEnd,s>o&&(d.className="past-date"),{el:i,version:e.version.toLowerCase(),versionOriginal:e.versionOriginal||e.version}}updateTableVisibility(){const e=this.tableRows.filter(t=>t.version.includes(this.filterText));this.tableRows.forEach(t=>{const n=t.el.querySelector(".table-badge");n.innerHTML=this.highlight(t.versionOriginal||t.version),t.el.classList.remove("row-visible"),t.el.classList.add("row-hidden")});const i=this.filterText===""&&e.length>this.visibleCount;if((i&&!this.isExpanded?e.slice(0,this.visibleCount):e).forEach(t=>{t.el.classList.remove("row-hidden"),t.el.classList.add("row-visible")}),this.tableToggleContainer.innerHTML="",i){const t=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>`;t.innerHTML=this.isExpanded?`${this.t("less")} ${n}`:`${this.t("more",{n:e.length-this.visibleCount})} ${n}`,t.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(e=>this.createRow(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(e,i){this.tooltip.innerHTML=i,this.tooltip.style.display="block",this.updateTooltipPos(e)}updateTooltipPos(e){let s=e.pageX+12,t=e.pageY+12;const n=this.tooltip.offsetWidth,o=this.tooltip.offsetHeight;s+n>window.innerWidth&&(s=e.pageX-n-12),t+o>window.innerHeight&&(t=e.pageY-o-12),this.tooltip.style.left=`${s}px`,this.tooltip.style.top=`${t}px`}hideTooltip(){this.tooltip&&(this.tooltip.style.display="none")}highlight(e){if(!this.filterText)return e;try{const i=new RegExp(`(${this.filterText})`,"gi");return e.replace(i,'<mark class="highlight-match">$1</mark>')}catch{return e}}el(e,i,s,t={}){const n=document.createElement(e);return i&&(n.className=i),Object.assign(n.style,t),s&&s.appendChild(n),n}renderToolbar(){const e=this.el("div","timeline-toolbar",this.root),i=this.el("div","timeline-filter-container",e);i.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",i);s.placeholder=this.t("filter"),s.value=this.filterText,s.setAttribute("aria-label",this.t("filter")),s.oninput=t=>{this.filterText=t.target.value.toLowerCase().trim(),this.updateVisibility()}}renderThemeToggle(){const e=this.el("button","theme-toggle-btn",this.root);e.title=this.t("dark"),e.setAttribute("aria-label",this.t("dark"));const i={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>'};e.innerHTML=this.theme==="dark"?i.sun:i.moon,e.onclick=()=>{this.theme=this.theme==="light"?"dark":"light",document.documentElement.setAttribute("data-theme",this.theme),e.innerHTML=this.theme==="dark"?i.sun:i.moon,e.setAttribute("aria-pressed",this.theme==="dark")}}renderLegend(){this.legendContainer.innerHTML=`
2
- <div class="release-legend" role="complementary" aria-label="Support Legend">
3
- ${["oss","ent","eol"].map(e=>`
4
- <div class="legend-block ${e==="ent"?"commercial":e}">
5
- <div class="legend-icon" aria-hidden="true"></div>
6
- <div><h3>${this.t(e)}</h3><p>${this.t(e+"Desc")}</p></div>
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=d=>this.showTooltip(d,u),r.onmousemove=d=>this.updateTooltipPos(d),r.onmouseleave=()=>this.hideTooltip(),r.onfocus=d=>{const g=r.getBoundingClientRect();this.showTooltip({pageX:g.left+window.scrollX,pageY:g.top+window.scrollY-40},u)},r.onblur=()=>this.hideTooltip(),r}updateVisibility(){const e=this.rows.filter(t=>t.version.includes(this.filterText));this.rows.forEach(t=>{const n=t.el.querySelector(".version-label"),o=n.querySelector(".version-link");o?o.innerHTML=this.highlight(t.versionOriginal||t.version):n.innerHTML=this.highlight(t.versionOriginal||t.version),t.el.classList.remove("row-visible"),t.el.classList.add("row-hidden")});const i=this.filterText===""&&e.length>this.visibleCount;if((i&&!this.isExpanded?e.slice(0,this.visibleCount):e).forEach(t=>{t.el.classList.remove("row-hidden"),t.el.classList.add("row-visible")}),this.showTable&&this.updateTableVisibility(),this.toggleContainer.innerHTML="",i){const t=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>`;t.innerHTML=this.isExpanded?`${this.t("less")} ${n}`:`${this.t("more",{n:e.length-this.visibleCount})} ${n}`,t.setAttribute("aria-expanded",this.isExpanded),t.onclick=()=>{this.isExpanded=!this.isExpanded,this.updateVisibility()}}}renderCurrentDateLine(){const e=new Date(this.minYear,0,1).getTime(),i=new Date(this.maxYear,11,31).getTime()-e,s=Date.now()-e;if(s<0||s>i)return;const t=new Date().toISOString().split("T")[0],n=this.el("div","current-date-indicator",this.indicators);n.style.left=`${s/i*100}%`,n.setAttribute("role","separator"),n.setAttribute("aria-label",this.t("today",{date:t}));const o=this.el("div","current-date-badge",n);o.textContent=t,o.setAttribute("aria-hidden","true")}}return p}));
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.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": "BUILD_SITE=true vite build --outDir dist-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}