Excel-style column filters, full-page Custom View, card field editor

Filters (web/excel-filter.js), modeled on Excel's AutoFilter:
- Every column header gets a dropdown: Sort A to Z / Z to A, Clear Filter
  From "<column>", Search, a checkbox list of the column's values with
  (Select All) and (Blanks), OK / Cancel. Values come from rows passing
  the other columns' filters; filters on several columns combine. Tags
  and software lists are split into individual items.
- The button shows a funnel when filtered and an arrow when sorted, and a
  "Clear filters (n)" button appears beside the group's copy buttons
- Replaces the old per-column filter row (which also removes the gap
  between header and filter rows); hooks the existing filter passes so
  global search, counts and worker re-renders keep working
- Menus are fixed-position on <body>, so sticky headers and neighbouring
  tables can't cover them; on phones they open as a bottom sheet
- Workbench stacks get the same menus (sorting done client-side)
- Custom View's filter icon opens the same menu

Layout:
- Sticky headers use separated borders so no hairline gap opens
- Phones/tablets: a sticky bar of column chips (with active counts and
  "Clear all") opens the filter menus
- Custom View fills the page like the other views on desktop; it still
  scrolls sideways when wide, with its header pinned under the group header
- Custom View on phones/tablets: a "Card fields" sheet to choose, order
  and remove the fields shown on each card

Demo: first visit puts 6 PCs (one per migration stage) on the Workbench.
This commit is contained in:
Johan Sandoval committed 2026-10-06 17:24:55 +00:00
1 parent d24be4a2d6
commit b216ce43f2
5 files changed
+788 -99

No files matched your search

