diff --git a/docs/changelog/assets/css/app.css b/docs/changelog/assets/css/app.css
index 796ec2d6a..31de425f3 100644
--- a/docs/changelog/assets/css/app.css
+++ b/docs/changelog/assets/css/app.css
@@ -1,282 +1,476 @@
+/*
+ * CSS Variables for consistent theming
+ *
+ * Usage in CSS: background-color: var(--color-webex-blue);
+ * Usage in JS: element.style.backgroundColor = 'var(--color-success)';
+ *
+ * Available variables:
+ * --color-success: Success state (green)
+ * --color-success-hover: Success hover state
+ * --color-danger: Error/danger state (red)
+ * --color-danger-hover: Danger hover state
+ * --color-error-bg: Error message background (light red)
+ * --color-error-text: Error message text color
+ * --color-webex-blue: Primary brand color
+ * --color-webex-blue-hover: Primary brand hover color
+ * --color-background: Page background color (light gray)
+ *
+ * Units: Use rem for font-size, padding, margin, gap, border-radius (scales with root).
+ * Use px for 1px borders, box-shadow blur/spread, sticky offsets, scrollbar size (crisp, consistent).
+ */
+:root {
+ --color-success: #28a745;
+ --color-success-hover: #218838;
+ --color-danger: #dc3545;
+ --color-danger-hover: #c82333;
+ --color-error-bg: #fee;
+ --color-error-text: #721c24;
+ --color-webex-blue: #00bceb;
+ --color-webex-blue-hover: #0099ba;
+ --color-background: #f5f5f5;
+}
+
* {
- box-sizing: border-box;
+ box-sizing: border-box;
}
::-webkit-scrollbar {
- -webkit-appearance: none;
- width: 7px;
+ -webkit-appearance: none;
+ width: 7px;
}
::-webkit-scrollbar-thumb {
- border-radius: 4px;
- background-color: rgba(0, 0, 0, .5);
- box-shadow: 0 0 1px rgba(255, 255, 255, .5);
+ border-radius: 4px;
+ background-color: rgba(0, 0, 0, 0.5);
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.5);
}
body {
- font-family: 'Roboto', sans-serif;
- background-color: #f5f5f5;
- margin: 0;
- display: flex;
- flex-direction: column;
- min-height: 100vh;
+ font-family: 'Roboto', sans-serif;
+ background-color: var(--color-background);
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
}
-.header_anchor{
- text-decoration: none;
+.header_anchor {
+ text-decoration: none;
}
#header {
- position: sticky;
- top: 0;
- background-color: #00bceb; /* Webex blue */
- color: white;
- padding: 20px;
- margin: 0;
- text-align: left; /* Align the title text to the left */
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
- font-size: 1.5rem;
- z-index: 1000; /* Ensure the header is above other content */
+ position: sticky;
+ top: 0;
+ background-color: var(--color-webex-blue);
+ color: white;
+ padding: 1.25rem; /* 20px */
+ margin: 0;
+ text-align: left; /* Align the title text to the left */
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
+ font-size: 1.5rem;
+ z-index: 1000; /* Ensure the header is above other content */
}
#body {
- flex: 1;
- padding: 20px;
+ flex: 1;
+ padding: 1.25rem; /* 20px */
}
#footer {
- background-color: #f5f5f5;
- text-align: center;
- padding: 10px;
- box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.2);
+ background-color: var(--color-background);
+ text-align: center;
+ padding: 0.625rem; /* 10px */
+ box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.2);
}
#footer a {
- color: #00bceb; /* Webex blue */
- text-decoration: none;
- font-weight: bold;
+ color: var(--color-webex-blue);
+ text-decoration: none;
+ font-weight: bold;
}
#footer a:hover {
- text-decoration: underline;
+ text-decoration: underline;
}
/* Rest of the CSS for form and results remains the same */
form#search-form {
- padding: 10px;
+ padding: 0.625rem; /* 10px */
}
label {
- display: block;
- margin-bottom: 8px;
- color: #333;
+ display: block;
+ margin-bottom: 0.5rem; /* 8px */
+ color: #333;
}
-input[type="text"],
+input[type='text'],
select {
- width: 100%;
- padding: 10px;
- margin-bottom: 20px;
- border: 1px solid #ccc;
- border-radius: 4px;
- box-sizing: border-box;
+ width: 100%;
+ padding: 0.625rem;
+ margin-bottom: 1.25rem;
+ border: 1px solid #ccc;
+ border-radius: 0.25rem;
+ box-sizing: border-box;
}
button {
- background-color: #00bceb; /* Webex blue */
- color: white;
- padding: 10px 20px;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-size: 16px;
- transition: background-color 0.3s;
+ background-color: var(--color-webex-blue);
+ color: white;
+ padding: 0.625rem 1.25rem; /* 10px 20px */
+ border: none;
+ border-radius: 0.25rem; /* 4px */
+ cursor: pointer;
+ font-size: 1rem; /* 16px */
+ transition: background-color 0.3s;
}
button:hover {
- background-color: #0099ba; /* Darker Webex blue */
+ background-color: var(--color-webex-blue-hover);
}
table {
- width: 100%;
- border-collapse: collapse;
+ width: 100%;
+ border-collapse: collapse;
}
-table, th, td {
- border: 1px solid #ccc;
+table,
+th,
+td {
+ border: 1px solid #ccc;
}
-th, td {
- padding: 10px;
- text-align: left;
+th,
+td {
+ padding: 0.625rem; /* 10px */
+ text-align: left;
}
th {
- background-color: #f5f5f5;
+ background-color: var(--color-background);
}
.search-container {
- background: white;
- border-radius: 8px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- margin: auto;
- width: 100%;
- padding: 20px; /* Add padding for spacing */
- box-sizing: border-box; /* Include padding in the width calculation */
+ background: white;
+ border-radius: 0.5rem; /* 8px */
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ margin: auto;
+ width: 100%;
+ padding: 1.25rem; /* 20px */
+ box-sizing: border-box; /* Include padding in the width calculation */
}
.form-row {
- display: flex;
- flex-wrap: wrap;
- margin-bottom: 20px;
+ display: flex;
+ flex-wrap: wrap;
+ margin-bottom: 1.25rem; /* 20px */
}
.form-group {
- flex: 1 1 25%; /* Each form group will take up half the width of the form-row */
- padding: 0 10px;
- box-sizing: border-box;
+ flex: 1 1 25%; /* Each form group will take up half the width of the form-row */
+ padding: 0 0.625rem; /* 10px */
+ box-sizing: border-box;
}
.full-width {
- margin: 0 10px; /* Add margin to maintain spacing */
+ margin: 0 0.625rem; /* 10px */
}
.results-container {
- width: 100%;
- background: white;
- padding: 20px;
- border-radius: 8px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- margin: auto;
- margin-top: 20px;
+ width: 100%;
+ background: white;
+ padding: 1.25rem; /* 20px */
+ border-radius: 0.5rem; /* 8px */
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ margin: auto;
+ margin-top: 1.25rem; /* 20px */
}
/* Responsive styles */
-@media (max-width: 768px) {
- .form-group {
- flex: 1 1 100%; /* Each form group will take up the full width on smaller screens */
- padding: 0 10px; /* Adjust padding for smaller screens */
- }
+@media (max-width: 48rem) {
+ /* 768px */
+ .form-group {
+ flex: 1 1 100%; /* Each form group will take up the full width on smaller screens */
+ padding: 0 0.625rem; /* 10px */
+ }
}
.changelog-item {
- padding-bottom: 20px;
- margin-bottom: 20px; /* Space between changelog items */
- border-bottom: 3px solid #e4e5e6;
+ padding-bottom: 1.25rem; /* 20px */
+ margin-bottom: 1.25rem; /* 20px */
+ border-bottom: 0.1875rem solid #e4e5e6; /* 3px */
}
-.changelog-item h2, .changelog-item h3 {
- color: #333;
- margin-bottom: 10px;
+.changelog-item h2,
+.changelog-item h3 {
+ color: #333;
+ margin-bottom: 0.625rem; /* 10px */
}
-.changelog-item .commits{
- margin-top: 20px;
- margin-bottom: 20px;
+.changelog-item .commits {
+ margin-top: 1.25rem; /* 20px */
+ margin-bottom: 1.25rem; /* 20px */
}
.changelog-item .commits ul {
- margin-top: 10px;
- list-style-type: none;
- padding: 0;
- max-height: 200px;
- overflow: scroll;
+ margin-top: 0.625rem; /* 10px */
+ list-style-type: none;
+ padding: 0;
+ max-height: 12.5rem; /* 200px */
+ overflow: scroll;
}
.changelog-item .commits ul li {
- background-color: #eee;
- margin-bottom: 5px;
- padding: 10px;
- border-radius: 3px;
- word-wrap: break-word;
+ background-color: #eee;
+ margin-bottom: 0.3125rem; /* 5px */
+ padding: 0.625rem; /* 10px */
+ border-radius: 0.1875rem; /* 3px */
+ word-wrap: break-word;
}
.changelog-item .commit-hash {
- font-family: monospace;
+ font-family: monospace;
}
-.changelog-item .related-packages{
- position: relative;
+.changelog-item .related-packages {
+ position: relative;
}
-.changelog-item .related-packages .table-wrapper{
- max-height: 200px;
- overflow: scroll;
- border: 1px solid #ddd;
- margin-top: 10px;
+.changelog-item .related-packages .table-wrapper {
+ max-height: 12.5rem;
+ overflow: scroll;
+ border: 1px solid #ddd;
+ margin-top: 0.625rem;
}
.changelog-item .related-packages table {
- width: 100%;
- border-collapse: collapse;
+ width: 100%;
+ border-collapse: collapse;
}
-
-.changelog-item table thead{
- position: sticky;
- top: -1px;
+.changelog-item table thead {
+ position: sticky;
+ top: -1px;
}
-.changelog-item th, .changelog-item td {
- border: 1px solid #ddd;
+.changelog-item th,
+.changelog-item td {
+ border: 1px solid #ddd;
}
-.changelog-item th, .changelog-item td {
- padding: 10px;
- text-align: left;
+.changelog-item th,
+.changelog-item td {
+ padding: 0.625rem; /* 10px */
+ text-align: left;
}
.changelog-item th {
- background-color: #f7f7f7;
+ background-color: #f7f7f7;
}
.changelog-item tbody tr:nth-child(odd) {
- background-color: #f9f9f9;
+ background-color: #f9f9f9;
}
.changelog-item tbody tr:hover {
- background-color: #f1f1f1;
+ background-color: #f1f1f1;
}
.copy-button {
- display: inline;
- position: absolute;
- margin-left: 5px;
- cursor: pointer;
- right: 0px;
+ display: inline;
+ position: absolute;
+ margin-left: 0.3125rem; /* 5px */
+ cursor: pointer;
+ right: 0;
}
.copy-button img {
- width: 20px;
- height: auto;
- margin-right: 5px;
+ width: 1.25rem; /* 20px */
+ height: auto;
+ margin-right: 0.3125rem; /* 5px */
}
footer {
- margin: 20px 0px;
+ margin: 1.25rem 0; /* 20px 0px */
}
footer .copyright {
- text-align: center;
- margin: 0 auto;
+ text-align: center;
+ margin: 0 auto;
}
.alert-info {
- background-color: #f0f9ff;
- border-left: 6px solid #00bceb;
- color: #333;
- padding: 10px;
- margin-bottom: 20px;
+ background-color: #f0f9ff;
+ border-left: 0.375rem solid var(--color-webex-blue); /* 6px */
+ color: #333;
+ padding: 0.625rem; /* 10px */
+ margin-bottom: 1.25rem; /* 20px */
}
.alert-info p.note {
- font-weight: bold;
- color: #00bceb;
+ font-weight: bold;
+ color: var(--color-webex-blue);
}
.alert-info p.warning {
- font-weight: bold;
- color: #ff7f0e;
- display: inline;
-}
\ No newline at end of file
+ font-weight: bold;
+ color: #ff7f0e;
+ display: inline;
+}
+
+/* ============================================
+ VERSION COMPARISON STYLES
+ ============================================ */
+
+/* Mode Toggle Buttons */
+.mode-toggle {
+ display: flex;
+ gap: 0.625rem; /* 10px */
+ margin-bottom: 1.25rem; /* 20px */
+}
+
+.mode-toggle button {
+ flex: 1;
+ padding: 0.75rem 1.25rem;
+ border: 2px solid var(--color-webex-blue);
+ background-color: white;
+ color: var(--color-webex-blue);
+ cursor: pointer;
+ transition: all 0.3s;
+ border-radius: 0.25rem; /* 4px */
+ font-weight: 500;
+}
+
+.mode-toggle button:hover {
+ background-color: #e6f7fb;
+}
+
+.mode-toggle button.active {
+ background-color: var(--color-webex-blue);
+ color: white;
+}
+
+/* Hide utility class */
+.hide {
+ display: none !important;
+}
+
+/* Comparison Form */
+#comparison-form {
+ margin-top: 1.25rem; /* 20px */
+}
+
+/* Comparison Results Table */
+#comparison-results .table-wrapper {
+ max-height: 31.25rem;
+ overflow-y: auto;
+ overflow-x: auto;
+ border: 1px solid #ddd;
+ border-radius: 0.25rem;
+ margin-top: 0.625rem;
+ position: relative;
+}
+
+.comparison-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: 0;
+ font-size: 0.875rem; /* 14px */
+}
+
+.comparison-table th,
+.comparison-table td {
+ padding: 0.75rem;
+ text-align: left;
+ border: 1px solid #ddd;
+}
+
+.comparison-table th {
+ background-color: var(--color-webex-blue);
+ color: white;
+ font-weight: bold;
+ position: sticky;
+ top: 0;
+ z-index: 10;
+}
+
+.comparison-table tbody tr {
+ transition: background-color 0.2s;
+}
+
+.comparison-table tbody tr:hover {
+ background-color: var(--color-background) !important;
+}
+
+/* Color Coding for Changes */
+.comparison-table tr.version-changed {
+ background-color: #fff3cd; /* Yellow - version changed */
+ border-left: 0.25rem solid #ffc107; /* 4px */
+}
+
+.comparison-table tr.only-in-a {
+ background-color: #f8d7da; /* Red - removed in B */
+ border-left: 0.25rem solid #dc3545; /* 4px */
+}
+
+.comparison-table tr.only-in-b {
+ background-color: #d4edda; /* Green - added in B */
+ border-left: 0.25rem solid #28a745; /* 4px */
+}
+
+.comparison-table tr.unchanged {
+ background-color: #ffffff; /* White - no change */
+}
+
+/* Comparison Summary */
+.comparison-summary {
+ background: linear-gradient(135deg, #e7f3ff 0%, #f0f9ff 100%);
+ padding: 1.5625rem; /* 25px */
+ border-radius: 0.5rem; /* 8px */
+ margin-bottom: 1.25rem; /* 20px */
+ border-left: 0.3125rem solid var(--color-webex-blue); /* 5px */
+}
+
+.comparison-summary h3 {
+ margin-top: 0;
+ margin-bottom: 0.9375rem; /* 15px */
+ color: var(--color-webex-blue);
+ font-size: 1.5em;
+}
+
+.summary-stats {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.9375rem; /* 15px */
+ margin-top: 0.9375rem; /* 15px */
+}
+
+.stat-item {
+ padding: 0.625rem 1.25rem;
+ background-color: white;
+ border-radius: 0.3125rem;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ font-size: 0.875rem;
+}
+
+.stat-item.changed {
+ border-left: 0.25rem solid #ffc107; /* 4px */
+}
+
+.stat-item.unchanged {
+ border-left: 0.25rem solid #6c757d; /* 4px */
+}
+
+.stat-item.added {
+ border-left: 0.25rem solid #28a745; /* 4px */
+}
+
+.stat-item.removed {
+ border-left: 0.25rem solid #dc3545; /* 4px */
+}
+
+.stat-item strong {
+ font-size: 1.2em;
+ color: #333;
+}
diff --git a/docs/changelog/assets/js/app.js b/docs/changelog/assets/js/app.js
index 184b291e1..6368c66d1 100644
--- a/docs/changelog/assets/js/app.js
+++ b/docs/changelog/assets/js/app.js
@@ -1,7 +1,22 @@
// Global variable to store the current changelog and version paths
let currentChangelog;
const versionPaths = {};
-const github_base_url = 'https://github.com/webex/widgets/';
+let comparisonListenersInitialized = false;
+const githubBaseUrl = 'https://github.com/webex/widgets/';
+import {
+ comparisonState,
+ extractPackagesFromVersion,
+ findLatestPackageVersion,
+ getEffectiveVersion,
+ getPackageVersion,
+ determinePackageStatus,
+ createPackageComparisonRow,
+ calculateComparisonStats,
+ buildPackagesList,
+ comparePackages,
+ fetchAndCompareVersions,
+ generatePackageComparisonData,
+} from './comparison-view.js';
// DOM elements
const versionSelectDropdown = document.getElementById('version-select');
@@ -17,23 +32,89 @@ const commitHashGroup = document.getElementById('commit-hash-group');
const searchForm = document.getElementById('search-form');
const searchButton = document.getElementById('search-button');
const searchResults = document.getElementById('search-results');
-searchResults.classList.add('hide');
+
+// DOM elements - Comparison Mode
+const comparisonResults = document.getElementById('comparison-results');
+const comparisonTemplateElement = document.getElementById('comparison-template');
+const comparisonForm = document.getElementById('comparison-form');
+const singleViewBtn = document.getElementById('single-view-btn');
+const comparisonViewBtn = document.getElementById('comparison-view-btn');
+const versionASelect = document.getElementById('version-a-select');
+const versionBSelect = document.getElementById('version-b-select');
+const comparisonPackageSelect = document.getElementById('comparison-package-select');
+const comparisonPackageRow = document.getElementById('comparison-package-row');
+const versionAPrereleaseSelect = document.getElementById('version-a-prerelease-select');
+const versionBPrereleaseSelect = document.getElementById('version-b-prerelease-select');
+const prereleaseRow = document.getElementById('comparison-prerelease-row');
+const compareButton = document.getElementById('compare-button');
+const clearComparisonButton = document.getElementById('clear-comparison-button');
+const copyComparisonLinkBtn = document.getElementById('copy-comparison-link');
+const comparisonHelper = document.getElementById('comparison-helper');
+
+// DOM elements - Shared
+const helperSection = document.getElementById('helper-section');
+const packageLevelSection = document.getElementById('package-level-comparison-section');
+
+// Centralized UI visibility: map of view state -> which elements are visible/hidden
+const uiViewElements = {
+ searchForm,
+ comparisonForm,
+ searchResults,
+ comparisonResults,
+ helperSection,
+ comparisonHelper,
+};
+
+const uiVisibilityStates = {
+ search: {
+ visible: ['searchForm', 'searchResults', 'helperSection'],
+ hidden: ['comparisonForm', 'comparisonResults', 'comparisonHelper'],
+ },
+ comparison: {
+ visible: ['comparisonForm', 'comparisonHelper'],
+ hidden: ['searchForm', 'searchResults', 'helperSection', 'comparisonResults'],
+ },
+};
+
+/**
+ * Updates visibility of main UI sections based on view state ('search' | 'comparison').
+ * Replaces scattered classList add/remove logic with a single source of truth.
+ * @param {'search' | 'comparison'} viewState - The desired view mode
+ */
+function updateUIVisibility(viewState) {
+ const stateConfig = uiVisibilityStates[viewState];
+ if (!stateConfig) {
+ console.warn(`Unknown view state: "${viewState}"`);
+ return;
+ }
+ stateConfig.hidden.forEach((key) => {
+ const el = uiViewElements[key];
+ if (el) el.classList.add('hide');
+ });
+ stateConfig.visible.forEach((key) => {
+ const el = uiViewElements[key];
+ if (el) el.classList.remove('hide');
+ });
+}
+
+// Initialize UI to search view on load (single source of truth for visibility)
+updateUIVisibility('search');
// Templates and Helpers - Handlebar
const changelogItemTemplate = document.getElementById('changelog-item-template');
var changelogUI = Handlebars.compile(changelogItemTemplate.innerHTML);
Handlebars.registerHelper('forIn', function (object) {
let returnArray = [];
- for (let prop in object) {
+ for (const prop in object) {
returnArray.push({key: prop, value: object[prop]});
}
return returnArray;
});
-Handlebars.registerHelper('json', function (context, package, version) {
+Handlebars.registerHelper('json', function (context, packageName, version) {
const copyElem = {
...context,
- [package]: version,
+ [packageName]: version,
};
return JSON.stringify(copyElem);
});
@@ -41,10 +122,10 @@ Handlebars.registerHelper('json', function (context, package, version) {
Handlebars.registerHelper('github_linking', function (string, type) {
switch (type) {
case 'hash':
- return `${string}`;
+ return `${string}`;
case 'message':
// if commit message has a pr number, replace that pr number with pr anchor link and send back the transformed commit message
- return string.replace(/#(\d+)/g, `#$1`);
+ return string.replace(/#(\d+)/g, `#$1`);
}
});
@@ -55,6 +136,16 @@ Handlebars.registerHelper('convertDate', function (timestamp) {
// Util Methods
const populateFormFieldsFromURL = async () => {
const queryParams = new URLSearchParams(window.location.search);
+
+ // Skip single-view URL handling if comparison parameters are present
+ if (
+ queryParams.has('compare') ||
+ queryParams.has('compareStableA') ||
+ (queryParams.has('versionA') && queryParams.has('versionB'))
+ ) {
+ return; // Comparison mode will handle these parameters
+ }
+
const searchParams = {
stable_version: queryParams.get('stable_version'),
package: queryParams.get('package'),
@@ -72,12 +163,10 @@ const populateFormFieldsFromURL = async () => {
});
}
- if (searchParams.package) {
- if (!packageNameInputDropdown.disabled) {
- packageNameInputDropdown.value = searchParams.package;
- packageNameInputDropdown.dispatchEvent(new Event('change'));
- hasAtleastOneParam = true;
- }
+ if (searchParams.package && !packageNameInputDropdown.disabled) {
+ packageNameInputDropdown.value = searchParams.package;
+ packageNameInputDropdown.dispatchEvent(new Event('change'));
+ hasAtleastOneParam = true;
}
if (searchParams.version) {
@@ -122,7 +211,6 @@ const populateVersions = async () => {
console.error('Error fetching version data:', error);
}
};
-
const fetchChangelog = async (versionPath) => {
try {
const response = await fetch(versionPath);
@@ -133,27 +221,39 @@ const fetchChangelog = async (versionPath) => {
};
const populatePackageNames = (changelog) => {
- let specialPackages = ['webex', '@webex/calling'];
- let filteredPackages = Object.keys(changelog).filter((pkg) => !specialPackages.includes(pkg));
+ let specialPackages = ['@webex/widgets', '@webex/cc-widgets'];
+
+ // Get all packages that actually exist in this version's changelog
+ let allPackages = Object.keys(changelog);
+
+ // Filter special packages that ACTUALLY EXIST in this version
+ let existingSpecialPackages = specialPackages.filter((pkg) => allPackages.includes(pkg));
+
+ // Get remaining packages (excluding special ones)
+ let otherPackages = allPackages.filter((pkg) => !specialPackages.includes(pkg));
// Sort the remaining packages alphabetically
- filteredPackages.sort();
+ otherPackages.sort();
- // Add 'webex' and '@webex/calling' back to the beginning of the array
- // let sortedPackages = ['separator', ...specialPackages, 'separator', ...filteredPackages];
- let optionsHtml = ''; // Placeholder option
+ // Build the sorted list - only add separator if special packages exist
+ let sortedPackages;
+ if (existingSpecialPackages.length > 0) {
+ sortedPackages = ['separator', ...existingSpecialPackages, 'separator', ...otherPackages];
+ } else {
+ // No special packages exist, just show others
+ sortedPackages = otherPackages;
+ }
- // sortedPackages.forEach((packageName) => {
- filteredPackages.forEach((packageName) => {
+ let optionsHtml = '';
+
+ sortedPackages.forEach((packageName) => {
if (packageName === 'separator') {
optionsHtml += ``;
return;
}
optionsHtml += ``;
});
-
- // packageNameInputDropdown.value = 'webex';
- packageNameInputDropdown.innerHTML = optionsHtml; // Set all options at once
+ packageNameInputDropdown.innerHTML = optionsHtml;
};
const doStableVersionChange = async ({stable_version}) => {
@@ -163,6 +263,7 @@ const doStableVersionChange = async ({stable_version}) => {
// Fetch the changelog and populate package names
await fetchChangelog(versionPaths[stable_version]);
populatePackageNames(currentChangelog);
+
updateFormState();
if (versionInput.value.trim() !== '') {
validateVersionInput({version: versionInput.value});
@@ -203,6 +304,7 @@ const updateFormState = (formParams) => {
commitHash: commitHashInput.value,
};
}
+
const disable = {
package: false,
version: false,
@@ -222,13 +324,14 @@ const updateFormState = (formParams) => {
disable.commitMessage = false;
disable.commitHash = false;
}
-
+ //If the package name is empty, disable the version input
if (formParams.package === null || formParams.package.trim() === '') {
disable.version = true;
} else {
disable.searchButton = false;
}
-
+ // If version filled → disable commit fields
+ // If commit fields filled → disable version input
if (formParams.version && formParams.version.trim() !== '') {
disable.version = false;
disable.commitMessage = true;
@@ -294,14 +397,14 @@ const updateFormState = (formParams) => {
}
}
};
-
-const doSearch_commit = (searchParams, drill_down) => {
- let resulting_versions = new Set(),
- resulting_commit_messages = new Set(),
- resulting_commit_hash = new Set(),
- search_results = [];
- for (let package in drill_down) {
- const thisPackage = drill_down[package];
+// Search changelog by commit message or hash.(A single commit can appear in multiple package versions.)
+const doSearch_commit = (searchParams, drillDown) => {
+ let resultingVersions = new Set(),
+ resultingCommitMessages = new Set(),
+ resultingCommitHash = new Set(),
+ searchResults = [];
+ for (let packageName in drillDown) {
+ const thisPackage = drillDown[packageName];
for (let version in thisPackage) {
const thisVersion = thisPackage[version];
let allHashes = new Set(),
@@ -309,15 +412,15 @@ const doSearch_commit = (searchParams, drill_down) => {
for (let hash in thisVersion.commits) {
const thisCommit = thisVersion.commits[hash];
if (discontinueSearch) {
- resulting_versions.add(`${package}-${version}`);
- resulting_commit_messages.add(thisCommit);
- resulting_commit_hash.add(...allHashes);
+ resultingVersions.add(`${packageName}-${version}`);
+ resultingCommitMessages.add(thisCommit);
+ allHashes.forEach((h) => resultingCommitHash.add(h));
} else {
allHashes.add(hash);
if (
- !resulting_versions.has(`${package}-${version}`) &&
- !resulting_commit_messages.has(thisCommit) &&
- !resulting_commit_hash.has(hash)
+ !resultingVersions.has(`${packageName}-${version}`) &&
+ !resultingCommitMessages.has(thisCommit) &&
+ !resultingCommitHash.has(hash)
) {
if (
(searchParams.commitMessage &&
@@ -326,13 +429,13 @@ const doSearch_commit = (searchParams, drill_down) => {
(searchParams.commitHash &&
(hash.includes(searchParams.commitHash) || searchParams.commitHash.startsWith(hash)))
) {
- resulting_versions.add(`${package}-${version}`);
- resulting_commit_messages.add(thisCommit);
- resulting_commit_hash.union(allHashes);
+ resultingVersions.add(`${packageName}-${version}`);
+ resultingCommitMessages.add(thisCommit);
+ allHashes.forEach((h) => resultingCommitHash.add(h));
allHashes = new Set();
discontinueSearch = true;
- search_results.push({
- package,
+ searchResults.push({
+ packageName,
version,
published_date: thisVersion.published_date,
commits: thisVersion.commits,
@@ -344,57 +447,64 @@ const doSearch_commit = (searchParams, drill_down) => {
}
}
}
- return search_results;
+ return searchResults;
};
const doSearch = (searchParams) => {
- const {package, version} = searchParams;
- let drill_down = {...currentChangelog},
+ const pkg = searchParams.package;
+ const version = searchParams.version;
+ let drillDown = {...currentChangelog},
shouldTransform = true,
- search_results = [];
-
- if (package !== null && package?.trim() !== '') {
- drill_down = {
- [package]: drill_down[package],
+ results = [];
+ // If package selected → filter to that package
+ if (pkg !== null && pkg?.trim() !== '') {
+ drillDown = {
+ [pkg]: drillDown[pkg],
};
}
+ // If version selected → filter to that version (only when package and version exist)
if (version !== null && version?.trim() !== '') {
- drill_down = drill_down[package][version]
- ? {
- [package]: {
- [version]: drill_down[package][version],
- },
- }
- : {};
+ if (pkg && drillDown[pkg] && drillDown[pkg][version]) {
+ drillDown = {
+ [pkg]: {
+ [version]: drillDown[pkg][version],
+ },
+ };
+ } else {
+ drillDown = {};
+ }
} else if (
(searchParams.commitMessage !== null && searchParams.commitMessage?.trim() !== '') ||
(searchParams.commitHash !== null && searchParams.commitHash?.trim() !== '')
) {
- search_results = doSearch_commit(searchParams, drill_down);
+ results = doSearch_commit(searchParams, drillDown);
shouldTransform = false;
}
if (shouldTransform) {
- Object.keys(drill_down).forEach((package) => {
- Object.keys(drill_down[package]).forEach((version) => {
- search_results.push({
- package,
- version,
- published_date: drill_down[package][version].published_date,
- commits: drill_down[package][version].commits,
- alongWith: drill_down[package][version].alongWith,
+ Object.keys(drillDown).forEach((pkg) => {
+ const versions = drillDown[pkg];
+ if (versions != null && typeof versions === 'object') {
+ Object.keys(versions).forEach((ver) => {
+ results.push({
+ package: pkg,
+ version: ver,
+ published_date: versions[ver].published_date,
+ commits: versions[ver].commits,
+ alongWith: versions[ver].alongWith,
+ });
});
- });
+ }
});
}
- // sort search results based on published date which will be in Unit timestamp
- search_results.sort((a, b) => b.published_date - a.published_date);
+ // sort search results based on published date (Unix timestamp)
+ results.sort((a, b) => b.published_date - a.published_date);
const searchResultsHtml = changelogUI({
data: {
- search_results,
+ search_results: results,
stable_version: searchParams.stable_version,
},
});
@@ -439,17 +549,129 @@ searchForm.addEventListener('submit', (event) => {
}
// Redirect to the same page with the query string
- window.history.pushState({}, 'Cisco Webex JS SDK', `${window.location.pathname}?${queryParams.toString()}`);
+ window.history.pushState({}, 'Cisco Webex Widgets', `${window.location.pathname}?${queryParams.toString()}`);
populateVersions();
});
const copyToClipboard = (copyButton) => {
- navigator.clipboard.writeText(JSON.stringify(JSON.parse(copyButton.dataset.alongWith), null, 4));
- const copyText = copyButton.querySelector('span');
- copyText.textContent = 'Copied!';
+ let textToCopy;
+ try {
+ textToCopy = JSON.stringify(JSON.parse(copyButton.dataset.alongWith), null, 4);
+ } catch (e) {
+ console.error('copyToClipboard: invalid data-along-with', e);
+ return;
+ }
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard
+ .writeText(textToCopy)
+ .then(() => showCopySuccess(copyButton))
+ .catch((err) => {
+ console.error('Clipboard API failed:', err);
+ fallbackCopyToClipboard(textToCopy, copyButton);
+ });
+ } else {
+ fallbackCopyToClipboard(textToCopy, copyButton);
+ }
+};
+
+/**
+ * Copy comparison link to clipboard
+ * Global function that can be called from HTML or JS
+ */
+const copyComparisonLink = () => {
+ const currentURL = window.location.href;
+
+ // Try modern clipboard API first
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard
+ .writeText(currentURL)
+ .then(() => {
+ showCopySuccess(copyComparisonLinkBtn);
+ })
+ .catch((err) => {
+ console.error('Clipboard API failed:', err);
+ fallbackCopyToClipboard(currentURL, copyComparisonLinkBtn);
+ });
+ } else {
+ fallbackCopyToClipboard(currentURL, copyComparisonLinkBtn);
+ }
+};
+window.copyToClipboard = copyToClipboard;
+window.copyComparisonLink = copyComparisonLink;
+/**
+ * Show success feedback on copy button
+ */
+const showCopySuccess = (button) => {
+ if (!button) return;
+
+ const originalText = button.innerHTML;
+ button.innerHTML = '✓ Link Copied!';
+ button.style.backgroundColor = 'var(--color-success)';
+ button.style.borderColor = 'var(--color-success)';
+
+ setTimeout(() => {
+ button.innerHTML = originalText;
+ button.style.backgroundColor = '';
+ button.style.borderColor = '';
+ }, 2000);
+};
+
+/**
+ * Fallback copy method for browsers without Clipboard API (Older browsers don't support navigator.clipboard)
+ */
+const fallbackCopyToClipboard = (text, button) => {
+ // Create temporary input element
+ const tempInput = document.createElement('input');
+ tempInput.style.position = 'fixed';
+ tempInput.style.opacity = '0';
+ tempInput.value = text;
+ document.body.appendChild(tempInput);
+
+ // Select and copy
+ tempInput.select();
+ tempInput.setSelectionRange(0, 99999); // For mobile devices
+
+ try {
+ const successful = document.execCommand('copy');
+ if (successful) {
+ showCopySuccess(button);
+ } else {
+ console.error('execCommand copy failed');
+ showCopyError(button);
+ }
+ } catch (err) {
+ console.error('Fallback copy failed:', err);
+ showCopyError(button);
+ }
+
+ // Remove temporary input
+ document.body.removeChild(tempInput);
+};
+
+/**
+ * Show error feedback
+ */
+const showCopyError = (button) => {
+ if (!button) {
+ alert('Could not copy link. Please copy manually from the address bar.');
+ return;
+ }
+
+ const originalText = button.innerHTML;
+ button.innerHTML = 'Copy Failed';
+ button.style.backgroundColor = 'var(--color-danger)';
+ button.style.borderColor = 'var(--color-danger)';
+
setTimeout(() => {
- copyText.textContent = 'Copy';
+ button.innerHTML = originalText;
+ button.style.backgroundColor = '';
+ button.style.borderColor = '';
}, 2000);
+
+ // Also show alert with instructions
+ setTimeout(() => {
+ alert('Could not copy link automatically.\n\nPlease copy manually from the address bar:\n' + window.location.href);
+ }, 100);
};
window.onhashchange = () => {
@@ -457,3 +679,816 @@ window.onhashchange = () => {
};
populateVersions();
+
+let comparisonMode = false;
+/* ============================================
+ UI HELPER FUNCTIONS
+ ============================================ */
+
+/**
+ * Show loading state for comparison
+ */
+const showComparisonLoading = () => {
+ if (!comparisonResults) return;
+ comparisonResults.innerHTML = '
Loading comparison...
';
+ comparisonResults.classList.remove('hide');
+};
+
+/**
+ * Show error state for comparison
+ * @param {Error} error - The error object
+ */
+const showComparisonError = (error) => {
+ if (!comparisonResults) return;
+
+ console.error('Error performing version comparison:', error);
+ console.error('Error stack:', error.stack);
+
+ comparisonResults.innerHTML = `
+ Error: Failed to compare versions. ${error.message}
+
Check browser console for details (F12)
+
`;
+};
+
+/* ============================================
+ DATA LAYER FUNCTIONS
+ ============================================ */
+
+/**
+ * UI LAYER: Handle version comparison UI updates
+ * @param {string} versionA - Base version
+ * @param {string} versionB - Target version
+ */
+const performVersionComparison = async (versionA, versionB) => {
+ // Show loading state
+ showComparisonLoading();
+
+ try {
+ // Fetch and compare data (pure data logic)
+ const result = await fetchAndCompareVersions(versionA, versionB, versionPaths);
+
+ // Display results (UI logic)
+ displayComparison(result.versionA, result.versionB, result.comparisonData);
+ } catch (error) {
+ // Handle error display (UI logic)
+ showComparisonError(error);
+ }
+};
+
+/**
+ * Display comparison results
+ * @param {string} versionA - Base version
+ * @param {string} versionB - Target version
+ * @param {Object} comparisonData - Comparison results
+ */
+const displayComparison = (versionA, versionB, comparisonData) => {
+ if (!comparisonResults) {
+ console.error('comparison-results element not found!');
+ return;
+ }
+
+ if (!comparisonTemplateElement) {
+ console.error('comparison-template element not found!');
+ return;
+ }
+
+ const comparisonTemplate = Handlebars.compile(comparisonTemplateElement.innerHTML);
+
+ const templateData = {
+ versionA,
+ versionB,
+ ...comparisonData,
+ };
+
+ console.log('Template data:', templateData);
+
+ try {
+ const html = comparisonTemplate(templateData);
+ console.log('Generated HTML length:', html.length);
+
+ comparisonResults.innerHTML = html;
+ comparisonResults.classList.remove('hide');
+
+ // Update URL with comparison parameters for permalinks
+ updateComparisonURL(versionA, versionB);
+
+ // Show the copy link button and helper text
+ if (copyComparisonLinkBtn) {
+ copyComparisonLinkBtn.classList.remove('hide');
+ console.log('Copy link button shown');
+ } else {
+ console.warn('Copy link button not found in DOM');
+ }
+ if (comparisonHelper) {
+ comparisonHelper.classList.remove('hide');
+ }
+
+ // Scroll to results smoothly
+ setTimeout(() => {
+ comparisonResults.scrollIntoView({behavior: 'smooth', block: 'start'});
+ }, 100);
+
+ console.log('Comparison displayed successfully');
+ } catch (error) {
+ console.error('Error rendering template:', error);
+ comparisonResults.innerHTML = `Error rendering comparison: ${error.message}
`;
+ }
+};
+
+/**
+ * Update URL with comparison parameters for sharing/bookmarking
+ * @param {string} versionA - Base version
+ * @param {string} versionB - Target version
+ */
+const updateComparisonURL = (versionA, versionB) => {
+ const url = new URL(window.location);
+
+ // Clear any single-view parameters
+ url.searchParams.delete('stable_version');
+ url.searchParams.delete('package');
+ url.searchParams.delete('version');
+ url.searchParams.delete('commitMessage');
+ url.searchParams.delete('commitHash');
+ // Clear enhanced (package-level) comparison params so full comparison link is not stale
+ url.searchParams.delete('compareStableA');
+ url.searchParams.delete('compareStableB');
+ url.searchParams.delete('comparePackage');
+ url.searchParams.delete('compareVersionA');
+ url.searchParams.delete('compareVersionB');
+
+
+ // Set comparison parameters
+ url.searchParams.set('compare', `${versionA}vs${versionB}`);
+
+ // Update URL without reloading the page
+ window.history.pushState({}, '', url);
+};
+
+/**
+ * Parse and handle comparison URL parameters
+ * Supports formats: ?compare=3.9.0vs3.10.0 or ?versionA=3.9.0&versionB=3.10.0
+ */
+const handleComparisonURLParams = async () => {
+ const urlParams = new URLSearchParams(window.location.search);
+
+ let versionA = null;
+ let versionB = null;
+
+ // Check for ?compare=AvB format
+ const compareParam = urlParams.get('compare');
+ if (compareParam && compareParam.includes('vs')) {
+ const versions = compareParam.split('vs');
+ versionA = versions[0]?.trim();
+ versionB = versions[1]?.trim();
+ }
+
+ // Also support ?versionA=X&versionB=Y format
+ if (!versionA) versionA = urlParams.get('versionA');
+ if (!versionB) versionB = urlParams.get('versionB');
+
+ // If comparison parameters are found, switch to comparison mode
+ if (versionA && versionB && versionA !== versionB) {
+ return {versionA, versionB, shouldCompare: true};
+ }
+
+ return {shouldCompare: false};
+};
+
+/**
+ * Switch to comparison mode programmatically
+ * @param {string} versionA - Base version (optional)
+ * @param {string} versionB - Target version (optional)
+ */
+const switchToComparisonMode = (versionA = null, versionB = null) => {
+ // Update mode
+ comparisonMode = true;
+
+ // Update button states
+ if (comparisonViewBtn && singleViewBtn) {
+ comparisonViewBtn.classList.add('active', 'btn-primary');
+ comparisonViewBtn.classList.remove('btn-default');
+ singleViewBtn.classList.remove('active', 'btn-primary');
+ singleViewBtn.classList.add('btn-default');
+ }
+
+ // Update form visibility (centralized view state)
+ updateUIVisibility('comparison');
+ // Hide package-level comparison section in version comparison mode
+ if (packageLevelSection) packageLevelSection.classList.add('hide');
+
+ // Populate version dropdowns
+ if (versionSelectDropdown && versionSelectDropdown.innerHTML) {
+ const options = versionSelectDropdown.innerHTML;
+ if (versionASelect) versionASelect.innerHTML = options;
+ if (versionBSelect) versionBSelect.innerHTML = options;
+ }
+
+ // Set selected versions if provided
+ if (versionA && versionASelect) versionASelect.value = versionA;
+ if (versionB && versionBSelect) versionBSelect.value = versionB;
+};
+
+/* ============================================
+ ENHANCED VERSION COMPARISON HELPERS
+ ============================================ */
+
+/**
+ * Get union of packages from both versions (all packages that exist in either version)
+ * @param {Object} changelogA - Changelog data for version A
+ * @param {Object} changelogB - Changelog data for version B
+ * @returns {Array} - Array of all package names (union)
+ */
+const getUnionPackages = (changelogA, changelogB) => {
+ const packagesA = new Set(Object.keys(changelogA));
+ const packagesB = new Set(Object.keys(changelogB));
+
+ // Create union of both package sets
+ const allPackages = new Set([...packagesA, ...packagesB]);
+
+ // Prioritize certain packages
+ const specialPackages = ['@webex/widgets', '@webex/cc-widgets'];
+ const filtered = [...allPackages].filter((pkg) => !specialPackages.includes(pkg));
+ filtered.sort();
+
+ return [...specialPackages.filter((pkg) => allPackages.has(pkg)), ...filtered];
+};
+/**
+ * UI LAYER: Compare specific package versions and render results
+ * @param {string} packageName - Package name
+ * @param {string} versionASpecific - Version A
+ * @param {string} versionBSpecific - Version B
+ * @param {Object} changelogA - Changelog A
+ * @param {Object} changelogB - Changelog B
+ */
+const compareAndRenderPackageVersions = (packageName, versionASpecific, versionBSpecific, changelogA, changelogB) => {
+ try {
+ // Generate comparison data (pure data logic from comparison-view.js)
+ const comparisonData = generatePackageComparisonData(
+ packageName,
+ versionASpecific,
+ versionBSpecific,
+ changelogA,
+ changelogB
+ );
+
+ console.log('comparisonData', comparisonData);
+
+ // Validate DOM elements
+ if (!comparisonResults) {
+ console.error('comparison-results element not found');
+ return;
+ }
+
+ if (!comparisonTemplateElement) {
+ console.error('comparison-template not found');
+ return;
+ }
+
+ // Render template
+ const template = Handlebars.compile(comparisonTemplateElement.innerHTML);
+ const html = template(comparisonData);
+
+ // Update DOM
+ comparisonResults.innerHTML = html;
+ comparisonResults.classList.remove('hide');
+
+ // Update URL for sharing
+ updateEnhancedComparisonURL(
+ versionASelect.value,
+ versionBSelect.value,
+ packageName,
+ comparisonData.versionA,
+ comparisonData.versionB
+ );
+
+ // Show copy link button and helper
+ if (copyComparisonLinkBtn) copyComparisonLinkBtn.classList.remove('hide');
+ if (comparisonHelper) comparisonHelper.classList.remove('hide');
+
+ // Scroll to results
+ setTimeout(() => {
+ comparisonResults.scrollIntoView({behavior: 'smooth', block: 'start'});
+ }, 100);
+ } catch (error) {
+ console.error('Error in package comparison:', error);
+
+ // Show error to user
+ if (error.message.includes('Could not find version data')) {
+ alert(error.message);
+ } else {
+ showComparisonError(error);
+ }
+ }
+};
+/**
+ * Populate the package dropdown with union of packages from both versions
+ * @param {Object} changelogA - Changelog for base version
+ * @param {Object} changelogB - Changelog for target version
+ */
+const populateUnionPackages = (changelogA, changelogB) => {
+ if (!comparisonPackageSelect || !comparisonPackageRow) return;
+
+ const allPackages = getUnionPackages(changelogA, changelogB);
+
+ if (allPackages.length === 0) {
+ comparisonPackageSelect.innerHTML = '';
+ comparisonPackageRow.style.display = 'none';
+ return;
+ }
+
+ let optionsHtml = '';
+ allPackages.forEach((pkg) => {
+ optionsHtml += ``;
+ });
+
+ comparisonPackageSelect.innerHTML = optionsHtml;
+ comparisonPackageRow.style.display = 'flex';
+};
+
+/**
+ * Populate pre-release versions for a selected package
+ * @param {string} packageName - Selected package name
+ * @param {Object} changelog - Changelog data
+ * @param {string} selectId - ID of the select element to populate
+ * @param {string} stableVersion - The stable version (e.g., 3.3.1)
+ */
+const populatePrereleaseVersions = (packageName, changelog, selectId, stableVersion) => {
+ const versionSelect =
+ selectId === 'version-a-prerelease-select' ? versionAPrereleaseSelect : versionBPrereleaseSelect;
+
+ if (!versionSelect || !packageName) {
+ if (versionSelect) {
+ versionSelect.innerHTML = '';
+ versionSelect.disabled = true;
+ }
+ return;
+ }
+
+ // Check if package exists in this changelog (it might not for union packages)
+ if (!changelog[packageName]) {
+ if (versionSelect) {
+ versionSelect.innerHTML = '';
+ versionSelect.disabled = true;
+ }
+ return;
+ }
+
+ // Get all versions for this package
+ const allVersions = Object.keys(changelog[packageName]);
+
+ // Filter for pre-release versions matching the stable version
+ // e.g., for stable version 3.3.1, get 3.3.1-next.1, 3.3.1-next.22, etc.
+ const prereleaseVersions = allVersions.filter((v) => v.startsWith(stableVersion + '-') && v !== stableVersion);
+
+ // Sort by version (newest first based on published date)
+ prereleaseVersions.sort((a, b) => {
+ const dateA = changelog[packageName][a]?.published_date || 0;
+ const dateB = changelog[packageName][b]?.published_date || 0;
+ return dateB - dateA;
+ });
+
+ let optionsHtml = '';
+
+ // Also add the stable version itself as an option
+ if (changelog[packageName][stableVersion]) {
+ const stableDate = changelog[packageName][stableVersion]?.published_date;
+ const dateStr = stableDate ? new Date(stableDate).toLocaleDateString() : '';
+ optionsHtml += ``;
+
+ if (prereleaseVersions.length > 0) {
+ optionsHtml += ``;
+ }
+ }
+
+ // Add pre-release versions
+ prereleaseVersions.forEach((version) => {
+ const date = changelog[packageName][version]?.published_date;
+ const dateStr = date ? new Date(date).toLocaleDateString() : '';
+ optionsHtml += ``;
+ });
+
+ versionSelect.innerHTML = optionsHtml;
+ versionSelect.disabled = false;
+};
+
+/* ============================================
+ MAIN DATA LAYER FUNCTION
+ ============================================ */
+
+/**
+ * Update URL with enhanced comparison parameters
+ */
+const updateEnhancedComparisonURL = (stableA, stableB, packageName, versionA, versionB) => {
+ const url = new URL(window.location);
+
+ // Clear old parameters
+ url.searchParams.delete('stable_version');
+ url.searchParams.delete('package');
+ url.searchParams.delete('version');
+ url.searchParams.delete('commitMessage');
+ url.searchParams.delete('commitHash');
+ url.searchParams.delete('compare');
+
+ // Set new comparison parameters
+ url.searchParams.set('compareStableA', stableA);
+ url.searchParams.set('compareStableB', stableB);
+ url.searchParams.set('comparePackage', packageName);
+ url.searchParams.set('compareVersionA', versionA);
+ url.searchParams.set('compareVersionB', versionB);
+
+ window.history.pushState({}, '', url);
+};
+
+/**
+ * Handle URL parameters for enhanced comparison
+ */
+const handleEnhancedComparisonURL = async () => {
+ const urlParams = new URLSearchParams(window.location.search);
+
+ const stableA = urlParams.get('compareStableA');
+ const stableB = urlParams.get('compareStableB');
+ const packageName = urlParams.get('comparePackage');
+ const versionA = urlParams.get('compareVersionA');
+ const versionB = urlParams.get('compareVersionB');
+
+ if (stableA && stableB && packageName && versionA && versionB) {
+ return {stableA, stableB, packageName, versionA, versionB, shouldCompare: true};
+ }
+
+ return {shouldCompare: false};
+};
+
+/**
+ * Populate version dropdowns for comparison mode
+ */
+const populateComparisonVersions = () => {
+ if (versionSelectDropdown && versionSelectDropdown.innerHTML) {
+ const options = versionSelectDropdown.innerHTML;
+ if (versionASelect) versionASelect.innerHTML = options;
+ if (versionBSelect) versionBSelect.innerHTML = options;
+ }
+};
+
+/**
+ * Reset comparison form selections
+ */
+const resetComparisonSelections = () => {
+ if (comparisonPackageSelect) comparisonPackageSelect.value = '';
+ if (versionAPrereleaseSelect) versionAPrereleaseSelect.value = '';
+ if (versionBPrereleaseSelect) versionBPrereleaseSelect.value = '';
+ if (comparisonPackageRow) comparisonPackageRow.style.display = 'none';
+ if (prereleaseRow) prereleaseRow.style.display = 'none';
+};
+
+/**
+ * Clear all comparison form inputs and state
+ */
+const clearComparisonForm = () => {
+ if (versionASelect) versionASelect.value = '';
+ if (versionBSelect) versionBSelect.value = '';
+ resetComparisonSelections();
+ if (comparisonResults) comparisonResults.classList.add('hide');
+
+ comparisonState.reset();
+
+ if (copyComparisonLinkBtn) copyComparisonLinkBtn.classList.add('hide');
+ if (comparisonHelper) comparisonHelper.classList.add('hide');
+ if (compareButton) compareButton.disabled = false;
+};
+
+/**
+ * Clear comparison URL parameters
+ */
+const clearComparisonURLParams = () => {
+ const url = new URL(window.location);
+ [
+ 'compare',
+ 'versionA',
+ 'versionB',
+ 'compareStableA',
+ 'compareStableB',
+ 'comparePackage',
+ 'compareVersionA',
+ 'compareVersionB',
+ ].forEach((param) => {
+ url.searchParams.delete(param);
+ });
+ window.history.pushState({}, '', url);
+};
+
+/**
+ * Check and update comparison button state based on form selections
+ */
+const updateCompareButtonState = () => {
+ if (!compareButton) return;
+
+ const selectedPackage = comparisonPackageSelect ? comparisonPackageSelect.value : null;
+ const versionASpecific = versionAPrereleaseSelect ? versionAPrereleaseSelect.value : null;
+ const versionBSpecific = versionBPrereleaseSelect ? versionBPrereleaseSelect.value : null;
+ const prereleaseRowVisible = prereleaseRow && prereleaseRow.style.display !== 'none';
+
+ if (selectedPackage) {
+ // Package selected - require at least one pre-release version
+ if (!prereleaseRowVisible || (!versionASpecific && !versionBSpecific)) {
+ compareButton.disabled = true;
+ } else {
+ compareButton.disabled = false;
+ }
+ } else {
+ // No package selected - enable for full version comparison
+ compareButton.disabled = false;
+ }
+};
+
+/**
+ * Update pre-release row labels with version numbers
+ */
+const updatePrereleaseLabels = () => {
+ if (!prereleaseRow) return;
+
+ const labelA = prereleaseRow.querySelector('label[for="version-a-prerelease-select"]');
+ const labelB = prereleaseRow.querySelector('label[for="version-b-prerelease-select"]');
+ if (labelA) labelA.textContent = `Pre-release Version for Base (${comparisonState.currentStableA}):`;
+ if (labelB) labelB.textContent = `Pre-release Version for Target (${comparisonState.currentStableB}):`;
+};
+
+/**
+ * Handle stable version changes - fetch changelogs and populate packages
+ */
+const handleStableVersionChange = async () => {
+ console.log('🟢 handleStableVersionChange FIRED');
+ const stableA = versionASelect.value;
+ const stableB = versionBSelect.value;
+
+ resetComparisonSelections();
+ updateCompareButtonState();
+
+ if (stableA && stableB) {
+ try {
+ const [changelogA, changelogB] = await Promise.all([
+ fetch(versionPaths[stableA]).then((res) => res.json()),
+ fetch(versionPaths[stableB]).then((res) => res.json()),
+ ]);
+
+ comparisonState.update(changelogA, changelogB, stableA, stableB);
+ populateUnionPackages(changelogA, changelogB);
+ updateCompareButtonState();
+ } catch (error) {
+ console.error('Error loading changelogs:', error);
+ alert('Error loading version data. Please try again.');
+ }
+ }
+};
+
+/**
+ * Handle package selection - populate pre-release versions
+ */
+const handlePackageChange = () => {
+ console.log('🟢 handlePackageChange FIRED');
+ const selectedPackage = comparisonPackageSelect.value;
+
+ if (versionAPrereleaseSelect) versionAPrereleaseSelect.value = '';
+ if (versionBPrereleaseSelect) versionBPrereleaseSelect.value = '';
+
+ if (selectedPackage && comparisonState.cachedChangelogA && comparisonState.cachedChangelogB) {
+ populatePrereleaseVersions(
+ selectedPackage,
+ comparisonState.cachedChangelogA,
+ 'version-a-prerelease-select',
+ comparisonState.currentStableA
+ );
+ populatePrereleaseVersions(
+ selectedPackage,
+ comparisonState.cachedChangelogB,
+ 'version-b-prerelease-select',
+ comparisonState.currentStableB
+ );
+
+ if (prereleaseRow) {
+ prereleaseRow.style.display = 'flex';
+ updatePrereleaseLabels();
+ }
+ } else {
+ if (prereleaseRow) prereleaseRow.style.display = 'none';
+ }
+
+ updateCompareButtonState();
+};
+
+/**
+ * Switch to single view mode
+ */
+const switchToSingleViewMode = () => {
+ console.log('🔵 Switching to SINGLE VIEW mode');
+ comparisonMode = false;
+
+ // Update button styles
+ singleViewBtn.classList.add('active', 'btn-primary');
+ singleViewBtn.classList.remove('btn-default');
+ comparisonViewBtn.classList.remove('active', 'btn-primary');
+ comparisonViewBtn.classList.add('btn-default');
+
+ // Toggle visibility (centralized view state)
+ updateUIVisibility('search');
+
+ clearComparisonURLParams();
+};
+
+/**
+ * Switch to comparison view mode
+ */
+const switchToComparisonViewMode = () => {
+ console.log('🔵 Switching to COMPARISON VIEW mode');
+ comparisonMode = true;
+
+ // Update button styles
+ comparisonViewBtn.classList.add('active', 'btn-primary');
+ comparisonViewBtn.classList.remove('btn-default');
+ singleViewBtn.classList.remove('active', 'btn-primary');
+ singleViewBtn.classList.add('btn-default');
+
+ // Toggle visibility (centralized view state)
+ updateUIVisibility('comparison');
+
+ populateComparisonVersions();
+};
+
+/**
+ * Validate comparison form inputs
+ */
+const validateComparisonInputs = (stableA, stableB, selectedPackage, versionASpecific, versionBSpecific) => {
+ if (!stableA || !stableB) {
+ alert('Please select both stable versions');
+ return false;
+ }
+
+ if (selectedPackage && !versionASpecific && !versionBSpecific) {
+ alert('Please select at least one pre-release version, or leave package empty for full version comparison');
+ return false;
+ }
+
+ return true;
+};
+
+/**
+ * Handle comparison form submission
+ */
+const handleComparisonSubmit = (event) => {
+ event.preventDefault();
+
+ const stableA = versionASelect.value;
+ const stableB = versionBSelect.value;
+ const selectedPackage = comparisonPackageSelect?.value;
+ const versionASpecific = versionAPrereleaseSelect?.value;
+ const versionBSpecific = versionBPrereleaseSelect?.value;
+
+ if (!validateComparisonInputs(stableA, stableB, selectedPackage, versionASpecific, versionBSpecific)) {
+ return;
+ }
+
+ if (selectedPackage && (versionASpecific || versionBSpecific)) {
+ // Package-level comparison
+ const finalVersionA = versionASpecific || stableA;
+ const finalVersionB = versionBSpecific || stableB;
+ console.log('Comparing:', finalVersionA, 'vs', finalVersionB);
+
+ compareAndRenderPackageVersions(
+ selectedPackage,
+ finalVersionA,
+ finalVersionB,
+ comparisonState.cachedChangelogA,
+ comparisonState.cachedChangelogB
+ );
+ } else {
+ // Full version comparison
+ performVersionComparison(stableA, stableB);
+ }
+
+ if (compareButton) compareButton.disabled = false;
+};
+
+/**
+ * Handle clear button click
+ */
+const handleClearClick = () => {
+ clearComparisonForm();
+ clearComparisonURLParams();
+};
+
+/**
+ * Setup event listeners for comparison mode
+ */
+const setupComparisonEventListeners = () => {
+ if (comparisonListenersInitialized) {
+ console.log('🔴 Comparison listeners already initialized,skipping......');
+ return;
+ }
+ console.log('🟢 Setting up comparison event listeners first time......');
+ comparisonListenersInitialized = true;
+ // Mode toggle buttons
+ if (singleViewBtn) singleViewBtn.addEventListener('click', switchToSingleViewMode);
+
+ if (comparisonViewBtn) comparisonViewBtn.addEventListener('click', switchToComparisonViewMode);
+
+ // Version and package selectors
+ if (versionASelect) versionASelect.addEventListener('change', handleStableVersionChange);
+ if (versionBSelect) versionBSelect.addEventListener('change', handleStableVersionChange);
+
+ if (comparisonPackageSelect) comparisonPackageSelect.addEventListener('change', handlePackageChange);
+
+ // Pre-release version selectors
+ if (versionAPrereleaseSelect) versionAPrereleaseSelect.addEventListener('change', updateCompareButtonState);
+
+ if (versionBPrereleaseSelect) versionBPrereleaseSelect.addEventListener('change', updateCompareButtonState);
+
+ // Form actions
+ if (comparisonForm) comparisonForm.addEventListener('submit', handleComparisonSubmit);
+ if (clearComparisonButton) clearComparisonButton.addEventListener('click', handleClearClick);
+ if (copyComparisonLinkBtn) copyComparisonLinkBtn.addEventListener('click', copyComparisonLink);
+
+ comparisonListenersInitialized = true;
+ console.log('comparison listeners are initialized sucessfully......');
+};
+
+/**
+ * Handle enhanced comparison URL parameters on page load
+ */
+const loadEnhancedComparisonFromURL = async (enhancedParams) => {
+ switchToComparisonMode();
+
+ await new Promise((resolve) => setTimeout(resolve, 300));
+
+ versionASelect.value = enhancedParams.stableA;
+ versionBSelect.value = enhancedParams.stableB;
+ await handleStableVersionChange();
+
+ await new Promise((resolve) => setTimeout(resolve, 300));
+
+ comparisonPackageSelect.value = enhancedParams.packageName;
+ handlePackageChange();
+
+ await new Promise((resolve) => setTimeout(resolve, 300));
+
+ versionAPrereleaseSelect.value = enhancedParams.versionA;
+ versionBPrereleaseSelect.value = enhancedParams.versionB;
+
+ compareAndRenderPackageVersions(
+ enhancedParams.packageName,
+ enhancedParams.versionA,
+ enhancedParams.versionB,
+ comparisonState.cachedChangelogA,
+ comparisonState.cachedChangelogB
+ );
+};
+
+/**
+ * Handle standard comparison URL parameters on page load
+ */
+const loadStandardComparisonFromURL = async (urlParams) => {
+ switchToComparisonMode(urlParams.versionA, urlParams.versionB);
+
+ await new Promise((resolve) => setTimeout(resolve, 300));
+
+ performVersionComparison(urlParams.versionA, urlParams.versionB);
+};
+
+/**
+ * Initialize comparison mode functionality (Refactored)
+ */
+const initializeComparisonMode = async () => {
+ // Setup all event listeners
+ setupComparisonEventListeners();
+
+ // Check for URL parameters on page load
+ const enhancedParams = await handleEnhancedComparisonURL();
+ if (enhancedParams.shouldCompare) {
+ await loadEnhancedComparisonFromURL(enhancedParams);
+ return;
+ }
+
+ // Check for standard comparison URL
+ const urlParams = await handleComparisonURLParams();
+ if (urlParams.shouldCompare) {
+ await loadStandardComparisonFromURL(urlParams);
+ }
+};
+
+/**
+ * Initialize application in correct order to prevent race conditions
+ * This ensures versionPaths is populated before URL parameters are checked
+ */
+const initializeApplication = async () => {
+ // Step 1: Load version paths first (critical for URL parameter handling!)
+ await populateVersions();
+
+ // Step 2: Then initialize comparison mode (which checks URL params)
+ await initializeComparisonMode();
+};
+
+// Wait for DOM to be ready, then initialize
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initializeApplication);
+} else {
+ // DOM is already ready
+ initializeApplication();
+}
\ No newline at end of file
diff --git a/docs/changelog/assets/js/comparison-view.js b/docs/changelog/assets/js/comparison-view.js
new file mode 100644
index 000000000..7352606aa
--- /dev/null
+++ b/docs/changelog/assets/js/comparison-view.js
@@ -0,0 +1,387 @@
+//Data Extraction and Processing
+const comparisonState = {
+ cachedChangelogA: null,
+ cachedChangelogB: null,
+ currentStableA: null,
+ currentStableB: null,
+
+ reset() {
+ this.cachedChangelogA = null;
+ this.cachedChangelogB = null;
+ this.currentStableA = null;
+ this.currentStableB = null;
+ },
+
+ update(changelogA, changelogB, stableA, stableB) {
+ this.cachedChangelogA = changelogA;
+ this.cachedChangelogB = changelogB;
+ this.currentStableA = stableA;
+ this.currentStableB = stableB;
+ },
+};
+const extractPackagesFromVersion = (changelog, specificVersions = null) => {
+ const packageMap = {};
+
+ for (const packageName of Object.keys(changelog)) {
+ const packageVersions = changelog[packageName];
+ console.log('packageVersions', packageVersions);
+
+ // Safety check: ensure packageVersions is an object
+ if (!packageVersions || typeof packageVersions !== 'object') continue;
+
+ const versionKeys = Object.keys(packageVersions);
+ console.log('versionKeys', versionKeys);
+
+ if (versionKeys.length === 0) continue;
+
+ let selectedVersion = null;
+
+ // Check if user specified a specific version for this package
+ if (specificVersions && specificVersions[packageName]) {
+ const requestedVersion = specificVersions[packageName];
+
+ if (packageVersions[requestedVersion]) {
+ selectedVersion = requestedVersion;
+ }
+ }
+
+ // If no specific version requested or not found, use earliest (first) version
+ if (!selectedVersion) {
+ let earliestVersion = versionKeys[0];
+ let earliestDate = packageVersions[earliestVersion]?.published_date || Infinity;
+
+ for (const version of versionKeys) {
+ const publishedDate = packageVersions[version]?.published_date || Infinity;
+ if (publishedDate < earliestDate) {
+ earliestDate = publishedDate;
+ earliestVersion = version;
+ }
+ }
+
+ selectedVersion = earliestVersion;
+ }
+
+ packageMap[packageName] = selectedVersion;
+ }
+
+ return packageMap;
+};
+const findLatestPackageVersion = (changelog, packageName) => {
+ if (!changelog[packageName]) return null;
+
+ const versions = Object.keys(changelog[packageName]);
+ if (versions.length === 0) return null;
+
+ // Find the latest version by published date
+ let latestVersion = versions[0];
+ let latestDate = changelog[packageName][versions[0]].published_date || 0;
+
+ versions.forEach((ver) => {
+ const publishedDate = changelog[packageName][ver].published_date || 0;
+ if (publishedDate > latestDate) {
+ latestDate = publishedDate;
+ latestVersion = ver;
+ }
+ });
+
+ return latestVersion;
+};
+const getEffectiveVersion = (changelog, packageName, requestedVersion) => {
+ // If requested version exists, use it
+ if (changelog[packageName]?.[requestedVersion]) {
+ return requestedVersion;
+ }
+
+ // Otherwise, fallback to latest version
+ return findLatestPackageVersion(changelog, packageName);
+};
+const getPackageVersion = (packageName, alongWithData, changelog) => {
+ // Priority 1: Check alongWith data
+ if (alongWithData[packageName]) {
+ return alongWithData[packageName];
+ }
+
+ // Priority 2: Find latest version in changelog
+ return findLatestPackageVersion(changelog, packageName);
+};
+//Comparison Logic
+const determinePackageStatus = (versionA, versionB, dataA, dataB) => {
+ if (!dataA && dataB) {
+ return {status: 'Added', changeClass: 'only-in-b'};
+ }
+
+ if (dataA && !dataB) {
+ return {status: 'Removed', changeClass: 'only-in-a'};
+ }
+
+ if (versionA !== versionB) {
+ return {status: 'Version Changed', changeClass: 'version-changed'};
+ }
+
+ return {status: 'Unchanged', changeClass: 'unchanged'};
+};
+const createPackageComparisonRow = (packageName, versionA, versionB, statusInfo) => {
+ return {
+ packageName,
+ versionA: versionA || 'N/A',
+ versionB: versionB || 'N/A',
+ status: statusInfo.status,
+ changeClass: statusInfo.changeClass,
+ };
+};
+const calculateComparisonStats = (packages) => {
+ const stats = {
+ changedCount: 0,
+ unchangedCount: 0,
+ onlyInACount: 0,
+ onlyInBCount: 0,
+ };
+
+ packages.forEach((pkg) => {
+ switch (pkg.status) {
+ case 'Version Changed':
+ stats.changedCount++;
+ break;
+ case 'Unchanged':
+ stats.unchangedCount++;
+ break;
+ case 'Removed':
+ stats.onlyInACount++;
+ break;
+ case 'Added':
+ stats.onlyInBCount++;
+ break;
+ }
+ });
+
+ return stats;
+};
+
+const buildPackagesList = (
+ mainPackage,
+ effectiveVersionA,
+ effectiveVersionB,
+ pkgDataA,
+ pkgDataB,
+ changelogA,
+ changelogB
+) => {
+ const packagesArray = [];
+
+ // Add main package row
+ const mainStatus = determinePackageStatus(effectiveVersionA, effectiveVersionB, pkgDataA, pkgDataB);
+ packagesArray.push(createPackageComparisonRow(mainPackage, effectiveVersionA, effectiveVersionB, mainStatus));
+
+ // Get alongWith data
+ const alongWithA = pkgDataA?.alongWith || {};
+ const alongWithB = pkgDataB?.alongWith || {};
+
+ // Get all packages from both changelogs
+ const allPackages = new Set([...Object.keys(changelogA), ...Object.keys(changelogB)]);
+
+ // Remove main package (already added)
+ allPackages.delete(mainPackage);
+
+ // Add comparison rows for all related packages
+ allPackages.forEach((pkg) => {
+ const pkgVerA = getPackageVersion(pkg, alongWithA, changelogA);
+ const pkgVerB = getPackageVersion(pkg, alongWithB, changelogB);
+
+ const statusInfo = determinePackageStatus(
+ pkgVerA,
+ pkgVerB,
+ pkgVerA ? {} : null, // Simplified - just check if version exists
+ pkgVerB ? {} : null
+ );
+
+ packagesArray.push(createPackageComparisonRow(pkg, pkgVerA, pkgVerB, statusInfo));
+ });
+
+ // Sort packages alphabetically
+ packagesArray.sort((a, b) => a.packageName.localeCompare(b.packageName));
+
+ return packagesArray;
+};
+const comparePackages = (packagesA, packagesB, changelogA, changelogB, stableVersionA, stableVersionB) => {
+ // Get ALL package names from both changelogs (entire changelog, not just specific versions)
+ const allPackageNames = new Set([
+ ...Object.keys(changelogA), //ALL packages in changelog A
+ ...Object.keys(changelogB), //ALL packages in changelog B
+ ]);
+
+ const packages = [];
+ let changedCount = 0;
+ let unchangedCount = 0;
+ let onlyInACount = 0;
+ let onlyInBCount = 0;
+
+ // Helper function to find stable version first, then highest pre-release version
+ const findStableVersion = (changelog, packageName, stableVersion) => {
+ if (!changelog[packageName]) return null;
+
+ const versions = Object.keys(changelog[packageName]);
+ if (versions.length === 0) return null;
+
+ // Escape dots in version string for regex (3.4.0 -> 3\.4\.0)
+ const escapedVersion = stableVersion.replace(/\./g, '\\.');
+
+ // Priority 1: Find exact stable version (e.g., "3.4.0" only, no suffixes)
+ const exactStablePattern = new RegExp(`^${escapedVersion}$`);
+ const exactStableVersion = versions.find((ver) => exactStablePattern.test(ver));
+
+ if (exactStableVersion) {
+ return exactStableVersion;
+ }
+
+ // Priority 2: Find oldest pre-release version (any tag: next, alpha, beta, rc, etc.)
+ // Pattern: 3.4.0-{tag}.{number} -> captures tag and number
+ const prereleasePattern = new RegExp(`^${escapedVersion}-([a-z]+)\\.(\\d+)$`, 'i');
+
+ const prereleaseVersions = versions
+ .filter((ver) => prereleasePattern.test(ver))
+ .sort((a, b) => {
+ const matchA = a.match(prereleasePattern);
+ const matchB = b.match(prereleasePattern);
+ if (!matchA || !matchB) return 0;
+ const numA = parseInt(matchA[2], 10);
+ const numB = parseInt(matchB[2], 10);
+ return numA - numB; // Sort ascending (lowest first)
+ });
+ //console.log('prereleaseVersions', prereleaseVersions);
+ //console.log('versions', versions);
+ // Return highest pre-release version, or fallback to first available
+ return prereleaseVersions[0] || versions[0];
+ };
+
+ allPackageNames.forEach((packageName) => {
+ // Use release version per stable train (exact stable or highest prerelease), not chronologically earliest
+ const versionA = findStableVersion(changelogA, packageName, stableVersionA);
+ const versionB = findStableVersion(changelogB, packageName, stableVersionB);
+
+ let status, changeClass; //Declare variables for status label and CSS class
+
+ if (versionA && versionB) {
+ //checks if package is in both changelogs
+ if (versionA === versionB) {
+ //if versionA is the same as versionB, then it is unchanged
+ status = 'Unchanged';
+ changeClass = 'unchanged';
+ unchangedCount++;
+ } else {
+ status = 'Version Changed';
+ changeClass = 'version-changed';
+ changedCount++;
+ }
+ } else if (versionA && !versionB) {
+ status = 'Removed';
+ changeClass = 'only-in-a';
+ onlyInACount++;
+ } else if (!versionA && versionB) {
+ status = 'Added';
+ changeClass = 'only-in-b';
+ onlyInBCount++;
+ }
+
+ packages.push({
+ packageName,
+ versionA: versionA || 'N/A',
+ versionB: versionB || 'N/A',
+ status,
+ changeClass,
+ });
+ });
+
+ // Sort packages alphabetically
+ packages.sort((a, b) => a.packageName.localeCompare(b.packageName));
+
+ return {
+ packages,
+ totalPackages: allPackageNames.size,
+ changedCount,
+ unchangedCount,
+ onlyInACount,
+ onlyInBCount,
+ };
+};
+
+//Data Fetching
+const fetchAndCompareVersions = async (versionA, versionB, versionPaths) => {
+ const [changelogA, changelogB] = await Promise.all([
+ fetch(versionPaths[versionA]).then((res) => {
+ if (!res.ok) throw new Error(`Failed to fetch ${versionA}`);
+ return res.json();
+ }),
+ fetch(versionPaths[versionB]).then((res) => {
+ if (!res.ok) throw new Error(`Failed to fetch ${versionB}`);
+ return res.json();
+ }),
+ ]);
+
+ // Extract packages from both versions
+ const packagesA = extractPackagesFromVersion(changelogA);
+ const packagesB = extractPackagesFromVersion(changelogB);
+
+ // Compare packages
+ const comparisonData = comparePackages(packagesA, packagesB, changelogA, changelogB, versionA, versionB);
+
+ return {
+ versionA,
+ versionB,
+ comparisonData,
+ };
+};
+const generatePackageComparisonData = (packageName, versionASpecific, versionBSpecific, changelogA, changelogB) => {
+ const effectiveVersionA = getEffectiveVersion(changelogA, packageName, versionASpecific);
+ const effectiveVersionB = getEffectiveVersion(changelogB, packageName, versionBSpecific);
+ console.log('effectiveVersionA', effectiveVersionA);
+ console.log('effectiveVersionB', effectiveVersionB);
+ // Get package data from changelogs
+ const pkgDataA = changelogA[packageName]?.[effectiveVersionA];
+ const pkgDataB = changelogB[packageName]?.[effectiveVersionB];
+ console.log('pkgDataA', pkgDataA);
+ console.log('pkgDataB', pkgDataB);
+
+ // Validate versions exist
+ if (!pkgDataA && !pkgDataB) {
+ throw new Error(`Could not find version data for ${packageName}`);
+ }
+
+ // Build packages list including main package and all related packages
+ const packagesArray = buildPackagesList(
+ packageName,
+ effectiveVersionA,
+ effectiveVersionB,
+ pkgDataA,
+ pkgDataB,
+ changelogA,
+ changelogB
+ );
+
+ // Calculate statistics
+ const stats = calculateComparisonStats(packagesArray);
+
+ // Return pure data object
+ return {
+ packageName,
+ versionA: effectiveVersionA,
+ versionB: effectiveVersionB,
+ packages: packagesArray,
+ totalPackages: packagesArray.length,
+ ...stats,
+ };
+};
+//Export All the functions
+export {
+ comparisonState,
+ extractPackagesFromVersion,
+ findLatestPackageVersion,
+ getEffectiveVersion,
+ getPackageVersion,
+ determinePackageStatus,
+ createPackageComparisonRow,
+ calculateComparisonStats,
+ buildPackagesList,
+ comparePackages,
+ fetchAndCompareVersions,
+ generatePackageComparisonData,
+};
diff --git a/docs/changelog/index.html b/docs/changelog/index.html
index b2e9b74dc..18a7eb548 100644
--- a/docs/changelog/index.html
+++ b/docs/changelog/index.html
@@ -82,9 +82,19 @@
+
+
+
+
+
+
+
+
+
@@ -131,7 +193,7 @@
Search Examples:
+
+
+
+
Version Comparison Examples:
+
+
How to use version comparison:
+
+ - Select two different stable versions to compare
+ - For full comparison: Click "Compare Versions" to see all package differences
+ - For package-level comparison: Select a specific package and pre-release versions
+ - View color-coded changes: ● Version Changed,
+ ● Added,
+ ● Removed
+ - Copy the comparison link to share with your team
+
+
+
+
^
@@ -230,6 +326,60 @@ Published: {{convertDate this.published_date}}
{{/each}}
-
+
+
+
+
+