This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git

commit c97325505b3972ddb6813cef21a7eafc1a12c966
Author: James Bognar <[email protected]>
AuthorDate: Thu Aug 20 17:45:21 2026 -0400

    Unify view toolbar chrome: grouped ribbons, findViewWrapper, paging 
checkmark
    
    Walk to the real DataTables wrapper so search sits on the paging/icon row, 
cluster
    ungrouped icon actions into one ribbon, hide leftover DT2 layout rows, and 
move the
    paging selected-item check into a left gutter.
---
 .../org/apache/juneau/views/juneau-ribbon.js       |  19 +-
 .../org/apache/juneau/views/juneau-views.css       | 340 +++++++++++++++++----
 .../rest/server/views/PagingPill_Wiring_Test.java  |  15 +-
 .../views/ViewsJs_ConfigApplication_Test.java      |  12 +
 .../src/test/js/config-application.cjs             |  17 ++
 5 files changed, 336 insertions(+), 67 deletions(-)

diff --git 
a/juneau-rest/juneau-rest-server-views/src/main/resources/org/apache/juneau/views/juneau-ribbon.js
 
b/juneau-rest/juneau-rest-server-views/src/main/resources/org/apache/juneau/views/juneau-ribbon.js
index 6d8df616fd..bdb8a94c07 100644
--- 
a/juneau-rest/juneau-rest-server-views/src/main/resources/org/apache/juneau/views/juneau-ribbon.js
+++ 
b/juneau-rest/juneau-rest-server-views/src/main/resources/org/apache/juneau/views/juneau-ribbon.js
@@ -224,10 +224,11 @@
         *
         * <p>Adjacent actions sharing a non-null {@code group} id 
(visual-parity design doc §4.A, item 2/5) are
         * clustered into ONE segmented {@code .juneau-view-ribbon-group} 
wrapper (shared borders, rounded only on the
-        * outer ends - see juneau-views.css) via the local {@code place(el, 
groupId)} helper below.  An {@code export}
-        * action's own resolved buttons are always clustered this way even 
without an explicit {@code group} (one
-        * action, one visual cluster - each export action gets its own 
synthetic per-index id so consecutive distinct
-        * export actions never merge).  A {@code divider} or an ungrouped 
action always closes any open cluster.
+        * outer ends - see juneau-views.css) via the local {@code place(el, 
groupId)} helper below.  Actions with no
+        * explicit {@code group} (including an {@code export} action's 
resolved buttons) share the synthetic
+        * {@code __ungrouped} id so consecutive icon buttons — refresh + 
copy/csv/excel/pdf — render as one connected
+        * ribbon rather than orphan glyphs.  A {@code divider} always closes 
any open cluster; an explicit
+        * {@code group} id still splits clusters the way the caller declared.
         */
        // NOSONAR javascript:S3776 -- one dispatch branch per 
RibbonAction.type (design doc §4.A); each branch is a
        // few lines and several are pinned verbatim by the wiring canary tests 