+152 -71
View File
@@ -1,6 +1,8 @@
/*
* Layout additions for the dashboard, loaded after the page's own <style> block:
* - sticky status-group headers (all sizes) and sticky column headers (desktop)
* - sticky status-group headers and table headers
* - Custom View filling the page on desktop
* - Excel-style filter menus, filter chip bar and card-field editor (excel-filter.js)
* - card view for tables on phones and tablets
* - small-screen fixes for forms, docs and dialogs
* layout.js measures the sticky offsets and labels card cells.
@@ -10,28 +12,35 @@
* Sticky headers
*
* A status group's header ("Not Started (37)") sticks under the top nav while you scroll
* through that group, then hands over to the next group's header. Beneath it the table's
* header (column names + filters on desktop, the filter panel on phones) sticks as one
* block, so the rows inside it can never drift apart. layout.js sets --nav-h and, per
* table, --sticky-top (nav + group header).
* through that group, then hands over to the next group's header. On desktop the column
* header sticks beneath it. layout.js sets --nav-h and, per table, --sticky-top
* (nav + group header).
* ------------------------------------------------------------------------------------- */
.group-header { position: sticky; top: var(--nav-h, 60px); z-index: 30; }
/* The old filter row (text boxes and fixed lists) is replaced by Excel-style column menus */
table.xf-table thead tr.filter-row { display: none !important; }
.master-table:not(#table-custom) thead { position: sticky; top: var(--sticky-top, 112px); z-index: 20; }
.master-table:not(#table-custom) thead tr.filter-row th {
/* the original header tint, but opaque so rows don't show through */
background: linear-gradient(rgba(0,0,0,0.2), rgba(0,0,0,0.2)), var(--bg-card);
}
@media (min-width: 1101px) {
/* An overflowing wrapper would capture the sticky header; these tables never need to
scroll sideways at desktop widths (they're fixed-layout, 100% wide) */
#masterList .table-wrapper, #software .table-wrapper, .stack-box.table-wrapper { overflow: visible !important; }
/* Custom View scrolls inside its own box (it can be wider than the page), so its
group header stays put and its column header sticks to the top of that box */
#wrapper-custom .group-header { position: static; }
#scroll-custom thead th { position: sticky; top: 0; z-index: 20; }
/* Collapsed table borders stay behind when a header sticks, leaving a hairline gap
that rows show through; separated borders move with their cells */
.master-table { border-collapse: separate; border-spacing: 0; }
/* Custom View fills the page like the other views. It still scrolls sideways when its
columns are wider than the page; its header is pinned by layout.js (--pin-y) since a
sideways-scrolling box can't hold a sticky header */
#custom, #wrapper-custom { overflow: visible !important; }
#scroll-custom { max-height: none !important; overflow-x: auto; overflow-y: hidden; }
#table-custom thead th {
position: relative !important; top: auto !important; z-index: 20;
transform: translateY(var(--pin-y, 0px));
}
}
/* ---------------------------------------------------------------------------------------
@@ -57,9 +66,9 @@ body { padding-bottom: var(--dock-h, 40px) !important; }
* An 11-column table can't be read on a phone or an iPad in portrait, and sideways
* scrolling only ever shows a few columns. Below 1100px (where the nav also collapses)
* each row becomes a card: the device name as its title and every other column as a
* "label: value" line. mobile.js copies the column names onto the cells. Cards flow one
* per row on phones and two or more on tablets. The column filter row becomes a filter
* panel above the cards.
* "label: value" line. layout.js copies the column names onto the cells. Cards flow one
* per row on phones and two or more on tablets. A sticky bar of column chips above the
* cards opens the same Excel-style filter menus as the desktop headers.
* ------------------------------------------------------------------------------------- */
@media (max-width: 1100px) {
.table-wrapper { overflow-x: visible !important; }
@@ -73,39 +82,13 @@ body { padding-bottom: var(--dock-h, 40px) !important; }
table.master-table thead tr.card-label-row,
table.master-table thead tr.card-aux-row { display: none; }
/* Filters: a wrapping panel of labeled controls */
table.master-table thead tr.filter-row {
display: flex; flex-wrap: wrap; gap: 8px 10px; padding: 10px 12px;
/* opaque: the panel sticks over the cards while scrolling */
background: linear-gradient(rgba(0,0,0,0.15), rgba(0,0,0,0.15)), var(--bg-card);
border-bottom: 1px solid var(--border-color); box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
table.master-table thead tr.filter-row th {
display: block; flex: 1 1 150px; min-width: 0; width: auto !important;
padding: 0; border: 0; background: transparent;
}
table.master-table thead tr.filter-row th:empty,
table.master-table thead tr.filter-row th[data-label=""] { display: none; }
table.master-table thead tr.filter-row th::before {
content: attr(data-label); display: block; margin-bottom: 3px;
font-size: 0.78em; font-weight: 600; color: var(--text-muted);
}
/* Column names live on the cards; the header is replaced by the filter chip bar */
table.xf-table thead { display: none !important; }
/* Custom View: cards don't need the sideways-scrolling box, so it scrolls with the
page like the other tables and its header strip sticks under the group header */
#wrapper-custom, #scroll-custom { overflow: visible !important; max-height: none !important; }
#table-custom thead { position: sticky; top: var(--sticky-top, 112px); z-index: 20; }
/* Custom View keeps its header (sort / filter / remove buttons) as a strip of chips */
#table-custom thead tr.card-label-row {
display: flex; flex-wrap: wrap; gap: 6px; padding: 10px 12px;
background: var(--table-header-bg);
}
#table-custom thead tr.card-label-row th {
display: flex; align-items: center; gap: 4px; width: auto !important; min-width: 0 !important;
padding: 4px 8px; border: 1px solid rgba(255,255,255,0.2); border-radius: 999px;
position: static; white-space: nowrap;
}
/* Custom View scrolls with the page like the other views */
#custom, #wrapper-custom, #scroll-custom { overflow: visible !important; max-height: none !important; }
/* Desktop column tools don't apply to cards ("Card fields" replaces them) */
#custom button[onclick*="shrinkCustomColumns"], #custom [onclick*="toggleAddColMenu"] { display: none !important; }
/* Rows -> cards */
table.master-table tbody {
@@ -146,30 +129,8 @@ body { padding-bottom: var(--dock-h, 40px) !important; }
/* "No devices" style rows span the full card */
table.master-table tbody td[colspan] { display: block; text-align: center; border: 0; padding: 6px 0; }
/* Filters fold away behind a button (added by mobile.js) */
.card-filter-toggle {
display: inline-flex; align-items: center; gap: 6px; margin: 10px 12px 0;
background: transparent; color: var(--text-main); border: 1px solid var(--border-color);
border-radius: 6px; padding: 6px 12px; font: inherit; font-size: 0.85em; cursor: pointer;
}
.card-filter-toggle .count {
background: #3498db; color: #fff; border-radius: 999px; padding: 0 7px; font-size: 0.85em;
}
table.master-table:not(.show-filters) thead tr.filter-row { display: none; }
table.master-table.show-filters thead tr.filter-row { margin-top: 10px; }
}
@media (min-width: 1101px) {
.card-filter-toggle { display: none; }
}
@media (max-width: 600px) {
/* Compact filter panel on phones: three per row, shorter controls */
table.master-table thead tr.filter-row { gap: 6px 8px; padding: 8px 10px; }
table.master-table thead tr.filter-row th { flex: 1 1 96px; }
table.master-table thead tr.filter-row th::before { font-size: 0.7em; margin-bottom: 2px; }
table.master-table thead tr.filter-row input[type="text"],
table.master-table thead tr.filter-row .select-box { height: 32px; padding: 3px 6px; }
table.master-table thead tr.filter-row .select-box { font-size: 13px; }
}
@media (max-width: 420px) {
table.master-table tbody td { padding-left: 7.8em; }
table.master-table tbody td::before { width: 7.2em; }
@@ -238,3 +199,123 @@ body { padding-bottom: var(--dock-h, 40px) !important; }
.group-header { font-size: 0.98em; padding: 8px 10px; }
.modal-content { margin: 66px 6px 6px !important; max-height: calc(100dvh - 72px) !important; }
}
/* ---------------------------------------------------------------------------------------
* Excel-style filters (excel-filter.js)
* ------------------------------------------------------------------------------------- */
/* Header dropdown button: takes the old sort icon's spot at the right of the header, next
to the copy-column icon (sorting is in the menu, and the button shows the sort arrow) */
table.xf-table thead th > .sort-icon { display: none !important; }
table.xf-table thead th > .xf-btn { position: absolute; right: 22px; top: 50%; transform: translateY(-50%); margin: 0; }
table.xf-table thead th:has(> .xf-btn):not(.sortable-th) { position: relative; padding-right: 40px !important; }
.xf-btn {
display: inline-flex; align-items: center; justify-content: center; vertical-align: middle;
width: 17px; height: 17px; margin-left: 4px; padding: 0; cursor: pointer;
border: 1px solid rgba(255,255,255,0.35); border-radius: 4px;
background: rgba(255,255,255,0.08); color: inherit;
}
.xf-btn:hover { background: rgba(255,255,255,0.22); }
.xf-btn svg { width: 10px; height: 10px; }
.xf-btn.xf-on { background: #f1c40f; border-color: #f1c40f; color: #1f2937; }
.xf-btn.xf-sorted { background: rgba(255,255,255,0.28); border-color: rgba(255,255,255,0.6); }
.col-filter-btn.xf-on { color: #f1c40f !important; opacity: 1 !important; }
/* Dropdown menu (desktop) / bottom sheet (phones) */
.xf-menu {
position: fixed; z-index: 2600; width: 290px; display: flex; flex-direction: column;
padding: 6px 0; overflow: hidden; font-size: 14px; text-align: left;
background: var(--bg-card); color: var(--text-main);
border: 1px solid var(--border-color); border-radius: 8px; box-shadow: 0 12px 32px rgba(0,0,0,0.5);
}
.xf-head { display: none; }
.xf-item {
display: flex; align-items: center; gap: 10px; padding: 7px 14px; cursor: pointer;
background: none; border: 0; color: inherit; font: inherit; text-align: left;
}
.xf-item:hover:not(:disabled) { background: var(--hover-bg); }
.xf-item:disabled { opacity: 0.45; cursor: default; }
.xf-item svg { width: 14px; height: 14px; flex: none; }
.xf-item.xf-checked { color: #3498db; font-weight: 700; }
.xf-sep { height: 1px; margin: 4px 0; background: var(--border-light); }
.xf-search {
margin: 6px 12px; padding: 7px 9px; font: inherit; color: var(--text-main);
background: var(--input-bg); border: 1px solid var(--border-color); border-radius: 5px;
}
.xf-list {
flex: 1 1 auto; min-height: 80px; max-height: 260px; overflow-y: auto;
margin: 0 12px; padding: 4px 0; border: 1px solid var(--border-light); border-radius: 5px;
}
.xf-opt { display: flex; align-items: center; gap: 8px; padding: 4px 10px; cursor: pointer; white-space: nowrap; }
.xf-opt span { overflow: hidden; text-overflow: ellipsis; }
.xf-opt:hover { background: var(--hover-bg); }
.xf-opt[hidden] { display: none; }
.xf-all { font-weight: 600; border-bottom: 1px solid var(--border-light); }
.xf-empty { padding: 8px 10px; color: var(--text-muted); }
.xf-actions { display: flex; justify-content: flex-end; gap: 8px; padding: 10px 12px 6px; }
.xf-actions button {
min-width: 76px; padding: 6px 12px; cursor: pointer; font: inherit;
background: transparent; color: inherit; border: 1px solid var(--border-color); border-radius: 5px;
}
.xf-actions .xf-ok { background: #3498db; border-color: #3498db; color: #fff; font-weight: 600; }
.xf-actions .xf-ok:disabled { opacity: 0.5; cursor: default; }
.xf-backdrop { position: fixed; inset: 0; z-index: 2590; background: rgba(0,0,0,0.5); }
.xf-menu.xf-sheet {
left: 0 !important; right: 0; bottom: 0; top: auto !important; margin: 0 auto;
width: auto; max-width: 560px; max-height: 82vh !important; max-height: 82dvh !important;
border-radius: 14px 14px 0 0; padding-bottom: calc(6px + env(safe-area-inset-bottom));
font-size: 15px;
}
.xf-sheet .xf-head {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 16px 10px; margin-bottom: 4px; font-weight: 700; font-size: 1.05em;
border-bottom: 1px solid var(--border-light);
}
.xf-x { background: none; border: 0; color: inherit; font-size: 1.7em; line-height: 1; cursor: pointer; padding: 0 4px; }
.xf-sheet .xf-item { padding: 11px 16px; }
.xf-sheet .xf-opt { padding: 9px 12px; }
.xf-sheet .xf-list { max-height: none; }
.xf-sheet input[type="checkbox"] { width: 18px; height: 18px; }
.xf-sheet .xf-actions button { flex: 1; padding: 11px; }
/* Filter chip bar (cards) */
.xf-bar { display: none; }
@media (max-width: 1100px) {
.xf-bar {
display: flex; gap: 6px; padding: 8px 10px; overflow-x: auto; scrollbar-width: none;
position: sticky; top: var(--sticky-top, 112px); z-index: 25;
background: linear-gradient(rgba(0,0,0,0.15), rgba(0,0,0,0.15)), var(--bg-card);
border-bottom: 1px solid var(--border-color); box-shadow: 0 4px 8px rgba(0,0,0,0.25);
}
.xf-bar::-webkit-scrollbar { display: none; }
}
.xf-chip {
display: inline-flex; align-items: center; gap: 5px; flex: none; padding: 6px 11px; cursor: pointer;
font: inherit; font-size: 0.82em; white-space: nowrap;
background: var(--bg-body); color: var(--text-main); border: 1px solid var(--border-color); border-radius: 999px;
}
.xf-chip svg { width: 10px; height: 10px; }
.xf-chip.xf-on { background: #f1c40f; border-color: #f1c40f; color: #1f2937; font-weight: 600; }
.xf-chip.xf-clear { color: #e74c3c; border-color: #e74c3c; }
.xf-chip.xf-fields { background: #3498db; border-color: #3498db; color: #fff; font-weight: 600; }
/* Custom View card-field editor */
.xf-hint { padding: 0 16px 8px; font-size: 0.85em; color: var(--text-muted); }
.xf-fields { flex: 0 0 auto; max-height: 34vh; overflow-y: auto; padding: 0 12px; } /* never squeezed by the add list */
.xf-field { display: flex; align-items: center; gap: 8px; padding: 6px 4px; border-bottom: 1px solid var(--border-light); }
.xf-field-name { flex: 1; min-width: 0; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.xf-badge { font-size: 0.7em; padding: 2px 8px; border-radius: 999px; background: #3498db; color: #fff; }
.xf-mini {
width: 36px; height: 36px; flex: none; cursor: pointer; font-size: 1.1em;
background: transparent; color: inherit; border: 1px solid var(--border-color); border-radius: 6px;
}
.xf-mini:disabled { opacity: 0.3; cursor: default; }
.xf-add { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; margin-top: 8px; }
.xf-fields-sheet .xf-list { flex: 1 1 auto; min-height: 110px; }
.xf-fields-sheet .xf-add .xf-search { flex: none; }
.xf-group { padding: 10px 10px 4px; font-size: 0.75em; font-weight: 700; letter-spacing: 0.5px; text-transform: uppercase; color: var(--text-muted); }
.xf-addfield { display: block; width: 100%; padding: 8px 10px; text-align: left; cursor: pointer; background: none; border: 0; color: #3498db; font: inherit; }
.xf-addfield:hover { background: var(--hover-bg); }
.xf-addfield[hidden] { display: none; }
.xf-clear-all { white-space: nowrap; color: #f1c40f !important; border-color: rgba(241,196,15,0.6) !important; }
.xf-clear-all svg { width: 14px; height: 14px; flex: none; }