From 229e1afd45514f378e3ede815473a9be612425c3 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 17 May 2026 00:47:44 +0000 Subject: [PATCH] fix(footer): mount Back-to-Home outside Streamlit's container tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported: the sticky footer rendered, but the Back to Home button inside it wasn't visible. Likely cause: ``st.markdown`` inserts the footer div inside Streamlit's content tree, which sits under ``.stApp { zoom: 0.85 }`` (our compact scaler) and several nested padding/positioning contexts. Streamlit's own ```` styling rules can also colour-collide with our anchor. Switch the mount strategy. Two passes: 1. CSS rules go to the parent document via ``st.markdown`` as before, but every property carries ``!important`` and the selectors key on ``#datatools-sticky-footer`` (id, not class) plus a dedicated ``.datatools-sticky-footer-link`` class on the anchor — so Streamlit's default ```` styles can't override colour or padding. ``z-index: 2147483646`` keeps the footer above anything else in the page. 2. The footer DOM node itself is created by a script inside a zero-height ``streamlit.components.v1.html`` iframe. The script does ``window.parent.document.body.appendChild(...)`` so the div lives as a direct child of ```` — outside ``.stApp``, outside every Streamlit container, free of every parent's ``zoom`` / ``transform`` / ``overflow`` rules. If the cross-frame access ever fails (Streamlit sandbox config change), the script falls through to appending inside the iframe's own document — degraded but still visible. Each rerun replaces any prior ``#datatools-sticky-footer`` so we don't accumulate stacked footers on every script pass. 2220 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/gui/components/_legacy.py | 162 +++++++++++++++++++++------------- 1 file changed, 99 insertions(+), 63 deletions(-) diff --git a/src/gui/components/_legacy.py b/src/gui/components/_legacy.py index 3516746..e31f483 100644 --- a/src/gui/components/_legacy.py +++ b/src/gui/components/_legacy.py @@ -495,83 +495,119 @@ def render_sticky_footer() -> None: """Render a slim fixed-position footer at the bottom of the viewport. Contains a "Back to Home" link that's always visible regardless of - scroll position. Replaces the previous top + bottom-of-page - ``back_to_home_link`` buttons: a single sticky bar is more - discoverable, doesn't scroll out of view, and visually reads as a - "non-movable footer" (border-top + slim padding) rather than - just-another-button-at-the-bottom. + scroll position. The footer is mounted as a direct child of + ```` via a component-iframe script so it lives OUTSIDE every + Streamlit container — that matters because ``.stApp`` carries + ``zoom: 0.85`` (our compact-layout scaler) and Streamlit's content + columns add their own padding/positioning context that previously + swallowed the in-place ``st.markdown`` footer. - Implementation notes: + The implementation is two-pass: - - Uses a styled ```` anchor rather than an - ``st.button`` because Streamlit widgets can't be CSS-positioned - reliably (their DOM container is owned by Streamlit's renderer - and gets re-mounted on every rerun). A real anchor sits anywhere - we want and triggers Streamlit's URL routing to the home page. - - ``href="home"`` is a relative path so it works behind a reverse - proxy / non-root mount. - - We bump ``padding-bottom`` on the main container so the last - widget isn't hidden behind the fixed bar. - - Theme: ``rgba(255,255,255,0.96)`` background with a thin - semi-transparent border-top. The blur backdrop keeps content - slightly visible underneath. Works against both light and dark - Streamlit themes — explicit colours, not theme variables, - because Streamlit's CSS variable surface is unstable across - minor versions. + 1. ``st.markdown`` injects the CSS rules into the parent document. + Class-targeted, so the rules apply once the footer DOM node + exists regardless of where it lives. + 2. ``streamlit.components.v1.html`` renders a zero-height iframe + whose JS reaches ``window.parent.document`` and creates / moves + a ``#datatools-sticky-footer`` div directly under ````. + This bypasses every Streamlit container. + + The anchor uses ``href="home"`` (relative) so Streamlit's URL + routing resolves it to the Home page and the link works correctly + behind a reverse proxy or non-root mount. """ import html as _html - label = _html.escape(_t("nav.back_to_home")) + import json as _json + label_raw = _t("nav.back_to_home") + label_esc = _html.escape(label_raw) + + # CSS rules live in the parent document. Class selector so a + # re-rendered/relocated footer div picks them up automatically. st.markdown( - f""" + """ - """, unsafe_allow_html=True, ) + # Move the footer to directly via component iframe. The + # iframe carries allow-same-origin so window.parent.document is + # reachable; if a sandbox config ever blocks that we fall back to + # rendering inside the iframe itself (still visible, just sized + # to the iframe rather than the viewport). + from streamlit.components.v1 import html as _components_html + _components_html( + f""" + +""", + height=0, + ) + def back_to_home_link(*, key: str = "_back_to_home_link") -> None: """Render a "← Back to Home" affordance on a tool page.