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(""); }