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: 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>`` Mounted as a direct child of ``<body>`` via a component-iframe so
footer into the page body. It looked nice (always-visible bar at it lives outside every Streamlit container — required because
the viewport bottom) but the click was a FULL-page navigation, ``.stApp`` carries ``zoom: 0.85`` and Streamlit's content
which on the user's Streamlit build doesn't preserve columns add padding/positioning context that would otherwise
``st.session_state`` — uploads disappeared after every Back to distort or clip the bar.
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.
So the sticky footer is retired. Each tool page renders the Close is a full-page ``<a href="./close">`` link to the Close
real Streamlit-button version of ``back_to_home_link`` near the page, which runs ``shutdown_app`` on render. State loss is fine
top AND near the bottom (the two-instance pattern from before here — the process is terminating. (This was the reason the
the sticky-footer attempt), so the button is visible at both Back-to-Home variant of this footer was retired; that case
ends of the page without ever risking state loss. needed a soft nav widget. Close does not.)
Kept as a callable so existing tool-page imports + call sites Help is pure UI: clicking toggles a small overlay panel
don't have to be touched in this commit — they all resolve to containing the version and support email — no navigation, so
this no-op. 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: def _render_sticky_footer_DISABLED() -> None:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -171,5 +171,13 @@
"close_title": "Close", "close_title": "Close",
"section_close": "Close", "section_close": "Close",
"back_to_home": "← Back to Home" "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", "close_title": "Cerrar",
"section_close": "Cerrar", "section_close": "Cerrar",
"back_to_home": "← Volver al inicio" "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"
} }
} }