update: table ui with sticky headers and improved tooltips
HDVinnie unit3d@protonmail.com
Fri, 25 Jul 2025 14:01:36 -0400
3 files changed,
251 insertions(+),
15 deletions(-)
M
assets/css/app.css
→
assets/css/app.css
@@ -379,16 +379,28 @@
.tracker-table__main th, .tracker-jackett__table th { background: var(--gradient-header); color: var(--text-primary); - padding: 1.25rem 1rem; + padding: 1rem 0.8rem; text-align: left; font-weight: 600; - font-size: 0.95rem; + font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 2px solid var(--accent-primary); + cursor: pointer; + transition: all 0.3s ease; + user-select: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + box-sizing: border-box; + border-right: 1px solid var(--border-tertiary); position: sticky; top: 0; z-index: 10; +} + +.tracker-table__main th:last-child, .tracker-jackett__table th:last-child { + border-right: none; } .tracker-table__sortable, .tracker-jackett__sortable {@@ -403,13 +415,13 @@ color: var(--accent-primary);
filter: brightness(1.5); } -.tracker-table__sortable i, .tracker-jackett__sortable i { - margin-left: 0.5rem; +.tracker-table__sortable .icon, .tracker-jackett__sortable .icon { + margin-left: 0.3rem; opacity: 0.6; transition: all 0.3s ease; } -.tracker-table__sortable:hover i, .tracker-jackett__sortable:hover i { +.tracker-table__sortable:hover .icon, .tracker-jackett__sortable:hover .icon { opacity: 1; color: var(--accent-primary); }@@ -609,6 +621,83 @@ .icon-sort {
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 512'%3E%3Cpath d='M41 288h238c21.4 0 32.1 25.9 17 41L177 448c-9.4 9.4-24.6 9.4-33.9 0L24 329c-15.1-15.1-4.4-41 17-41zm255-105L177 64c-9.4-9.4-24.6-9.4-33.9 0L24 183c-15.1 15.1-4.4 41 17 41h238c21.4 0 32.1-25.9 17-41z'/%3E%3C/svg%3E") no-repeat center; mask-size: contain; background-color: currentColor; +} + +/* Sticky Header */ +.tracker-table__sticky-header { + position: fixed; + top: 85px; /* Just below the navbar */ + z-index: 50; + background: var(--bg-secondary); + backdrop-filter: blur(15px); + border: 1px solid var(--border-primary); + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + overflow: hidden; +} + +.tracker-table__sticky-table { + width: 100%; + border-collapse: collapse; + background: transparent; + table-layout: fixed; /* Force column widths to be respected */ +} + +.tracker-table__sticky-table th { + background: var(--gradient-header); + color: var(--text-primary); + padding: 1rem 0.8rem; + text-align: left; + font-weight: 600; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 2px solid var(--accent-primary); + cursor: pointer; + transition: all 0.3s ease; + user-select: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + box-sizing: border-box; + /* Ensure consistent sizing */ + border-right: 1px solid var(--border-tertiary); +} + +.tracker-table__sticky-table th:last-child { + border-right: none; +} + +.tracker-table__sticky-table th:hover { + background: var(--hover-bg); + color: var(--accent-primary); + filter: brightness(1.5); +} + +.tracker-table__sticky-table .icon { + margin-left: 0.3rem; + opacity: 0.6; + transition: all 0.3s ease; +} + +.tracker-table__sticky-table th:hover .icon { + opacity: 1; + color: var(--accent-primary); +} + +/* Responsive adjustments for sticky header */ +@media (max-width: 768px) { + .tracker-table__sticky-header { + top: 140px; /* Account for taller mobile navbar */ + left: 1rem !important; + right: 1rem; + width: calc(100vw - 2rem) !important; + } + + .tracker-table__sticky-table th { + padding: 0.75rem 0.5rem; + font-size: 0.8rem; + } } /* Responsive Design */
M
assets/js/app.js
→
assets/js/app.js
@@ -76,12 +76,99 @@ trackers: [],
search: '', sortColumn: '', sortDirection: 'asc', + showStickyHeader: false, + stickyHeaderLeft: 0, + stickyHeaderWidth: 0, tooltip: { show: false, text: '', x: 0, y: 0 }, + init() { + this.loadTrackers(); + // Set up scroll listener for sticky header + this.setupStickyHeader(); + }, + setupStickyHeader() { + const handleScroll = () => { + const tableContainer = document.querySelector('.tracker-table__container'); + const originalHeader = document.querySelector('.tracker-table__main thead'); + const tableSection = document.querySelector('.tracker-table'); + + if (!tableContainer || !originalHeader || !tableSection) return; + + const containerRect = tableContainer.getBoundingClientRect(); + const headerRect = originalHeader.getBoundingClientRect(); + const tableSectionRect = tableSection.getBoundingClientRect(); + const navbar = document.querySelector('.tracker-navbar'); + const navbarHeight = navbar ? navbar.offsetHeight : 85; + + // Show sticky header when: + // 1. Original header is above viewport + // 2. We haven't scrolled past the entire table section + const headerOutOfView = headerRect.bottom < navbarHeight + 20; + const stillInTableSection = tableSectionRect.bottom > navbarHeight + 60; + const shouldShow = headerOutOfView && stillInTableSection; + + this.showStickyHeader = shouldShow; + + if (shouldShow) { + // Position sticky header to match the table container + this.stickyHeaderLeft = Math.max(0, containerRect.left); + this.stickyHeaderWidth = Math.min(containerRect.width, window.innerWidth - this.stickyHeaderLeft - 20); + + // Match column widths after a brief delay to ensure DOM is ready + this.$nextTick(() => { + this.matchColumnWidths(); + }); + } + }; + + // Throttle scroll events for better performance + let ticking = false; + const throttledScroll = () => { + if (!ticking) { + requestAnimationFrame(() => { + handleScroll(); + ticking = false; + }); + ticking = true; + } + }; + + window.addEventListener('scroll', throttledScroll); + window.addEventListener('resize', throttledScroll); + + // Initial call + handleScroll(); + }, + matchColumnWidths() { + // Get the original table header cells + const originalCells = document.querySelectorAll('.tracker-table__main thead th'); + const stickyCells = document.querySelectorAll('.tracker-table__sticky-table thead th'); + + if (originalCells.length !== stickyCells.length) return; + + // Copy widths from original to sticky header + originalCells.forEach((originalCell, index) => { + if (stickyCells[index]) { + const width = originalCell.getBoundingClientRect().width; + stickyCells[index].style.width = `${width}px`; + stickyCells[index].style.minWidth = `${width}px`; + stickyCells[index].style.maxWidth = `${width}px`; + } + }); + }, + async loadTrackers() { + try { + const res = await fetch('./trackers.json'); + const data = await res.json(); + this.trackers = data.trackers || []; + } catch (error) { + console.error('Error loading trackers:', error); + } + }, get filteredTrackers() { let filtered = this.trackers;@@ -135,7 +222,7 @@ showTooltip(event, text) {
const rect = event.target.getBoundingClientRect(); this.tooltip.text = text; this.tooltip.x = rect.left + rect.width / 2; - this.tooltip.y = rect.top - 10; + this.tooltip.y = rect.top - 70; this.tooltip.show = true; }, hideTooltip() {@@ -178,15 +265,6 @@ this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
} else { this.sortColumn = column; this.sortDirection = 'asc'; - } - }, - async init() { - try { - const res = await fetch('./trackers.json'); - const data = await res.json(); - this.trackers = data.trackers || []; - } catch (error) { - console.error('Error loading trackers:', error); } } }
M
index.html
→
index.html
@@ -81,6 +81,75 @@ :style="`left: ${tooltip.x}px; top: ${tooltip.y}px; transform: translateX(-50%)`"
x-text="tooltip.text"> </div> + <!-- Floating Sticky Header --> + <div x-show="showStickyHeader" + x-transition:enter="transition ease-out duration-200" + x-transition:enter-start="opacity-0 transform -translate-y-2" + x-transition:enter-end="opacity-100 transform translate-y-0" + x-transition:leave="transition ease-in duration-150" + x-transition:leave-start="opacity-100 transform translate-y-0" + x-transition:leave-end="opacity-0 transform -translate-y-2" + class="tracker-table__sticky-header" + :style="`left: ${stickyHeaderLeft}px; width: ${stickyHeaderWidth}px;`"> + <table class="tracker-table__sticky-table"> + <thead> + <tr> + <th @click="sortBy('Name')" class="tracker-table__sortable"> + Name <span class="icon icon-sort"></span> + </th> + <th @click="sortBy('Abbreviation')" class="tracker-table__sortable"> + Abbr <span class="icon icon-sort"></span> + </th> + <th @click="sortBy('Type')" class="tracker-table__sortable"> + Type <span class="icon icon-sort"></span> + </th> + <th @click="sortBy('Codebase')" class="tracker-table__sortable"> + Codebase <span class="icon icon-sort"></span> + </th> + <th @click="sortBy('Observatory Grade')" class="tracker-table__sortable"> + Moz Grade <span class="icon icon-sort"></span> + </th> + <th @click="sortBy('Users')" class="tracker-table__sortable"> + Users <span class="icon icon-sort"></span> + </th> + <th @click="sortBy('Torrents')" class="tracker-table__sortable"> + Torrents <span class="icon icon-sort"></span> + </th> + <th @click="sortBy('Peers')" class="tracker-table__sortable"> + Peers <span class="icon icon-sort"></span> + </th> + <th @click="sortBy('Ratio')" class="tracker-table__sortable"> + Ratio <span class="icon icon-sort"></span> + </th> + <th @click="sortBy('Ratio Diff')" class="tracker-table__sortable"> + Ratio Diff <span class="icon icon-sort"></span> + </th> + <th @click="sortBy('Freeleech')" class="tracker-table__sortable"> + Freeleech <span class="icon icon-sort"></span> + </th> + <th @click="sortBy('Points')" class="tracker-table__sortable"> + Points <span class="icon icon-sort"></span> + </th> + <th @click="sortBy('Hit & Run')" class="tracker-table__sortable"> + H&R <span class="icon icon-sort"></span> + </th> + <th @click="sortBy('Birthdate')" class="tracker-table__sortable"> + B-Day <span class="icon icon-sort"></span> + </th> + <th @click="sortBy('Join')" class="tracker-table__sortable"> + Join <span class="icon icon-sort"></span> + </th> + <th @click="sortBy('Join Diff')" class="tracker-table__sortable"> + Join Diff <span class="icon icon-sort"></span> + </th> + <th @click="sortBy('Updated')" class="tracker-table__sortable"> + Updated <span class="icon icon-sort"></span> + </th> + </tr> + </thead> + </table> + </div> + <div class="tracker-table__controls"> <input class="tracker-table__search" type="text" placeholder="Search trackers..." x-model="search"> <div class="tracker-table__info">