below `functionBody(body, "function
@@ -259,7 +260,7 @@
                        openGroup.el.appendChild(el);
                }
 
-               actions.forEach(function (a, idx) {
+               actions.forEach(function (a) {
                        if (a.type === "divider") {
                                openGroup = null;
                                const d = document.createElement("span");
@@ -278,7 +279,7 @@
                                                        buttons: ids,
                                                        exportOptions: { 
columns: ":visible" }
                                                });
-                                               const exportGroupId = a.group 
!= null ? a.group : ("__export" + idx);
+                                               const exportGroupId = a.group 
!= null ? a.group : "__ungrouped";
                                                ids.forEach(function (id) {
                                                        place(button(id, 
resolveButtonIcon(null, id), function () {
                                                                
ctx.dataTable.button(id).trigger();
@@ -289,7 +290,7 @@
                                return;
                        }
                        if (a.type === "refresh") {
-                               place(button(a.title || "Refresh", 
resolveButtonIcon(a, "refresh"), function () { ctx.redraw(); }), a.group || 
null);
+                               place(button(a.title || "Refresh", 
resolveButtonIcon(a, "refresh"), function () { ctx.redraw(); }), a.group || 
"__ungrouped");
                                return;
                        }
                        if (a.type === "columnSearchToggle") {
@@ -297,11 +298,11 @@
                                        csBtn.setAttribute("aria-pressed", 
toggleColumnSearch(viewDef, ctx) ? "true" : "false");
                                });
                                csBtn.setAttribute("aria-pressed", 
ctx.columnSearchOn ? "true" : "false");
-                               place(csBtn, a.group || null);
+                               place(csBtn, a.group || "__ungrouped");
                                return;
                        }
                        if (a.type === "option") {
-                               place(optionToggle(viewDef, a, ctx), a.group || 
null);
+                               place(optionToggle(viewDef, a, ctx), a.group || 
"__ungrouped");
                                return;
                        }
                        if (a.type === "optionGroup") {
diff --git 
a/juneau-rest/juneau-rest-server-views/src/main/resources/org/apache/juneau/views/juneau-views.css
 
b/juneau-rest/juneau-rest-server-views/src/main/resources/org/apache/juneau/views/juneau-views.css
index 8f710514dd..825b5e3803 100644
--- 
a/juneau-rest/juneau-rest-server-views/src/main/resources/org/apache/juneau/views/juneau-views.css
+++ 
b/juneau-rest/juneau-rest-server-views/src/main/resources/org/apache/juneau/views/juneau-views.css
@@ -43,6 +43,54 @@
        vertical-align: baseline;
 }
 
+/*
+ * Threshold progress bar.  Views-owned shape classes (.jc-progress / 
.jc-progress-bar.is-* /
+ * .jc-progress-label); theming hook is .progress.{field}.  Neutral greys via 
--jc-* tokens so the bar
+ * is legible without a theme; a consumer stylesheet may override the tokens.  
No hue names.
+ */
+:root {
+       --jc-progress-track: #e6e6e6;
+       --jc-progress-ok: #6b6b6b;
+       --jc-progress-warn: #8a8a8a;
+       --jc-progress-exceeds: #333333;
+       --jc-popover-bg: #f7f7f7;
+       --jc-popover-border: #ccc;
+       --jc-popover-shadow: 0 2px 10px rgba(0, 0, 0, 0.18);
+       --jc-popover-z: 1200;
+}
+
+.jc-progress {
+       position: relative;
+       display: inline-block;
+       width: 6em;
+       height: 1.1em;
+       background: var(--jc-progress-track);
+       border-radius: 0.2em;
+       vertical-align: middle;
+       overflow: hidden;
+}
+
+.jc-progress-bar {
+       position: absolute;
+       left: 0;
+       top: 0;
+       bottom: 0;
+}
+
+.jc-progress-bar.is-ok { background: var(--jc-progress-ok); }
+.jc-progress-bar.is-warn { background: var(--jc-progress-warn); }
+.jc-progress-bar.is-exceeds { background: var(--jc-progress-exceeds); }
+
+.jc-progress-label {
+       position: relative;
+       z-index: 1;
+       display: block;
+       text-align: center;
+       font-size: 0.7em;
+       line-height: 1.1em;
+       white-space: nowrap;
+}
+
 /* Fail-loud contract-version-mismatch banner - neutral, no palette color. */
 .juneau-view-error {
        display: block;
@@ -63,47 +111,71 @@
 .juneau-view-ribbon-btn:focus-visible,
 .juneau-view-pagingpill-btn:focus-visible,
 .juneau-view-pagingpill-menu-option:focus-visible,
-.juneau-view-columnsearch-input:focus-visible {
+.juneau-view-columnsearch-input:focus-visible,
+.juneau-ts:focus-visible {
        outline: 2px solid currentColor;
        outline-offset: 2px;
 }
 
 /*
- * DataTables table density/shape (design doc §4 IRS-parity pass) - compact 
rows + thin separators.  NEUTRAL -
- * shape only (font-size, cell padding, border WIDTH/style); no 
color/background-color/border-color palette here
- * (console-ui's chrome.css themes the SAME selectors with --jc-* colors, 
mirroring the ".tag"/ribbon reconciliation
- * above).  Selectors list BOTH `table[data-juneau-view]` (the marker 
attribute present from first paint) and
- * `table.dataTable` (the class DataTables' own init ADDS to that same element 
- it never removes the attribute,
- * so both describe the identical element pre/post-init) so the rule applies 
whether or not DataTables has run
- * yet.  Child-combinator (`>`) selectors deliberately mirror the vendored 
dataTables.dataTables.css structure
- * (`table.dataTable>thead>tr>th`, `table.dataTable>tbody>tr>td`) so this 
rule's specificity TIES the vendored
- * one instead of losing to it; this file is served AFTER the vendored 
DataTables CSS in every juneau-views page's
- * <head>, so the tie resolves in our favor by cascade order.
+ * DataTables table density/shape (IRS Instances metrics: compact 4px 5px 
cells, 0.75rem type,
+ * hairline separators, no vertical grid).  NEUTRAL - shape only (font-size, 
padding, border WIDTH/style);
+ * no color/background-color/border-color palette here (console-ui's 
chrome.css themes the SAME selectors
+ * with --jc-* colors).  Selectors list BOTH `table[data-juneau-view]` and 
`table.dataTable` so the rule
+ * applies whether or not DataTables has run yet.  Child-combinator (`>`) 
selectors deliberately mirror
+ * the vendored dataTables.dataTables.css structure so this rule's specificity 
TIES the vendored one;
+ * this file is served AFTER the vendored DataTables CSS, so the tie resolves 
in our favor.
  */
 table[data-juneau-view],
 table.dataTable {
-       font-size: 0.8125rem;
+       font-size: 0.75rem;
+       border-collapse: collapse;
+       border-spacing: 0;
 }
 
 table[data-juneau-view] > thead > tr > th,
 table[data-juneau-view] > thead > tr > td,
 table.dataTable > thead > tr > th,
 table.dataTable > thead > tr > td {
-       padding: 8px 10px;
-       font-weight: 600;
+       padding: 4px 5px;
+       font-weight: normal;
        text-align: left;
-       border-bottom-width: 1px;
-       border-bottom-style: solid;   /* color falls back to currentColor - no 
palette color set here */
+       vertical-align: middle;
+       min-width: 20px;
+       border: none;
+       border-bottom: 1px solid;     /* currentColor - hairline; chrome.css 
themes the color */
 }
 
 table[data-juneau-view] > tbody > tr > th,
 table[data-juneau-view] > tbody > tr > td,
 table.dataTable > tbody > tr > th,
 table.dataTable > tbody > tr > td {
-       padding: 6px 10px;
+       padding: 4px 5px;
        vertical-align: middle;
-       border-bottom-width: 1px;
-       border-bottom-style: solid;   /* color falls back to currentColor - no 
palette color set here */
+       border: none;
+       border-bottom: 1px solid;
+}
+
+/* Sort affordance: keep DT2's dt-column-order on the RIGHT of the label 
(numeric columns otherwise
+   reverse the flex and put the chevron on the left). Hide DT1's duplicate 
:before/:after glyphs. */
+table[data-juneau-view] thead th div.dt-column-header,
+table[data-juneau-view] thead td div.dt-column-header,
+table.dataTable thead th div.dt-column-header,
+table.dataTable thead td div.dt-column-header {
+       flex-direction: row !important;   /* DT2 numeric columns otherwise 
reverse and put the chevron on the left */
+}
+
+table.dataTable thead .sorting:before,
+table.dataTable thead .sorting_asc:before,
+table.dataTable thead .sorting_desc:before,
+table.dataTable thead .sorting_asc_disabled:before,
+table.dataTable thead .sorting_desc_disabled:before,
+table.dataTable thead .sorting:after,
+table.dataTable thead .sorting_asc:after,
+table.dataTable thead .sorting_desc:after,
+table.dataTable thead .sorting_asc_disabled:after,
+table.dataTable thead .sorting_desc_disabled:after {
+       content: none;
 }
 
 /*
@@ -120,24 +192,26 @@ table.dataTable > tbody > tr > td {
 .juneau-view-ribbon {
        display: flex;
        align-items: center;
-       flex-wrap: wrap;
-       gap: 0.25em;
+       flex-wrap: nowrap;
+       gap: 0.5em;
 }
 
 .juneau-view-ribbon-btn {
        display: flex;
        align-items: center;
        justify-content: center;
-       width: 32px;
+       box-sizing: border-box;
+       min-width: 32px;
        height: 32px;
-       padding: 0;
+       padding: 0 10px;              /* IRS ribbon-button horizontal padding 
between glyphs */
        border: 1px solid;            /* currentColor - no palette color set 
here */
        border-radius: 0.25em;
        background: transparent;
+       font-family: inherit;         /* <button> UA font otherwise; apps theme 
via body font */
        cursor: pointer;
 }
 
-.juneau-view-ribbon-btn svg { width: 18px; height: 18px; fill: currentColor; }
+.juneau-view-ribbon-btn svg { width: 16px; height: 16px; fill: currentColor; }
 
 .juneau-view-ribbon-btn:disabled { opacity: 0.4; cursor: not-allowed; }
 
@@ -149,28 +223,34 @@ table.dataTable > tbody > tr > td {
 .juneau-view-ribbon-btn[aria-pressed="true"] { border-width: 2px; }
 
 /*
- * Segmented ribbon group (design doc §4.A, item 2/5 - IRS parity): a "single 
ribbon" of adjacent buttons that
- * share a border (no double borders) with rounding only on the group's outer 
ends.  NEUTRAL - shape only (radius/
- * margin-collapse); console-ui's chrome.css themes the member buttons' colors 
exactly like a standalone button
- * (same ".juneau-view-ribbon-btn" class, so :hover/:active/[aria-pressed] all 
already apply unchanged).  The
- * negative margin-left overlaps each member's left border onto its 
predecessor's right border (collapsing the
- * pair into one hairline); a raised z-index on hover/focus/pressed keeps that 
member's own border/background on
- * top of its neighbors' overlapping edges.
+ * Segmented ribbon group (design doc §4.A, item 2/5 - IRS parity): one OUTER 
chrome around the group,
+ * no inner vertical borders between glyphs.  Ends keep left/right; middles 
keep only top/bottom.  Rounding
+ * only on the group's outer ends.  NEUTRAL - shape only; console-ui's 
chrome.css themes the member buttons'
+ * colors exactly like a standalone button (same ".juneau-view-ribbon-btn" 
class).
  */
-.juneau-view-ribbon-group { display: inline-flex; }
+.juneau-view-ribbon-group { display: inline-flex; align-items: stretch; }
 
 .juneau-view-ribbon-group .juneau-view-ribbon-btn {
        border-radius: 0;
-       margin-left: -1px;
-       position: relative;
+       border-left: none;
+       border-right: none;
+}
+
+.juneau-view-ribbon-group .juneau-view-ribbon-btn:first-child {
+       border-left: 1px solid;
+       border-radius: 0.25em 0 0 0.25em;
 }
 
-.juneau-view-ribbon-group .juneau-view-ribbon-btn:first-child { margin-left: 
0; border-radius: 0.25em 0 0 0.25em; }
-.juneau-view-ribbon-group .juneau-view-ribbon-btn:last-child { border-radius: 
0 0.25em 0.25em 0; }
+.juneau-view-ribbon-group .juneau-view-ribbon-btn:last-child {
+       border-right: 1px solid;
+       border-radius: 0 0.25em 0.25em 0;
+}
 
-.juneau-view-ribbon-group .juneau-view-ribbon-btn:hover,
-.juneau-view-ribbon-group .juneau-view-ribbon-btn:focus,
-.juneau-view-ribbon-group .juneau-view-ribbon-btn[aria-pressed="true"] { 
z-index: 1; }
+.juneau-view-ribbon-group .juneau-view-ribbon-btn:only-child {
+       border-left: 1px solid;
+       border-right: 1px solid;
+       border-radius: 0.25em;
+}
 
 /*
  * Unified paging ribbon (visual-parity follow-up) - hides BOTH DataTables 
major-version's native length-select/
@@ -184,11 +264,22 @@ table.dataTable > tbody > tr > td {
  */
 .dataTables_length, .dt-length, .dataTables_paginate, .dt-paging { display: 
none; }
 
+/*
+ * DataTables 2's default layout puts search/length/paging in their own 
`.dt-layout-row` *above* the table
+ * (sibling of `.dt-layout-row.dt-layout-table`).  `buildToolbarRow` relocates 
`.dt-search` into the unified
+ * toolbar; these leftover feature rows would otherwise keep an empty (or 
still-populated, if the move missed)
+ * second line.  DT1 has no `.dt-layout-*` nodes, so this rule is a no-op 
there.
+ */
+.dt-container > .dt-layout-row:not(.dt-layout-table) { display: none; }
+
 .juneau-view-pagingpill {
        display: inline-flex;
        align-items: stretch;
+       box-sizing: border-box;
+       height: 32px;
        border: 1px solid;            /* currentColor - no palette color set 
here */
        border-radius: 0.25em;
+       font-family: inherit;         /* <button> UA font otherwise; apps theme 
via body font */
 }
 
 /* Corner-rounding on the outer segments only (no `overflow: hidden` on the 
pill itself - that would clip the
@@ -200,17 +291,18 @@ table.dataTable > tbody > tr > td {
        display: flex;
        align-items: center;
        justify-content: center;
-       width: 32px;
-       height: 32px;
-       padding: 0;
+       box-sizing: border-box;
+       min-width: 32px;
+       padding: 0 8px;
        border: none;
        border-right: 1px solid;      /* currentColor - segment divider; 
last-child override below */
        background: transparent;
+       font-family: inherit;
        cursor: pointer;
 }
 .juneau-view-pagingpill-btn:last-child { border-right: none; }
 .juneau-view-pagingpill-btn:disabled { opacity: 0.4; cursor: not-allowed; }
-.juneau-view-pagingpill-btn svg { width: 18px; height: 18px; fill: 
currentColor; }
+.juneau-view-pagingpill-btn svg { width: 12px; height: 12px; fill: 
currentColor; }
 
 /*
  * The pill's central segment (visual-parity follow-up): BOTH the compact "n-n 
of n" range summary AND the
@@ -228,18 +320,18 @@ table.dataTable > tbody > tr > td {
        display: flex;
        align-items: center;
        gap: 0.35em;
-       height: 32px;
-       padding: 0 0.6em;
+       padding: 0 0.75em;
        border: none;
        background: transparent;
-       font-size: 0.8125rem;
+       font-family: inherit;
+       font-size: inherit;
        white-space: nowrap;
        cursor: pointer;
 }
 
-.juneau-view-pagingpill-info { white-space: nowrap; }
+.juneau-view-pagingpill-info { white-space: nowrap; font-family: inherit; }
 
-.juneau-view-pagingpill-caret svg { width: 14px; height: 14px; fill: 
currentColor; }
+.juneau-view-pagingpill-caret svg { width: 12px; height: 12px; fill: 
currentColor; }
 
 /* The popup itself - absolutely positioned below the menu button; `[hidden]` 
(set/cleared by juneau-views.js)
    is enough to show/hide it, so no explicit `display` toggle rule is needed 
here. */
@@ -257,19 +349,27 @@ table.dataTable > tbody > tr > td {
 }
 
 .juneau-view-pagingpill-menu-option {
-       padding: 0.3em 1.75em 0.3em 0.75em;
+       padding: 0.3em 0.75em;
        white-space: nowrap;
        cursor: pointer;
 }
 
+/* Reserved left gutter so every option's label lines up, selected or not - 
the selected-item check sits
+   here (standard checkbox-column layout), not floated to the far right of the 
row. */
+.juneau-view-pagingpill-menu-option::before {
+       content: "";
+       display: inline-block;
+       width: 1.1em;
+       margin-right: 0.35em;
+       text-align: center;
+}
+
 .juneau-view-pagingpill-menu-option[aria-selected="true"] { font-weight: bold; 
}
 
 /* A plain checkmark glyph (not an icon-registry glyph - a bare CSS content 
string) indicating the current
    page-size selection, mirroring the affordance a native <select> gives its 
chosen <option> for free. */
-.juneau-view-pagingpill-menu-option[aria-selected="true"]::after {
+.juneau-view-pagingpill-menu-option[aria-selected="true"]::before {
        content: "\2713";
-       float: right;
-       margin-left: 0.75em;
 }
 
 /*
@@ -279,21 +379,44 @@ table.dataTable > tbody > tr > td {
  */
 .juneau-view-toolbar-row {
        display: flex;
-       flex-wrap: wrap;
+       flex-wrap: nowrap;
        justify-content: space-between;
        align-items: center;
        gap: 0.5em;
        margin: 0 0 0.5em 0;
+       font-size: 0.75rem;           /* same type size as the table / IRS 
paging+search */
 }
 
 .juneau-view-toolbar-left,
 .juneau-view-toolbar-right {
        display: flex;
-       flex-wrap: wrap;
+       flex-wrap: nowrap;
        align-items: center;
        gap: 0.5em;
 }
 
+/* Native DT search box, once relocated into the right cluster: inline with 
the icon ribbon, not a
+   full-width / floated block that drops onto its own line. Same 32px control 
class as the paging
+   pill and icon ribbon (box-sizing so the 1px border is inside the 32px, not 
added on top). */
+.juneau-view-toolbar-right .dataTables_filter,
+.juneau-view-toolbar-right .dt-search {
+       float: none;
+       margin: 0;
+       display: inline-flex;
+       align-items: center;
+}
+
+.juneau-view-toolbar-right .dataTables_filter input,
+.juneau-view-toolbar-right .dt-search input {
+       box-sizing: border-box;
+       height: 32px;
+       margin: 0;
+       padding: 0 0.5em;
+       border: 1px solid;            /* currentColor - no palette color set 
here */
+       border-radius: 0.25em;
+       font-family: inherit;
+}
+
 .juneau-view-ribbon-divider {
        display: inline-block;
        width: 0.5em;               /* pure spacing only; no visible glyph. 
Stays in the DOM (unchanged JS branch). */
@@ -406,11 +529,18 @@ table.dataTable > tbody > tr > td {
        cursor: pointer;
 }
 
+.juneau-view-detail-row > td:first-child {
+       padding-left: 0;
+}
+
 .juneau-view-detail-row > td:first-child::before {
        content: "\25B8";   /* ▸ */
        display: inline-block;
-       width: 1em;
-       margin-right: 0.35em;
+       width: 20px;
+       max-width: 20px;
+       margin-right: 0;
+       text-align: center;
+       vertical-align: middle;
 }
 
 .juneau-view-detail-open > td:first-child::before {
@@ -573,8 +703,12 @@ table.dataTable > tbody > tr > td {
  */
 .juneau-view-select-th,
 .juneau-view-select-cell {
-       width: 2.25em;
+       width: 1%;
+       white-space: nowrap;
        text-align: center;
+       vertical-align: middle;
+       padding-left: 8px;
+       padding-right: 8px;
 }
 
 .juneau-view-select-checkbox,
@@ -607,3 +741,95 @@ table.dataTable > tbody > tr > td {
        opacity: 0.4;
        cursor: not-allowed;
 }
+
+/*
+ * Timestamp (ts-zulu / datetime+popup) hover/focus popup — two lines of local 
+ California time.
+ * White overlay (same exception as .juneau-view-dialog): a floating box must 
be opaque to stay readable
+ * over table cells.  Position/visibility are set by juneau-renders.js.
+ */
+.juneau-ts {
+       white-space: nowrap;
+       cursor: default;
+}
+
+.juneau-ts-popup {
+       position: fixed;
+       z-index: 1100;
+       display: none;
+       pointer-events: none;
+       padding: 0.4em 0.65em;
+       border: 1px solid #ccc;
+       border-radius: 4px;
+       background: #fff;
+       color: #222;
+       box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+       font-size: 0.8125rem;
+       line-height: 1.4;
+       white-space: nowrap;
+}
+
+/*
+ * Author-declared cell popover (Render.popover).  Portaled to document.body, 
position:fixed.
+ * Neutral greys via --jc-* tokens; do not restyle .juneau-ts-popup here.
+ */
+.jc-cell-popover-trigger {
+       display: inline-block;
+       margin-left: 0.35em;
+       padding: 0;
+       border: 0;
+       background: transparent;
+       cursor: pointer;
+       vertical-align: middle;
+       width: 1em;
+       height: 1em;
+       line-height: 1;
+}
+
+.jc-cell-popover-trigger::before {
+       content: "";
+       display: block;
+       width: 0.55em;
+       height: 0.55em;
+       margin: 0.15em auto 0;
+       border: 0.12em solid currentColor;
+       border-radius: 50%;
+       box-sizing: border-box;
+}
+
+.jc-cell-popover {
+       position: fixed;
+       z-index: var(--jc-popover-z);
+       display: none;
+       min-width: 12em;
+       max-width: 24em;
+       padding: 0.5em 0.75em;
+       border: 1px solid var(--jc-popover-border);
+       border-radius: 4px;
+       background: var(--jc-popover-bg);
+       color: #222;
+       box-shadow: var(--jc-popover-shadow);
+       font-size: 0.8125rem;
+       line-height: 1.4;
+}
+
+.jc-cell-popover-title {
+       font-weight: 600;
+       margin-bottom: 0.35em;
+}
+
+.jc-cell-popover-row {
+       display: flex;
+       gap: 0.75em;
+       margin: 0.15em 0;
+}
+
+.jc-cell-popover-label {
+       flex: 0 0 7em;
+       color: #555;
+}
+
+.jc-cell-popover-value {
+       flex: 1 1 auto;
+       min-width: 0;
+       word-break: break-word;
+}
diff --git 
a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/PagingPill_Wiring_Test.java
 
b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/PagingPill_Wiring_Test.java
index 899b58b090..ca763409f8 100644
--- 
a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/PagingPill_Wiring_Test.java
+++ 
b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/PagingPill_Wiring_Test.java
@@ -134,7 +134,9 @@ class PagingPill_Wiring_Test extends TestBase {
         * info node, which (a) put it at the BOTTOM of the table by default, 
and (b) broke once `info:false` removed
         * that info node entirely (the pill's insertion fallback appended to 
the END of the wrapper - still the
         * bottom). buildToolbarRow(...) now owns ALL toolbar placement: pill + 
native search + ribbon assembled into
-        * ONE row and inserted as the wrapper's first child, i.e. above the 
table.
+        * ONE row and inserted as the wrapper's first child, i.e. above the 
table.  The wrapper is the real DT
+        * container ({@code findViewWrapper} → {@code .dt-container} / {@code 
.dataTables_wrapper}), not
+        * {@code table.parentNode}, which under DT2 is a nested layout cell 
that cannot see {@code .dt-search}.
         */
        @Test void 
b02_buildToolbarRow_assemblesPillSearchAndRibbonAboveTheTable() throws 
Exception {
                var body = 
cWithMixin.get(ViewsMixin.VIEWS_JS_PATH).run().assertStatus(200).getContent().asString();
@@ -148,8 +150,13 @@ class PagingPill_Wiring_Test extends TestBase {
                assertTrue(fnBody.contains("juneau-view-toolbar-right"), 
fnBody);
                assertTrue(fnBody.contains("left.appendChild(pill)"), fnBody);
 
+               var findBody = functionBody(body, "function findViewWrapper(");
+               assertTrue(findBody.contains(".dt-container, 
.dataTables_wrapper"), findBody);
+               assertTrue(findBody.contains("table.closest("), findBody);
+
                var initBody = functionBody(body, "function constructTable(");
                assertTrue(initBody.contains("buildPagingPill("), initBody);
+               assertTrue(initBody.contains("findViewWrapper(table)"), 
initBody);
                assertTrue(initBody.contains("buildToolbarRow(wrapper, pill, 
bar)"), initBody);
        }
 
@@ -163,6 +170,8 @@ class PagingPill_Wiring_Test extends TestBase {
                var hideStart = body.indexOf(".dataTables_length, .dt-length, 
.dataTables_paginate, .dt-paging {");
                var hideEnd = body.indexOf("}", hideStart);
                assertTrue(body.substring(hideStart, 
hideEnd).contains("display: none"), body);
+               // DT2 leftover feature rows (search/length/paging) after the 
unified toolbar relocates those controls.
+               assertTrue(body.contains(".dt-container > 
.dt-layout-row:not(.dt-layout-table) { display: none; }"), body);
        }
 
        @Test void c02_viewsCss_hasPagingPillShapeAndDisabledDim() throws 
Exception {
@@ -170,6 +179,7 @@ class PagingPill_Wiring_Test extends TestBase {
                assertTrue(body.contains(".juneau-view-pagingpill {"), body);
                assertTrue(body.contains(".juneau-view-pagingpill-btn {"), 
body);
                assertTrue(body.contains(".juneau-view-pagingpill-btn:disabled 
{ opacity:"), body);
+               assertTrue(body.contains("font-family: inherit"), body);
        }
 
        /**
@@ -228,6 +238,9 @@ class PagingPill_Wiring_Test extends TestBase {
                assertTrue(body.contains(".juneau-view-pagingpill-menu {"), 
body);
                assertTrue(body.contains(".juneau-view-pagingpill-menu-option 
{"), body);
                
assertTrue(body.contains(".juneau-view-pagingpill-menu-option[aria-selected=\"true\"]"),
 body);
+               // Selected-item check lives in a left-side ::before gutter, 
not a far-right ::after float.
+               
assertTrue(body.contains(".juneau-view-pagingpill-menu-option[aria-selected=\"true\"]::before"),
 body);
+               
assertFalse(body.contains(".juneau-view-pagingpill-menu-option[aria-selected=\"true\"]::after"),
 body);
        }
 
        /** data-testid hooks (selector-ambiguity removal): the unified paging 
ribbon and the right actions ribbon. */
diff --git 
a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ViewsJs_ConfigApplication_Test.java
 
b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ViewsJs_ConfigApplication_Test.java
index 8b2b3d13fc..222d13a81b 100644
--- 
a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ViewsJs_ConfigApplication_Test.java
+++ 
b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ViewsJs_ConfigApplication_Test.java
@@ -119,6 +119,10 @@ class ViewsJs_ConfigApplication_Test extends TestBase {
                assertTrue(compute.contains("String(override).trim()"), 
compute);
                var swap = functionBody(body, "function swapRenderId(");
                assertTrue(swap.contains("render.meta"), swap);
+               assertTrue(swap.contains("render.popover"), swap);
+               assertTrue(body.contains("function copyCatalogColumn("), body);
+               var copy = functionBody(body, "function copyCatalogColumn(");
+               assertTrue(copy.contains("popover"), copy);
        }
 
        @Test void a05_dtIndex_isIndexIntoActualOptsColumns_notVisibleOffset() 
throws Exception {
@@ -327,4 +331,12 @@ class ViewsJs_ConfigApplication_Test extends TestBase {
                var r = report();
                assertEquals(List.of("A", "C"), r.get("m_firstVisitVisible"));
        }
+
+       @Test void b14_copyAndSwap_preservePopover() {
+               var r = report();
+               assertEquals(true, r.get("n_hasCopyExport"));
+               assertEquals(true, r.get("n_copyPreservesPopover"));
+               assertEquals(true, r.get("n_copyIsStructured"));
+               assertEquals(true, r.get("n_swapKeepsPopover"));
+       }
 }
diff --git 
a/juneau-rest/juneau-rest-server-views/src/test/js/config-application.cjs 
b/juneau-rest/juneau-rest-server-views/src/test/js/config-application.cjs
index 7139b7df14..a6f7c5fd4c 100644
--- a/juneau-rest/juneau-rest-server-views/src/test/js/config-application.cjs
+++ b/juneau-rest/juneau-rest-server-views/src/test/js/config-application.cjs
@@ -180,4 +180,21 @@ const catalogHiddenB = [
 const firstVisit = C.computeEffectiveColumns(catalogHiddenB, null);
 out.m_firstVisitVisible = firstVisit.filter(function (c) { return c.visible; 
}).map(function (c) { return c.data; });
 
+const popoverCol = {
+       data: 'used',
+       render: {
+               id: 'progress',
+               meta: { max: '100' },
+               popover: { title: 'CPU', fields: [{ data: 'actual', title: 
'Actual', render: { id: 'decimal' } }] }
+       }
+};
+const copied = C.copyCatalogColumn(popoverCol);
+out.n_copyPreservesPopover = !!(copied.render && copied.render.popover && 
copied.render.popover.title === 'CPU'
+       && copied.render.popover.fields[0].data === 'actual');
+copied.render.popover.title = 'mutated';
+out.n_copyIsStructured = popoverCol.render.popover.title === 'CPU';
+const swapped = C.swapRenderId(popoverCol.render, 'decimal');
+out.n_swapKeepsPopover = swapped.id === 'decimal' && swapped.popover && 
swapped.popover.title === 'CPU';
+out.n_hasCopyExport = typeof C.copyCatalogColumn === 'function';
+
 process.stdout.write(JSON.stringify(out));

Reply via email to