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:
@@ -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:
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user