diff --git a/layouts/partials/headers.html b/layouts/partials/headers.html index 6587e2fd..9bcedbb2 100644 --- a/layouts/partials/headers.html +++ b/layouts/partials/headers.html @@ -35,6 +35,7 @@ + {{ ` diff --git a/static/css/custom.css b/static/css/custom.css index 9b5b449f..29ccffeb 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -534,6 +534,175 @@ h3.text-uppercase { padding: 12px 10px; } +/* Dense data tables */ +.data-table { + background: var(--surface); + border: 1px solid var(--line); + border-radius: var(--radius-md); + box-shadow: 0 8px 28px rgba(23, 38, 51, 0.08); + margin: 24px 0 36px; + overflow: hidden; +} + +.data-table__toolbar { + align-items: center; + background: #f8fafb; + border-bottom: 1px solid var(--line); + display: flex; + gap: 18px; + justify-content: space-between; + padding: 12px 16px; +} + +.data-table__search { + flex: 0 1 340px; + margin: 0; + position: relative; +} + +.data-table__search i { + color: #71838d; + left: 13px; + pointer-events: none; + position: absolute; + top: 50%; + transform: translateY(-50%); +} + +.data-table__input { + background: #fff; + border: 1px solid #cbd8de; + border-radius: var(--radius-sm); + color: var(--ink); + font-size: 14px; + height: 40px; + padding: 8px 13px 8px 38px; + width: 100%; +} + +.data-table__input:focus { + border-color: var(--primary-accent); + box-shadow: 0 0 0 3px var(--form-shadow); + outline: 0; +} + +.data-table__count { + color: var(--ink-muted); + flex: none; + font-size: 13px; + font-weight: 650; +} + +.data-table__scroll { + max-width: 100%; + overflow: auto; + overscroll-behavior: contain; + scrollbar-color: #aebfc6 #edf2f4; + scrollbar-width: thin; +} + +.data-table--large .data-table__scroll { + max-height: min(68vh, 720px); +} + +#content .data-table__table { + border-collapse: separate; + border-spacing: 0; + margin: 0; + min-width: 760px; + width: 100%; +} + +#content .data-table__table th, +#content .data-table__table td { + background-clip: padding-box; + border: 0; + border-bottom: 1px solid var(--line); + padding: 13px 16px; + vertical-align: middle; +} + +#content .data-table__table thead th { + background: #eaf5f7; + border-bottom: 2px solid var(--primary-accent); + color: #173b47; + font-size: 14px; + font-weight: 750; + position: sticky; + top: 0; + white-space: normal; + z-index: 2; +} + +#content .data-table__table tbody tr:nth-child(even) td { + background-color: #f8fafb; +} + +#content .data-table__table tbody tr:hover td { + background-color: #eaf7f9; +} + +#content .data-table__table th:first-child, +#content .data-table__table td:first-child { + box-shadow: 1px 0 0 var(--line); + left: 0; + min-width: 210px; + position: sticky; + text-align: left; + z-index: 1; +} + +#content .data-table__table thead th:first-child { + background: #deeff2; + z-index: 3; +} + +#content .data-table__table tbody td:not(:first-child) { + min-width: 118px; + text-align: center; +} + +#content .data-table__table tbody tr:last-child td { + border-bottom: 0; +} + +#content .data-table__table tr[hidden] { + display: none; +} + +.data-table__scroll:focus-visible { + box-shadow: inset 0 0 0 3px var(--form-shadow); + outline: 0; +} + +@media (max-width: 767px) { + .data-table { + border-radius: var(--radius-sm); + margin-left: -8px; + margin-right: -8px; + } + + .data-table__toolbar { + align-items: stretch; + flex-direction: column; + gap: 8px; + } + + .data-table__search { + flex-basis: auto; + } + + #content .data-table__table th, + #content .data-table__table td { + padding: 11px 13px; + } + + #content .data-table__table th:first-child, + #content .data-table__table td:first-child { + min-width: 180px; + } +} + #content blockquote { background: var(--surface-soft); border-left: 4px solid var(--primary-accent); diff --git a/static/js/table-enhancements.js b/static/js/table-enhancements.js new file mode 100644 index 00000000..b036efd9 --- /dev/null +++ b/static/js/table-enhancements.js @@ -0,0 +1,87 @@ +(function () { + "use strict"; + + function getTableLabel(table, index) { + var heading = table.previousElementSibling; + + while (heading && !/^H[2-6]$/.test(heading.tagName)) { + heading = heading.previousElementSibling; + } + + return heading ? heading.textContent.trim() : "Data table " + (index + 1); + } + + function enhanceTable(table, index) { + if (table.closest(".data-table") || table.matches(".table-plain")) { + return; + } + + var rows = Array.prototype.slice.call(table.tBodies).reduce(function (allRows, body) { + return allRows.concat(Array.prototype.slice.call(body.rows)); + }, []); + var label = getTableLabel(table, index); + var wrapper = document.createElement("div"); + var scroller = document.createElement("div"); + + wrapper.className = "data-table" + (rows.length >= 15 ? " data-table--large" : ""); + scroller.className = "data-table__scroll"; + scroller.tabIndex = 0; + scroller.setAttribute("role", "region"); + scroller.setAttribute("aria-label", label + ", scrollable table"); + + table.parentNode.insertBefore(wrapper, table); + wrapper.appendChild(scroller); + scroller.appendChild(table); + table.classList.add("data-table__table"); + + if (rows.length < 15) { + return; + } + + var toolbar = document.createElement("div"); + var searchGroup = document.createElement("label"); + var searchIcon = document.createElement("i"); + var search = document.createElement("input"); + var count = document.createElement("span"); + var updateCount = function (visible) { + count.textContent = visible + " of " + rows.length + " entries"; + }; + + toolbar.className = "data-table__toolbar"; + searchGroup.className = "data-table__search"; + searchIcon.className = "fas fa-search"; + searchIcon.setAttribute("aria-hidden", "true"); + search.type = "search"; + search.className = "data-table__input"; + search.placeholder = "Filter table…"; + search.setAttribute("aria-label", "Filter " + label); + count.className = "data-table__count"; + count.setAttribute("aria-live", "polite"); + + search.addEventListener("input", function () { + var query = search.value.trim().toLocaleLowerCase(); + var visible = 0; + + rows.forEach(function (row) { + var matches = !query || row.textContent.toLocaleLowerCase().indexOf(query) !== -1; + row.hidden = !matches; + if (matches) { + visible += 1; + } + }); + + updateCount(visible); + }); + + searchGroup.appendChild(searchIcon); + searchGroup.appendChild(search); + toolbar.appendChild(searchGroup); + toolbar.appendChild(count); + wrapper.insertBefore(toolbar, scroller); + updateCount(rows.length); + } + + document.addEventListener("DOMContentLoaded", function () { + Array.prototype.forEach.call(document.querySelectorAll("#content table"), enhanceTable); + }); +}());