feat(footer): slim sticky footer with Close + Help, drop bottom Back-to-Home

The duplicate full-width Back-to-Home button at the bottom of every
tool page was reading as a "huge footer." Replace it with a real
slim sticky footer holding two controls:

- Close: <a href="./close"> to the Close page (which shuts down).
  Full-page nav is fine here — the process is terminating, so the
  session-state-loss concern that retired the previous sticky
  footer doesn't apply.
- Help: JS-toggled popover showing version + support@datatools.app.
  No navigation, no state loss.

Top-of-page Back-to-Home stays (uses st.switch_page, preserves
state). Add footer.* i18n keys for en + es.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 14:56:02 +00:00
parent 65c85107b6
commit d1b9f642e2
12 changed files with 221 additions and 28 deletions

View File

@@ -518,29 +518,215 @@ html_download_button = local_download_button
def render_sticky_footer() -> None:
"""No-op shim.
"""Slim fixed-position footer with Close and Help controls.
The previous implementation injected a CSS-positioned ``<a href>``
footer into the page body. It looked nice (always-visible bar at
the viewport bottom) but the click was a FULL-page navigation,
which on the user's Streamlit build doesn't preserve
``st.session_state`` — uploads disappeared after every Back to
Home click. Soft navigation via ``st.switch_page`` requires a
real Streamlit button widget, which we can't reliably
CSS-position to the viewport bottom because Streamlit owns the
widget's DOM container and remounts it on every rerun.
Mounted as a direct child of ``<body>`` via a component-iframe so
it lives outside every Streamlit container — required because
``.stApp`` carries ``zoom: 0.85`` and Streamlit's content
columns add padding/positioning context that would otherwise
distort or clip the bar.
So the sticky footer is retired. Each tool page renders the
real Streamlit-button version of ``back_to_home_link`` near the
top AND near the bottom (the two-instance pattern from before
the sticky-footer attempt), so the button is visible at both
ends of the page without ever risking state loss.
Close is a full-page ``<a href="./close">`` link to the Close
page, which runs ``shutdown_app`` on render. State loss is fine
here — the process is terminating. (This was the reason the
Back-to-Home variant of this footer was retired; that case
needed a soft nav widget. Close does not.)
Kept as a callable so existing tool-page imports + call sites
don't have to be touched in this commit — they all resolve to
this no-op.
Help is pure UI: clicking toggles a small overlay panel
containing the version and support email — no navigation, so
no state loss.
"""
return
import html as _html
import json as _json
from src import __version__
close_label = _html.escape(_t("footer.close"))
help_label = _html.escape(_t("footer.help"))
help_title = _html.escape(_t("footer.help_title"))
help_version = _html.escape(
_t("footer.help_version").format(version=__version__)
)
support_email = "support@datatools.app"
help_support = _html.escape(
_t("footer.help_support").format(email=support_email)
)
help_dismiss = _html.escape(_t("footer.help_dismiss"))
st.markdown(
"""
<style>
[data-testid="stAppViewBlockContainer"] {
padding-bottom: 3rem !important;
}
#datatools-sticky-footer {
position: fixed !important;
bottom: 0 !important;
left: 0 !important;
right: 0 !important;
background: rgba(255, 255, 255, 0.97) !important;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border-top: 1px solid rgba(49, 51, 63, 0.2) !important;
padding: 0.25rem 0.75rem !important;
z-index: 2147483646 !important;
display: flex !important;
align-items: center !important;
justify-content: flex-end !important;
gap: 0.4rem !important;
font-family: system-ui, -apple-system, sans-serif !important;
box-sizing: border-box !important;
min-height: 32px !important;
}
#datatools-sticky-footer .datatools-footer-btn {
display: inline-block !important;
color: rgb(38, 39, 48) !important;
background: rgb(240, 242, 246) !important;
text-decoration: none !important;
padding: 0.2rem 0.6rem !important;
border-radius: 0.35rem !important;
border: 1px solid rgba(49, 51, 63, 0.22) !important;
font-size: 12px !important;
font-weight: 500 !important;
line-height: 1.3 !important;
cursor: pointer !important;
transition: background 0.12s ease, border-color 0.12s ease;
}
#datatools-sticky-footer .datatools-footer-btn:hover {
background: rgb(225, 228, 235) !important;
border-color: rgba(49, 51, 63, 0.35) !important;
}
#datatools-sticky-footer .datatools-footer-btn.close {
color: rgb(176, 0, 32) !important;
border-color: rgba(176, 0, 32, 0.35) !important;
}
#datatools-sticky-footer .datatools-footer-btn.close:hover {
background: rgba(176, 0, 32, 0.08) !important;
}
#datatools-help-popover {
position: fixed !important;
right: 0.75rem !important;
bottom: 44px !important;
background: white !important;
border: 1px solid rgba(49, 51, 63, 0.25) !important;
border-radius: 0.5rem !important;
box-shadow: 0 8px 20px rgba(0,0,0,0.12) !important;
padding: 0.75rem 0.9rem !important;
z-index: 2147483647 !important;
font-family: system-ui, -apple-system, sans-serif !important;
font-size: 13px !important;
color: rgb(38, 39, 48) !important;
min-width: 220px !important;
max-width: 320px !important;
}
#datatools-help-popover[hidden] { display: none !important; }
#datatools-help-popover .dt-help-title {
font-weight: 600 !important;
margin-bottom: 0.35rem !important;
}
#datatools-help-popover .dt-help-row {
margin: 0.15rem 0 !important;
line-height: 1.4 !important;
}
#datatools-help-popover .dt-help-row a {
color: rgb(0, 102, 204) !important;
text-decoration: none !important;
}
#datatools-help-popover .dt-help-row a:hover {
text-decoration: underline !important;
}
#datatools-help-popover .dt-help-dismiss {
margin-top: 0.5rem !important;
font-size: 11px !important;
color: rgb(90, 95, 110) !important;
background: none !important;
border: none !important;
cursor: pointer !important;
padding: 0 !important;
}
#datatools-help-popover .dt-help-dismiss:hover {
color: rgb(38, 39, 48) !important;
}
</style>
""",
unsafe_allow_html=True,
)
from streamlit.components.v1 import html as _components_html
_components_html(
f"""
<script>
(function () {{
var labels = {_json.dumps({
"close": close_label,
"help": help_label,
"help_title": help_title,
"help_version": help_version,
"help_support": help_support,
"help_dismiss": help_dismiss,
"support_email": support_email,
})};
function build(doc) {{
var prev = doc.getElementById('datatools-sticky-footer');
if (prev) prev.remove();
var prevPop = doc.getElementById('datatools-help-popover');
if (prevPop) prevPop.remove();
var div = doc.createElement('div');
div.id = 'datatools-sticky-footer';
var helpBtn = doc.createElement('button');
helpBtn.type = 'button';
helpBtn.className = 'datatools-footer-btn help';
helpBtn.textContent = labels.help;
var closeLink = doc.createElement('a');
closeLink.className = 'datatools-footer-btn close';
closeLink.href = './close';
closeLink.target = '_self';
closeLink.textContent = labels.close;
div.appendChild(helpBtn);
div.appendChild(closeLink);
var pop = doc.createElement('div');
pop.id = 'datatools-help-popover';
pop.hidden = true;
pop.innerHTML =
'<div class="dt-help-title">' + labels.help_title + '</div>' +
'<div class="dt-help-row">' + labels.help_version + '</div>' +
'<div class="dt-help-row">' + labels.help_support.replace(
labels.support_email,
'<a href="mailto:' + labels.support_email + '">' + labels.support_email + '</a>'
) + '</div>' +
'<button type="button" class="dt-help-dismiss">' + labels.help_dismiss + '</button>';
helpBtn.addEventListener('click', function (e) {{
e.preventDefault();
pop.hidden = !pop.hidden;
}});
pop.querySelector('.dt-help-dismiss').addEventListener('click', function () {{
pop.hidden = true;
}});
doc.addEventListener('click', function (e) {{
if (pop.hidden) return;
if (pop.contains(e.target) || helpBtn.contains(e.target)) return;
pop.hidden = true;
}});
doc.body.appendChild(div);
doc.body.appendChild(pop);
}}
try {{
build(window.parent.document);
}} catch (e) {{
build(document);
}}
}})();
</script>
""",
height=0,
)
def _render_sticky_footer_DISABLED() -> None:

View File

@@ -406,7 +406,6 @@ else:
# Footer
# ---------------------------------------------------------------------------
back_to_home_link(key="_back_to_home_link_bottom")
st.divider()
st.caption(

View File

@@ -383,7 +383,6 @@ with dl_c:
mime="application/json",
)
back_to_home_link(key="_back_to_home_link_bottom")
st.divider()
st.caption("Runs locally. Your data never leaves this computer. | DataTools v3.0")

View File

@@ -653,7 +653,6 @@ with dl_c:
mime="application/json",
)
back_to_home_link(key="_back_to_home_link_bottom")
st.divider()
st.caption("Runs locally. Your data never leaves this computer. | DataTools v3.0")

View File

@@ -416,7 +416,6 @@ with dl_c:
mime="application/json",
)
back_to_home_link(key="_back_to_home_link_bottom")
st.divider()
st.caption("Runs locally. Your data never leaves this computer. | DataTools v3.0")

View File

@@ -460,7 +460,6 @@ with dl_c:
mime="application/json",
)
back_to_home_link(key="_back_to_home_link_bottom")
st.divider()
st.caption("Runs locally. Your data never leaves this computer. | DataTools v3.0")

View File

@@ -101,7 +101,6 @@ st.button("Detect Outliers", type="primary", use_container_width=True, disabled=
# Footer
# ---------------------------------------------------------------------------
back_to_home_link(key="_back_to_home_link_bottom")
st.divider()
st.caption(

View File

@@ -99,7 +99,6 @@ st.button("Merge Files", type="primary", use_container_width=True, disabled=True
# Footer
# ---------------------------------------------------------------------------
back_to_home_link(key="_back_to_home_link_bottom")
st.divider()
st.caption(

View File

@@ -106,7 +106,6 @@ st.button("Validate & Generate Report", type="primary", use_container_width=True
# Footer
# ---------------------------------------------------------------------------
back_to_home_link(key="_back_to_home_link_bottom")
st.divider()
st.caption(

View File

@@ -418,7 +418,6 @@ with dl_c:
mime="application/json",
)
back_to_home_link(key="_back_to_home_link_bottom")
st.divider()
st.caption("Runs locally. Your data never leaves this computer. | DataTools v3.0")

View File

@@ -171,5 +171,13 @@
"close_title": "Close",
"section_close": "Close",
"back_to_home": "← Back to Home"
},
"footer": {
"close": "Close",
"help": "Help",
"help_title": "DataTools",
"help_version": "Version {version}",
"help_support": "Support: {email}",
"help_dismiss": "Close"
}
}

View File

@@ -171,5 +171,13 @@
"close_title": "Cerrar",
"section_close": "Cerrar",
"back_to_home": "← Volver al inicio"
},
"footer": {
"close": "Cerrar",
"help": "Ayuda",
"help_title": "DataTools",
"help_version": "Versión {version}",
"help_support": "Soporte: {email}",
"help_dismiss": "Cerrar"
}
}