diff --git a/dist/css/styles.css b/dist/css/styles.css
index e1dee11..afad7cf 100644
--- a/dist/css/styles.css
+++ b/dist/css/styles.css
@@ -160,10 +160,7 @@
contain: layout paint style
}
-.loading-spinner,
-.status-indicator svg,
-.table-check,
-.table-cross {
+.loading-spinner {
will-change: transform;
backface-visibility: hidden
}
@@ -318,6 +315,26 @@ a:focus-visible {
box-shadow: 0 1px 0 0 rgba(255, 255, 255, .1), 0 1px 3px 0 rgba(0, 0, 0, .02), 0 4px 8px 0 rgba(0, 0, 0, .04)
}
+@media (max-width:900px) {
+ .site-header {
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+ background: rgba(255, 255, 255, .97)
+ }
+
+ [data-theme="dark"] .site-header {
+ background: rgba(15, 23, 42, .97)
+ }
+
+ .site-header.is-scrolled {
+ box-shadow: 0 6px 14px rgba(15, 23, 42, .08)
+ }
+
+ [data-theme="dark"] .site-header.is-scrolled {
+ box-shadow: 0 6px 14px rgba(0, 0, 0, .4)
+ }
+}
+
[data-theme="dark"] .site-header {
background: rgba(15, 23, 42, 0)
}
@@ -1774,6 +1791,67 @@ tr:hover {
background: color-mix(in srgb, var(--bg-secondary) 80%, transparent)
}
+.status-cell,
+.support-status {
+ text-align: center;
+}
+
+.status-indicator {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 1.9rem;
+ height: 1.9rem;
+ border-radius: var(--radius-full);
+ border: 1.5px solid var(--border-secondary);
+ background: var(--bg-primary);
+ color: var(--text-secondary);
+ font-weight: var(--font-semibold);
+ letter-spacing: .02em;
+ transition: transform var(--transition-fast), background-color var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
+}
+
+.status-indicator::before {
+ content: attr(data-symbol);
+ line-height: 1;
+ font-size: .9rem;
+}
+
+.status-indicator[data-supported="true"] {
+ background: rgba(16, 185, 129, .18);
+ border-color: rgba(16, 185, 129, .45);
+ color: var(--success);
+}
+
+[data-theme="dark"] .status-indicator[data-supported="true"] {
+ background: rgba(16, 185, 129, .25);
+ border-color: rgba(16, 185, 129, .55);
+}
+
+.status-indicator[data-supported="false"] {
+ background: rgba(239, 68, 68, .12);
+ border-color: rgba(239, 68, 68, .3);
+ color: var(--error);
+}
+
+[data-theme="dark"] .status-indicator[data-supported="false"] {
+ background: rgba(239, 68, 68, .18);
+ border-color: rgba(239, 68, 68, .35);
+}
+
+.status-cell:hover .status-indicator,
+.support-status:hover .status-indicator {
+ transform: scale(1.05);
+}
+
+.empty-state-row {
+ text-align: center;
+ padding: var(--space-2xl) !important;
+ color: var(--text-secondary);
+ background: var(--bg-secondary);
+ font-style: italic;
+}
+
.pricing-table .service-name {
color: inherit
}
@@ -1803,20 +1881,6 @@ tr:hover {
font-family: 'Courier New', Courier, monospace
}
-.table-check,
-.table-cross {
- padding: .2em;
- border-radius: 50px
-}
-
-.table-check {
- background-color: rgba(90, 221, 90, .568)
-}
-
-.table-cross {
- background-color: #db747465
-}
-
.search-section {
margin-bottom: var(--space-4xl)
}
@@ -1924,6 +1988,20 @@ tr:hover {
opacity: 1
}
+@media (max-width:768px) {
+ .status-card,
+ .referral-card {
+ box-shadow: none;
+ transform: none !important;
+ transition: background var(--transition-fast), border-color var(--transition-fast)
+ }
+
+ .status-card::before,
+ .referral-card::before {
+ display: none
+ }
+}
+
.status-header {
display: flex;
align-items: center;
diff --git a/dist/js/app.js b/dist/js/app.js
index d13180c..60faeae 100644
--- a/dist/js/app.js
+++ b/dist/js/app.js
@@ -144,11 +144,13 @@ class TableManager {
currentSort: { column: null, direction: "asc" },
currentPage: 1,
services: [], // Store services array for indexed format
+ columns: [],
isIndexedFormat: false // Flag to track data format
};
this.handleSearch = Utils.debounce(this._performSearch.bind(this), 250);
this.handleSort = this._performSort.bind(this);
+ this.renderToken = null;
this._init();
}
@@ -220,6 +222,7 @@ class TableManager {
const firstHostKey = Object.keys(processedData)[0];
const columns = firstHostKey ? Object.keys(processedData[firstHostKey]) : [];
+ this.state.columns = columns;
const tableId = this.elements.container.id.replace("-container", "");
const fragment = document.createDocumentFragment();
@@ -236,7 +239,6 @@ class TableManager {
table.appendChild(thead);
const tbody = document.createElement("tbody");
- tbody.innerHTML = this._generateTableRows(this.state.filteredData, columns);
table.appendChild(tbody);
wrapper.appendChild(table);
@@ -246,6 +248,7 @@ class TableManager {
this.elements.container.appendChild(fragment);
this._attachTableEvents();
+ this._renderTableBody(this.state.filteredData, columns);
}
_generateTableHeader(columns) {
@@ -269,33 +272,105 @@ class TableManager {
`;
}
- _generateTableRows(data, columns) {
- const rows = [];
- for (const [host, hostData] of Object.entries(data)) {
- rows.push(`
-
- |
-
- ${host}
-
- |
- ${columns.map(column => {
- const isSupported = hostData[column] === "✅";
- return `
-
-
- ${isSupported ?
- '' :
- ''
- }
-
- |
- `;
- }).join("")}
-
- `);
+ _renderTableBody(data, columns = this.state.columns) {
+ const table = this.elements.container.querySelector("table");
+ const tbody = table?.querySelector("tbody");
+ if (!tbody) return;
+
+ const effectiveColumns = Array.isArray(columns) && columns.length ? columns : this.state.columns;
+ const entries = Object.entries(data);
+ const token = Symbol("render");
+ this.renderToken = token;
+
+ tbody.textContent = "";
+
+ if (!entries.length) {
+ const emptyRow = document.createElement("tr");
+ const emptyCell = document.createElement("td");
+ emptyCell.colSpan = (effectiveColumns?.length || 0) + 1;
+ emptyCell.className = "empty-state-row";
+ emptyCell.textContent = "No services match your current filters.";
+ emptyRow.appendChild(emptyCell);
+ tbody.appendChild(emptyRow);
+ return;
}
- return rows.join("");
+
+ let index = 0;
+ const baseChunk = Math.max(20, 60 - Math.max(0, effectiveColumns.length - 4) * 5);
+ const isCompact = typeof window !== "undefined" && typeof window.matchMedia === "function"
+ ? window.matchMedia("(max-width: 640px)").matches
+ : false;
+ const chunkSize = isCompact ? Math.max(10, Math.floor(baseChunk / 1.5)) : baseChunk;
+ const schedule = (cb) => {
+ if (typeof requestIdleCallback === "function") {
+ requestIdleCallback(cb, { timeout: 160 });
+ } else {
+ requestAnimationFrame(cb);
+ }
+ };
+
+ const renderChunk = () => {
+ if (this.renderToken !== token) return;
+
+ const fragment = document.createDocumentFragment();
+ const limit = Math.min(entries.length, index + chunkSize);
+ for (; index < limit; index++) {
+ const [host, hostData] = entries[index];
+ fragment.appendChild(this._createTableRow(host, hostData, effectiveColumns));
+ }
+ tbody.appendChild(fragment);
+
+ if (index < entries.length) {
+ schedule(renderChunk);
+ }
+ };
+
+ schedule(renderChunk);
+ }
+
+ _createTableRow(host, hostData, columns) {
+ const row = document.createElement("tr");
+ row.dataset.host = host.toLowerCase();
+ row.setAttribute("role", "row");
+
+ const serviceCell = document.createElement("td");
+ serviceCell.className = "service-cell";
+ serviceCell.setAttribute("role", "gridcell");
+
+ const serviceInfo = document.createElement("div");
+ serviceInfo.className = "service-info";
+
+ const serviceName = document.createElement("span");
+ serviceName.className = "service-name";
+ serviceName.textContent = host;
+
+ serviceInfo.appendChild(serviceName);
+ serviceCell.appendChild(serviceInfo);
+ row.appendChild(serviceCell);
+
+ columns.forEach((column) => {
+ const statusCell = document.createElement("td");
+ statusCell.className = "status-cell";
+ statusCell.dataset.status = hostData[column] || "";
+ const isSupported = hostData[column] === "✅";
+ statusCell.dataset.supported = isSupported ? "true" : "false";
+ statusCell.setAttribute("role", "gridcell");
+ const indicator = document.createElement("span");
+ indicator.className = "status-indicator";
+ indicator.dataset.supported = isSupported ? "true" : "false";
+ indicator.dataset.symbol = isSupported ? "✓" : "✕";
+ indicator.setAttribute("aria-hidden", "true");
+
+ const srText = document.createElement("span");
+ srText.className = "visually-hidden";
+ srText.textContent = isSupported ? "Supported" : "Not supported";
+
+ statusCell.appendChild(indicator);
+ statusCell.appendChild(srText);
+ row.appendChild(statusCell);
+ });
+
+ return row;
}
_attachTableEvents() {
@@ -423,15 +498,7 @@ class TableManager {
}
_updateTableContent() {
- const tbody = this.elements.container.querySelector("tbody");
- if (!tbody) return;
-
- const firstHostKey = Object.keys(this.state.currentData)[0];
- const columns = firstHostKey ? Object.keys(this.state.currentData[firstHostKey]) : [];
-
- requestAnimationFrame(() => {
- tbody.innerHTML = this._generateTableRows(this.state.filteredData, columns);
- });
+ this._renderTableBody(this.state.filteredData, this.state.columns);
}
}
@@ -605,20 +672,27 @@ class ComparisonManager {
_generateComparisonRows(service1, service2) {
const rows = [];
- const checkIcon = '';
- const crossIcon = '';
-
- // Get all hosts (keys from the original data structure)
const hosts = this.isIndexedFormat ? Object.keys(this.data.supported) : Object.keys(this.data);
-
+ const getStatusCell = (supported) => {
+ const supportedClass = supported ? "supported" : "not-supported";
+ const supportedFlag = supported ? "true" : "false";
+ const symbol = supported ? "✓" : "✕";
+ return `
+
+
+ ${supported ? "Supported" : "Not supported"}
+ |
+ `;
+ };
+
for (const host of hosts) {
const hostData = this._getHostData(host);
const supported1 = hostData[service1] === "✅";
const supported2 = hostData[service2] === "✅";
-
+
let statusClass = "neither-supported";
let statusText = "Neither";
-
+
if (supported1 && supported2) {
statusClass = "both-supported";
statusText = "Both";
@@ -629,25 +703,17 @@ class ComparisonManager {
statusClass = "service2-only";
statusText = `${service2} only`;
}
-
+
rows.push(`
| ${host} |
-
-
- ${supported1 ? checkIcon : crossIcon}
-
- |
-
-
- ${supported2 ? checkIcon : crossIcon}
-
- |
+ ${getStatusCell(supported1)}
+ ${getStatusCell(supported2)}
${statusText} |
`);
}
-
+
return rows.join("");
}