leaderboard: lock GT row via in-iframe scroll capped by max-height
Browse filesThe previous attempt capped the gallery scroll box from JS using
window.parent.innerHeight, but inside HF's nested iframes that is not the
browser viewport, so the cap came out huge: the box never scrolled, the
iframe grew to full height, and the page scrolled the GT row away.
Instead, scroll inside the iframe itself (the column header + GT row are
sticky against the iframe viewport, which reliably works) and cap the
iframe with `max-height: 80vh` in app.py, where vh is the Gradio doc
viewport. fitIframe still shrinks the iframe to content for a compact
page when there are few rows; once content exceeds the cap the iframe
body scrolls and the GT row stays pinned. Drops the unreliable JS cap
and the parent-aware modal positioning (back to plain fixed centering).
Co-authored-by: Cursor <cursoragent@cursor.com>
- app.py +8 -5
- gallery.py +9 -92
|
@@ -793,13 +793,16 @@ def _gallery_iframe_html() -> str:
|
|
| 793 |
rows, _render_proxy_url, _gt_proxy_url, _render_diff_proxy_url,
|
| 794 |
)
|
| 795 |
escaped = html.escape(doc, quote=True)
|
| 796 |
-
# The gallery JS (`fitIframe`) resizes this iframe to its own content so
|
| 797 |
-
#
|
| 798 |
-
#
|
| 799 |
-
#
|
|
|
|
|
|
|
|
|
|
| 800 |
return (
|
| 801 |
f'<iframe srcdoc="{escaped}" '
|
| 802 |
-
'style="width:100%; height:
|
| 803 |
'title="CADGenBench gallery"></iframe>'
|
| 804 |
)
|
| 805 |
|
|
|
|
| 793 |
rows, _render_proxy_url, _gt_proxy_url, _render_diff_proxy_url,
|
| 794 |
)
|
| 795 |
escaped = html.escape(doc, quote=True)
|
| 796 |
+
# The gallery JS (`fitIframe`) resizes this iframe to its own content so it
|
| 797 |
+
# is compact when there are few rows. `max-height` caps it: once the content
|
| 798 |
+
# is taller than the cap the iframe stops growing and its own body scrolls,
|
| 799 |
+
# which is what keeps the gallery's sticky column header + ground-truth row
|
| 800 |
+
# locked at the top. The cap is in `vh` here (reliable in the Gradio doc)
|
| 801 |
+
# rather than computed from JS (the nested-iframe parent height is not the
|
| 802 |
+
# browser viewport). The inline height is the pre-script fallback.
|
| 803 |
return (
|
| 804 |
f'<iframe srcdoc="{escaped}" '
|
| 805 |
+
'style="width:100%; height:80vh; max-height:80vh; border:0; display:block;" '
|
| 806 |
'title="CADGenBench gallery"></iframe>'
|
| 807 |
)
|
| 808 |
|
|
@@ -250,17 +250,7 @@ body {
|
|
| 250 |
.section-caption { margin: 0 0 16px; font-size: 12.5px; color: var(--ink-soft); line-height: 1.5; }
|
| 251 |
.section-caption b { color: var(--ink); font-weight: 600; }
|
| 252 |
|
| 253 |
-
|
| 254 |
-
sticky` against this box, so they stay locked at the top while the
|
| 255 |
-
submission rows scroll. `--gallery-max` is set in JS from the parent
|
| 256 |
-
viewport height; the px value here is only the pre-script fallback.
|
| 257 |
-
When the content is shorter than the cap, `overflow:auto` shows no
|
| 258 |
-
scrollbar and the box just sizes to its rows. */
|
| 259 |
-
.gallery {
|
| 260 |
-
background: var(--panel); border: 1px solid var(--line);
|
| 261 |
-
border-radius: var(--radius); box-shadow: var(--shadow); position: relative;
|
| 262 |
-
max-height: var(--gallery-max, 640px); overflow: auto;
|
| 263 |
-
}
|
| 264 |
.grid-head, .grow {
|
| 265 |
display: grid;
|
| 266 |
grid-template-columns: 52px minmax(200px, 1.3fr) 160px repeat(var(--ncol, 4), minmax(140px, 1fr));
|
|
@@ -569,98 +559,27 @@ function openModal(fxId, sub) {
|
|
| 569 |
document.getElementById('modalNote').innerHTML =
|
| 570 |
'CAD score for this sample: <b>' + cad + '</b>. The full per-sample report '
|
| 571 |
+ '(shape similarity, interface, topology + 3D view) opens from the report viewer.';
|
| 572 |
-
|
| 573 |
-
back.classList.add('show');
|
| 574 |
-
positionModalToView();
|
| 575 |
-
attachModalViewSync();
|
| 576 |
}
|
| 577 |
function closeModal() {
|
| 578 |
document.getElementById('modalBack').classList.remove('show');
|
| 579 |
-
detachModalViewSync();
|
| 580 |
}
|
| 581 |
document.getElementById('modalClose').onclick = closeModal;
|
| 582 |
document.getElementById('modalBack').onclick = (e) => { if (e.target.id === 'modalBack') closeModal(); };
|
| 583 |
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); });
|
| 584 |
|
| 585 |
-
// --- Modal positioning ----------------------------------------------------
|
| 586 |
-
// The page lives in a srcdoc iframe sized to its full content (see fitIframe),
|
| 587 |
-
// so a plain `position: fixed` overlay would anchor to the iframe's full
|
| 588 |
-
// height and land far below the fold. Instead we pin the overlay to the part
|
| 589 |
-
// of the iframe currently visible inside the parent viewport. srcdoc iframes
|
| 590 |
-
// are same-origin with the embedding document, so frameElement / parent are
|
| 591 |
-
// readable; everything is wrapped in try/catch and falls back to a fixed,
|
| 592 |
-
// viewport-centred overlay if that access is ever blocked.
|
| 593 |
-
function positionModalToView() {
|
| 594 |
-
const back = document.getElementById('modalBack');
|
| 595 |
-
try {
|
| 596 |
-
const fe = window.frameElement;
|
| 597 |
-
const pv = window.parent;
|
| 598 |
-
if (fe && pv) {
|
| 599 |
-
const rect = fe.getBoundingClientRect(); // iframe box in parent viewport
|
| 600 |
-
const docH = document.documentElement.scrollHeight;
|
| 601 |
-
const visTop = Math.max(0, -rect.top); // iframe-doc y at top of view
|
| 602 |
-
const visBottom = Math.min(docH, -rect.top + pv.innerHeight);
|
| 603 |
-
if (visBottom > visTop) {
|
| 604 |
-
back.style.position = 'absolute';
|
| 605 |
-
back.style.left = '0';
|
| 606 |
-
back.style.right = '0';
|
| 607 |
-
back.style.bottom = 'auto';
|
| 608 |
-
back.style.top = visTop + 'px';
|
| 609 |
-
back.style.height = (visBottom - visTop) + 'px';
|
| 610 |
-
return;
|
| 611 |
-
}
|
| 612 |
-
}
|
| 613 |
-
} catch (e) { /* cross-origin / sandboxed -> fixed fallback below */ }
|
| 614 |
-
back.style.position = 'fixed';
|
| 615 |
-
back.style.top = '0'; back.style.left = '0';
|
| 616 |
-
back.style.right = '0'; back.style.bottom = '0';
|
| 617 |
-
back.style.height = '';
|
| 618 |
-
}
|
| 619 |
-
|
| 620 |
-
let _modalSync = null;
|
| 621 |
-
function attachModalViewSync() {
|
| 622 |
-
try {
|
| 623 |
-
const pv = window.parent;
|
| 624 |
-
_modalSync = () => positionModalToView();
|
| 625 |
-
pv.addEventListener('scroll', _modalSync, { passive: true });
|
| 626 |
-
pv.addEventListener('resize', _modalSync);
|
| 627 |
-
} catch (e) { _modalSync = null; }
|
| 628 |
-
}
|
| 629 |
-
function detachModalViewSync() {
|
| 630 |
-
try {
|
| 631 |
-
const pv = window.parent;
|
| 632 |
-
if (_modalSync) {
|
| 633 |
-
pv.removeEventListener('scroll', _modalSync);
|
| 634 |
-
pv.removeEventListener('resize', _modalSync);
|
| 635 |
-
}
|
| 636 |
-
} catch (e) { /* ignore */ }
|
| 637 |
-
_modalSync = null;
|
| 638 |
-
}
|
| 639 |
-
|
| 640 |
// Pin the GT row exactly beneath the sticky column header.
|
| 641 |
function syncHeadHeight() {
|
| 642 |
const head = document.getElementById('gridHead');
|
| 643 |
if (head) document.documentElement.style.setProperty('--head-h', head.offsetHeight + 'px');
|
| 644 |
}
|
| 645 |
|
| 646 |
-
//
|
| 647 |
-
//
|
| 648 |
-
//
|
| 649 |
-
//
|
| 650 |
-
//
|
| 651 |
-
|
| 652 |
-
try {
|
| 653 |
-
const pv = window.parent;
|
| 654 |
-
if (pv && pv.innerHeight) {
|
| 655 |
-
const cap = Math.max(360, Math.round(pv.innerHeight * 0.78));
|
| 656 |
-
document.documentElement.style.setProperty('--gallery-max', cap + 'px');
|
| 657 |
-
}
|
| 658 |
-
} catch (e) { /* keep CSS fallback */ }
|
| 659 |
-
}
|
| 660 |
-
|
| 661 |
-
// Size the iframe to its content so the page is as compact as the (capped)
|
| 662 |
-
// gallery -- no oversized fixed box, no empty space when there are few rows.
|
| 663 |
-
// No-ops if frameElement is unreadable (the wrapper then keeps its CSS height).
|
| 664 |
function fitIframe() {
|
| 665 |
try {
|
| 666 |
const fe = window.frameElement;
|
|
@@ -669,11 +588,9 @@ function fitIframe() {
|
|
| 669 |
}
|
| 670 |
|
| 671 |
buildGallery();
|
| 672 |
-
capGallery();
|
| 673 |
fitIframe();
|
| 674 |
-
function relayout() { syncHeadHeight();
|
| 675 |
window.addEventListener('resize', relayout);
|
| 676 |
-
try { window.parent.addEventListener('resize', relayout); } catch (e) { /* ignore */ }
|
| 677 |
if (window.ResizeObserver) new ResizeObserver(fitIframe).observe(document.body);
|
| 678 |
if (document.fonts && document.fonts.ready) document.fonts.ready.then(relayout);
|
| 679 |
"""
|
|
|
|
| 250 |
.section-caption { margin: 0 0 16px; font-size: 12.5px; color: var(--ink-soft); line-height: 1.5; }
|
| 251 |
.section-caption b { color: var(--ink); font-weight: 600; }
|
| 252 |
|
| 253 |
+
.gallery { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); position: relative; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
.grid-head, .grow {
|
| 255 |
display: grid;
|
| 256 |
grid-template-columns: 52px minmax(200px, 1.3fr) 160px repeat(var(--ncol, 4), minmax(140px, 1fr));
|
|
|
|
| 559 |
document.getElementById('modalNote').innerHTML =
|
| 560 |
'CAD score for this sample: <b>' + cad + '</b>. The full per-sample report '
|
| 561 |
+ '(shape similarity, interface, topology + 3D view) opens from the report viewer.';
|
| 562 |
+
document.getElementById('modalBack').classList.add('show');
|
|
|
|
|
|
|
|
|
|
| 563 |
}
|
| 564 |
function closeModal() {
|
| 565 |
document.getElementById('modalBack').classList.remove('show');
|
|
|
|
| 566 |
}
|
| 567 |
document.getElementById('modalClose').onclick = closeModal;
|
| 568 |
document.getElementById('modalBack').onclick = (e) => { if (e.target.id === 'modalBack') closeModal(); };
|
| 569 |
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); });
|
| 570 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 571 |
// Pin the GT row exactly beneath the sticky column header.
|
| 572 |
function syncHeadHeight() {
|
| 573 |
const head = document.getElementById('gridHead');
|
| 574 |
if (head) document.documentElement.style.setProperty('--head-h', head.offsetHeight + 'px');
|
| 575 |
}
|
| 576 |
|
| 577 |
+
// Size the iframe to its content, but only up to the `max-height` the host sets
|
| 578 |
+
// on the iframe element (see app.py). With few rows the iframe shrinks to the
|
| 579 |
+
// content (compact, no empty box); once the content is taller than the cap the
|
| 580 |
+
// iframe stays at the cap and its own body scrolls -- which is what lets the
|
| 581 |
+
// sticky column header + ground-truth row stay locked at the top while the
|
| 582 |
+
// submission rows scroll under them. No-ops if frameElement is unreadable.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 583 |
function fitIframe() {
|
| 584 |
try {
|
| 585 |
const fe = window.frameElement;
|
|
|
|
| 588 |
}
|
| 589 |
|
| 590 |
buildGallery();
|
|
|
|
| 591 |
fitIframe();
|
| 592 |
+
function relayout() { syncHeadHeight(); fitIframe(); }
|
| 593 |
window.addEventListener('resize', relayout);
|
|
|
|
| 594 |
if (window.ResizeObserver) new ResizeObserver(fitIframe).observe(document.body);
|
| 595 |
if (document.fonts && document.fonts.ready) document.fonts.ready.then(relayout);
|
| 596 |
"""
|