From 981650bb72ebc5f7d1e753b21a4337236feefb0e Mon Sep 17 00:00:00 2001 From: hayden <1623906259@qq.com> Date: Sun, 28 Jun 2026 23:31:19 +0800 Subject: [PATCH] Improve project detail panel performance --- website/static/main.js | 90 +++++++++++++++++++++--- website/static/style.css | 148 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+), 9 deletions(-) diff --git a/website/static/main.js b/website/static/main.js index d5b337b341..0b79b52a96 100644 --- a/website/static/main.js +++ b/website/static/main.js @@ -14,6 +14,8 @@ const noResults = document.querySelector(".no-results"); const rows = document.querySelectorAll(".table tbody tr.row"); const tags = document.querySelectorAll(".tag"); const tbody = document.querySelector(".table tbody"); +let activeDetailRow = null; +let detailPanel = null; function initRevealSections() { const sections = document.querySelectorAll("[data-reveal]"); @@ -128,6 +130,77 @@ function collapseAll() { row.classList.remove("open"); row.setAttribute("aria-expanded", "false"); }); + closeDetailPanel(); +} + +function getDetailPanel() { + if (detailPanel) return detailPanel; + + detailPanel = document.createElement("aside"); + detailPanel.id = "project-detail-panel"; + detailPanel.className = "detail-panel"; + detailPanel.setAttribute("role", "complementary"); + detailPanel.setAttribute("aria-hidden", "true"); + detailPanel.setAttribute("aria-labelledby", "project-detail-panel-title"); + detailPanel.innerHTML = + '
' + + '
' + + '
' + + '
Project details
' + + '
' + + "
" + + '' + + "
" + + '
' + + "
"; + + detailPanel + .querySelector(".detail-panel-close") + .addEventListener("click", closeDetailPanel); + document.body.appendChild(detailPanel); + return detailPanel; +} + +function closeDetailPanel() { + const rowToFocus = activeDetailRow; + if (activeDetailRow) { + activeDetailRow.classList.remove("selected"); + activeDetailRow.setAttribute("aria-expanded", "false"); + activeDetailRow.removeAttribute("aria-controls"); + activeDetailRow = null; + } + if (!detailPanel) return; + detailPanel.classList.remove("visible"); + detailPanel.setAttribute("aria-hidden", "true"); + return rowToFocus; +} + +function openDetailPanel(row, options) { + const shouldFocus = options && options.focusPanel; + if (!row._expandRow) return; + if (activeDetailRow === row) { + const closedRow = closeDetailPanel(); + if (shouldFocus && closedRow) closedRow.focus(); + return; + } + + closeDetailPanel(); + + const panel = getDetailPanel(); + const body = panel.querySelector(".detail-panel-body"); + const title = panel.querySelector(".detail-panel-title"); + const name = row.querySelector(".col-name a"); + const content = row._expandRow.querySelector(".expand-content"); + title.textContent = name ? name.textContent.trim() : "Project details"; + body.innerHTML = content ? content.innerHTML : ""; + + row.classList.add("selected"); + row.setAttribute("aria-expanded", "true"); + row.setAttribute("aria-controls", panel.id); + activeDetailRow = row; + panel.classList.add("visible"); + panel.setAttribute("aria-hidden", "false"); + if (shouldFocus) panel.querySelector(".detail-panel-close").focus(); } function applyFilters() { @@ -247,6 +320,7 @@ function getSortValue(row, col) { function sortRows() { if (!tbody) return; + closeDetailPanel(); const arr = Array.prototype.slice.call(rows); const col = activeSort.col; @@ -313,14 +387,7 @@ if (tbody) { } if (!row) return; - const isOpen = row.classList.contains("open"); - if (isOpen) { - row.classList.remove("open"); - row.setAttribute("aria-expanded", "false"); - } else { - row.classList.add("open"); - row.setAttribute("aria-expanded", "true"); - } + openDetailPanel(row); }); // Keyboard: Enter or Space on focused .row toggles expand @@ -329,7 +396,7 @@ if (tbody) { const row = e.target.closest("tr.row"); if (!row) return; e.preventDefault(); - row.click(); + openDetailPanel(row, { focusPanel: true }); }); } @@ -401,6 +468,11 @@ if (searchInput) { }); document.addEventListener("keydown", function (e) { + if (e.key === "Escape" && detailPanel && activeDetailRow) { + const rowToFocus = closeDetailPanel(); + if (rowToFocus) rowToFocus.focus(); + return; + } if ( e.key === "/" && !["INPUT", "TEXTAREA", "SELECT"].includes( diff --git a/website/static/style.css b/website/static/style.css index 93056570aa..73f93aacd3 100644 --- a/website/static/style.css +++ b/website/static/style.css @@ -800,6 +800,10 @@ kbd { border-bottom-color: transparent; } +.row.selected td { + background: linear-gradient(180deg, var(--row-open-start), var(--row-open-end)); +} + .col-num { width: 3.5rem; color: var(--ink-muted); @@ -1036,6 +1040,119 @@ th[data-sort].sort-asc::after { vertical-align: bottom; } +.detail-panel { + position: fixed; + top: 4.75rem; + right: 0.75rem; + z-index: 40; + width: min(30rem, calc(100vw - 1.5rem)); + max-height: calc(100vh - 6rem); + overflow: hidden; + color: var(--ink-soft); + background: + linear-gradient(180deg, oklch(100% 0 0 / 0.9), oklch(97% 0.01 75 / 0.96)), + var(--bg-paper); + border: 1px solid var(--line-strong); + border-radius: 0.9rem; + box-shadow: + 0 1.7rem 3.8rem -2.4rem oklch(0% 0 0 / 0.55), + 0 0.35rem 1.2rem -0.9rem oklch(0% 0 0 / 0.28); + opacity: 0; + pointer-events: none; + transform: translateX(1rem) scale(0.985); + transition: + opacity 180ms ease, + transform 220ms cubic-bezier(0.22, 1, 0.36, 1); +} + +.detail-panel.visible { + opacity: 1; + pointer-events: auto; + transform: translateX(0) scale(1); +} + +.detail-panel-shell { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + max-height: inherit; +} + +.detail-panel-header { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 1rem; + align-items: start; + padding: 1.05rem 1.15rem 0.95rem; + border-bottom: 1px solid var(--line); + background: oklch(98% 0.01 75 / 0.72); +} + +.detail-panel-kicker { + margin-bottom: 0.32rem; + color: var(--ink-muted); + font-size: 0.68rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.detail-panel-body { + max-height: calc(100vh - 12rem); + padding: 1.05rem 1.15rem 1.15rem; + overflow: auto; +} + +.detail-panel-close { + width: 2rem; + height: 2rem; + border: 1px solid var(--line); + border-radius: 999px; + color: var(--ink-muted); + background: var(--bg-paper); + font: inherit; + font-size: 1.15rem; + line-height: 1; + cursor: pointer; + transition: + color 160ms ease, + border-color 160ms ease, + background-color 160ms ease, + transform 160ms ease; +} + +.detail-panel-close:hover { + color: var(--ink); + background: var(--accent-soft); + border-color: oklch(68% 0.08 58 / 0.5); +} + +.detail-panel-close:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.detail-panel-close:active { + transform: translateY(1px); +} + +.detail-panel-title { + margin: 0; + color: var(--ink); + font-size: var(--text-lg); + font-weight: 800; + line-height: 1.2; + overflow-wrap: break-word; +} + +.detail-panel .expand-content { + animation: none; +} + +.detail-panel .expand-meta { + padding-top: 0.8rem; + border-top: 1px solid var(--line); +} + .expand-sep { margin-inline: 0.25rem; color: var(--line-strong); @@ -1722,6 +1839,33 @@ th[data-sort].sort-asc::after { padding-right: 0.8rem; } + .detail-panel { + top: auto; + right: 0; + bottom: 0; + left: 0; + width: auto; + max-height: min(74vh, 34rem); + border-right: 0; + border-bottom: 0; + border-left: 0; + border-radius: 1rem 1rem 0 0; + transform: translateY(1.2rem); + } + + .detail-panel.visible { + transform: translateY(0); + } + + .detail-panel-header, + .detail-panel-body { + padding-inline: 1rem; + } + + .detail-panel-body { + max-height: calc(min(74vh, 34rem) - 5.5rem); + } + .col-stars { width: 5.4rem; } @@ -1736,6 +1880,10 @@ th[data-sort].sort-asc::after { scroll-behavior: auto; } + .detail-panel { + transition: none; + } + *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important;