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