diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd6c3bc..b6b17f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,4 +3,5 @@
### 🚀 Features
- Added changelog
+- Added Themes
## [0.1.0] - 2026-09-23
diff --git a/src/main.js b/src/main.js
index bce5afe..979b8a2 100644
--- a/src/main.js
+++ b/src/main.js
@@ -19,7 +19,7 @@ var theme = (() => {
}
try {
localStorage.setItem(STORAGE_KEY, name);
- } catch (e) {}
+ } catch (e) { }
}
function init() {
@@ -28,7 +28,7 @@ var theme = (() => {
var picker = document.getElementById('theme-picker');
picker.value = saved;
- picker.addEventListener('change', function() {
+ picker.addEventListener('change', function () {
set(picker.value);
});
}
@@ -39,12 +39,18 @@ var theme = (() => {
return { init };
})();
const table = (() => {
+ var dirty = false;
+ var flushTimer = null;
+ var ipIndex = {};
let data = [];
let filtered = [];
let sortKey = 'ip';
let sortDir = 'asc';
let filterText = '';
let checkedIps = new Set();
+ var ROW_HEIGHT = 32;
+ var BUFFER_ROWS = 20;
+ var scrollContainer = null;
function ipToNum(ip) {
const parts = String(ip).split('.');
@@ -193,20 +199,61 @@ const table = (() => {
}
function render() {
- const tbody = document.getElementById('results-body');
- const count = document.getElementById('host-count');
+ var tbody = document.getElementById('results-body');
+ var count = document.getElementById('host-count');
if (!tbody) return;
+
expandedIp = null;
- const rows = filtered.map((host) => {
- const ip = host.ip;
- const status = host.status || 'Unknown';
- const cls = 'status-' + status.toLowerCase();
- const rtt = host.ping ? host.ping.rtt_ms.toFixed(1) : '-';
- const hostname = host.hostname || '-';
+ if (!scrollContainer) {
+ scrollContainer = document.getElementById('results-table-container');
+ scrollContainer.addEventListener('scroll', onScroll);
+ }
- const isChecked = checkedIps.has(ip) ? 'checked' : '';
- return '
'
+ renderVisible();
+
+ if (count) {
+ var up = data.filter(function (h) { return h.status === 'Up'; }).length;
+ count.textContent = up + ' up / ' + data.length + ' total';
+ }
+
+ if (columns && columns.apply) columns.apply();
+ }
+
+ function renderVisible() {
+ var tbody = document.getElementById('results-body');
+ if (!tbody || !scrollContainer) return;
+
+ var totalRows = filtered.length;
+ var viewHeight = scrollContainer.clientHeight;
+ var scrollTop = scrollContainer.scrollTop;
+ var headerHeight = 36;
+
+ var startIdx = Math.max(0, Math.floor((scrollTop - headerHeight) / ROW_HEIGHT) - BUFFER_ROWS);
+ var visibleCount = Math.ceil(viewHeight / ROW_HEIGHT) + (BUFFER_ROWS * 2);
+ var endIdx = Math.min(totalRows, startIdx + visibleCount);
+
+ var topSpace = startIdx * ROW_HEIGHT;
+ var bottomSpace = (totalRows - endIdx) * ROW_HEIGHT;
+
+ var colCount = document.querySelectorAll('#results-table th').length;
+
+ var html = '';
+
+ if (topSpace > 0) {
+ html += '
|
';
+ }
+
+ for (var i = startIdx; i < endIdx; i++) {
+ var host = filtered[i];
+ var ip = host.ip;
+ var status = host.status || 'Unknown';
+ var cls = 'status-' + status.toLowerCase();
+ var rtt = host.ping ? host.ping.rtt_ms.toFixed(1) : '-';
+ var hostname = host.hostname || '-';
+ var isChecked = checkedIps.has(ip) ? 'checked' : '';
+
+ html += ''
+ ' | '
+ '' + ip + ' | '
+ '' + status + ' | '
@@ -214,24 +261,43 @@ const table = (() => {
+ '' + hostname + ' | '
+ '' + formatPorts(host) + ' | '
+ '
';
- });
-
- tbody.innerHTML = rows.join('');
-
- if (count) {
- const up = data.filter((h) => h.status === 'Up').length;
- count.textContent = up + ' up / ' + data.length + ' total';
}
- if (columns && columns.apply) columns.apply();
+
+ if (bottomSpace > 0) {
+ html += ' |
';
+ }
+
+ tbody.innerHTML = html;
+ }
+
+ var scrollTimeout = null;
+ function onScroll() {
+ if (scrollTimeout) return;
+ scrollTimeout = requestAnimationFrame(function () {
+ scrollTimeout = null;
+ renderVisible();
+ if (columns && columns.apply) columns.apply();
+ });
}
function upsert(host) {
- const idx = data.findIndex((h) => h.ip === host.ip);
+ var idx = data.findIndex(function (h) { return h.ip === host.ip; });
if (idx >= 0) {
data[idx] = host;
} else {
+ ipIndex[host.ip] = data.length;
data.push(host);
}
+ dirty = true;
+ if (!flushTimer) {
+ flushTimer = setTimeout(flush, 200);
+ }
+ }
+
+ function flush() {
+ flushTimer = null;
+ if (!dirty) return;
+ dirty = false;
refresh();
}
@@ -239,6 +305,7 @@ const table = (() => {
data = [];
filtered = [];
checkedIps.clear();
+ ipIndex = {};
document.getElementById('check-all').checked = false;
refresh();
}
@@ -1061,7 +1128,7 @@ var updater = (() => {
return { init };
})();
var changelog = (() => {
- var modal = function() { return document.getElementById('changelog-modal'); };
+ var modal = function () { return document.getElementById('changelog-modal'); };
var loaded = false;
async function open() {
@@ -1085,11 +1152,11 @@ var changelog = (() => {
document.getElementById('btn-changelog').addEventListener('click', open);
document.getElementById('changelog-close').addEventListener('click', close);
- modal().addEventListener('click', function(e) {
+ modal().addEventListener('click', function (e) {
if (e.target === modal()) close();
});
- document.addEventListener('keydown', function(e) {
+ document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && modal().classList.contains('visible')) close();
});
}
diff --git a/src/styles.css b/src/styles.css
index 85b6be5..5cc067f 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -409,6 +409,7 @@ body {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ height: 32px;
}
#results-table tbody tr {
@@ -428,6 +429,15 @@ body {
box-shadow: inset 3px 0 0 var(--cyan);
}
+.virtual-spacer {
+ border: none !important;
+}
+
+.virtual-spacer td {
+ padding: 0 !important;
+ border: none !important;
+}
+
/* ===== Status Colors ===== */
.status-up {
@@ -1322,6 +1332,7 @@ body {
.changelog-content a:hover {
text-decoration: underline;
}
+
.theme-picker {
padding: 1px 6px;
font-size: 11px;
@@ -1345,6 +1356,7 @@ body {
background: var(--bg-secondary);
color: var(--text-primary);
}
+
/* ===== Theme: Midnight ===== */
:root[data-theme="midnight"] {