feat(ui): page header + files card + action bar + findings cards (mockup 2)
Closes the remaining gaps between the live home page and the
``datatools_layout_redesign2.html`` mockup. Four pieces land
together because they all consume the same new CSS scaffold:
1. Page header (§page-header)
``st.title`` + ``st.caption`` + ``st.divider`` collapse into one
flex header: h1 + body subtitle on the left, ``Runs 100% locally``
privacy pill (success-fill + lock SVG) on the right, soft border
below. The "Runs 100% locally" phrase moved out of
``home.caption`` into the new ``home.privacy_pill`` i18n key
(en + es).
2. Files card (§files-card)
The "Imported files" list is now a single bordered card with a
section head (count + KB total on the right, mockup §section-head).
Each row renders a 28px accent-fill chip carrying the inline
document SVG, a mono filename, a right-aligned mono size, and a
compact ``✕`` button. The word-button ``Remove`` is gone —
replaced by an icon-only tertiary button styled via a new CSS
rule that goes transparent → danger-fill on hover (mockup
§file-remove).
3. Action bar (§action-bar)
Three buttons in one row: ``Run analysis`` (primary ink), a new
disabled ``Export report`` (secondary; coming soon, tooltip), and
``Clear results``. New i18n key ``upload.export_report``.
4. Findings — per-file group cards (§finding-group)
``render_findings_panel`` rewritten end-to-end. Output is now:
• A head row (``dt-finding-group-head``) bleeding to the card
edges: worst-severity dot · mono filename · count pills
enumerating non-zero severities (e.g. ``2 info`` blue,
``1 warning`` amber, ``1 error`` rose).
• A flat list of finding rows sorted error → warn → info.
Each row: tinted Material-icon chip + title (description
with optional ``<code>`` column chip) + mono meta line
(rows affected, samples captured) + tertiary
``Open <Tool> →`` action button that ``st.switch_page``s
to the relevant tool.
The previous tool-grouped expander stack is dropped — the new
layout is denser and matches the mockup's single-card-per-file
structure.
``_render_one_finding`` (the old per-finding helper that emitted
markdown lines + sample tables) remains in the file but is no
longer called from the home flow; left in place for any other
surface that still depends on the markdown style.
The "no issues" success state renders a green dot + mono
filename + ``no issues`` success pill in the same card chrome,
so empty-result files visually match the rest of the panel
rather than getting a generic ``st.success`` callout.
CSS additions (``_DESIGN_TOKENS_CSS``):
``.dt-page-header / .dt-page-subtitle / .dt-privacy-pill``
``.dt-files-section-head / .dt-section-meta``
``.dt-file-row / .dt-file-icon-chip / .dt-file-name / .dt-file-size``
``.dt-finding-group-head / .dt-severity-dot{.warn,.info,.error,.success}``
``.dt-group-filename / .dt-group-counts``
``.dt-count-pill{.warn,.info,.error,.success}``
``.dt-finding-row / .dt-finding-icon{.warn,.info,.error}``
``.dt-finding-title / .dt-finding-meta``
Tertiary button rule (transparent → danger-fill on hover) for
the X button and the ``Open Tool →`` row action.
theme.py:
Explicitly loads Material Symbols Outlined alongside Geist —
the severity-chip ligatures (``info`` / ``warning`` / ``error``)
need the font present even when no ``:material/`` token has been
emitted yet on the page. Tightened ``.dt-finding-icon .dt-mui``
selector with ``[data-testid="stMarkdownContainer"]``-scoped
variant so the Material font wins over theme.py's base
``var(--font-sans) !important`` on markdown descendants.
Leading section-heading emojis stripped from i18n
(``upload.heading``) for parity with the mockup's clean ``Files``
/ ``Findings`` h2s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
125
src/gui/_home.py
125
src/gui/_home.py
@@ -161,9 +161,30 @@ def _home_page() -> None:
|
||||
hide_streamlit_chrome()
|
||||
render_sticky_footer()
|
||||
|
||||
st.title(t("home.title"))
|
||||
st.caption(t("home.caption"))
|
||||
st.divider()
|
||||
import html as _html
|
||||
# Page header — h1 + body subtitle on the left, privacy pill on
|
||||
# the right (mockup §page-header). Rendered as a single HTML block
|
||||
# so the title/subtitle/pill share one flex row; ``st.title`` +
|
||||
# ``st.caption`` + ``st.divider`` would stack vertically and lose
|
||||
# the right-aligned pill. Bottom border replaces the explicit
|
||||
# ``st.divider`` that used to sit below the caption.
|
||||
privacy_label = _html.escape(t("home.privacy_pill"))
|
||||
st.markdown(
|
||||
'<header class="dt-page-header">'
|
||||
'<div>'
|
||||
f'<h1>{_html.escape(t("home.title"))}</h1>'
|
||||
f'<p class="dt-page-subtitle">{_html.escape(t("home.caption"))}</p>'
|
||||
'</div>'
|
||||
'<span class="dt-privacy-pill">'
|
||||
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">'
|
||||
'<rect x="4" y="11" width="16" height="10" rx="2"/>'
|
||||
'<path d="M8 11V7a4 4 0 018 0v4"/>'
|
||||
'</svg>'
|
||||
f'{privacy_label}'
|
||||
'</span>'
|
||||
'</header>',
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
|
||||
st.markdown(f"### {t('upload.heading')}")
|
||||
st.caption(t("upload.intro_multi"))
|
||||
@@ -211,27 +232,59 @@ def _home_page() -> None:
|
||||
# interleaving widget-key updates with state changes.
|
||||
if home_uploads:
|
||||
import hashlib
|
||||
st.markdown("**Imported files**")
|
||||
# Files-card head: count + total size (mockup §section-head).
|
||||
total_bytes = sum(meta["size"] for meta in home_uploads.values())
|
||||
if total_bytes >= 1024:
|
||||
total_label = f"{total_bytes / 1024:.1f} KB"
|
||||
else:
|
||||
total_label = f"{total_bytes:,} B"
|
||||
n_files = len(home_uploads)
|
||||
files_word = "file" if n_files == 1 else "files"
|
||||
st.markdown(
|
||||
'<div class="dt-files-section-head">'
|
||||
f'<h3>Imported files</h3>'
|
||||
f'<span class="dt-section-meta">{n_files} {files_word}'
|
||||
f' · {_html.escape(total_label)} total</span>'
|
||||
'</div>',
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
|
||||
to_remove: str | None = None
|
||||
for name in list(home_uploads.keys()):
|
||||
digest = hashlib.sha1(
|
||||
name.encode("utf-8"), usedforsecurity=False,
|
||||
).hexdigest()[:10]
|
||||
col_file, col_remove = st.columns([8, 1])
|
||||
col_file.markdown(
|
||||
f"📄 `{name}` "
|
||||
f"<span style='opacity:0.6'>"
|
||||
f"({home_uploads[name]['size']:,} bytes)</span>",
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
if col_remove.button(
|
||||
"Remove",
|
||||
key=f"_home_remove_{digest}",
|
||||
help=f"Remove {name}",
|
||||
type="secondary",
|
||||
width="stretch",
|
||||
):
|
||||
to_remove = name
|
||||
# Document-icon SVG used by every file row's icon chip
|
||||
# (mockup §file-row .file-icon).
|
||||
_DOC_SVG = (
|
||||
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">'
|
||||
'<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/>'
|
||||
'<path d="M14 2v6h6"/>'
|
||||
'</svg>'
|
||||
)
|
||||
with st.container(border=True):
|
||||
for name in list(home_uploads.keys()):
|
||||
digest = hashlib.sha1(
|
||||
name.encode("utf-8"), usedforsecurity=False,
|
||||
).hexdigest()[:10]
|
||||
col_name, col_size, col_x = st.columns([8, 1.6, 0.55])
|
||||
col_name.markdown(
|
||||
'<div class="dt-file-row">'
|
||||
f'<span class="dt-file-icon-chip">{_DOC_SVG}</span>'
|
||||
f'<span class="dt-file-name">{_html.escape(name)}</span>'
|
||||
'</div>',
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
col_size.markdown(
|
||||
f'<div style="text-align:right;">'
|
||||
f'<span class="dt-file-size">'
|
||||
f'{home_uploads[name]["size"]:,} B'
|
||||
'</span></div>',
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
if col_x.button(
|
||||
"✕",
|
||||
key=f"_home_remove_{digest}",
|
||||
help=f"Remove {name}",
|
||||
type="tertiary",
|
||||
):
|
||||
to_remove = name
|
||||
|
||||
if to_remove is not None:
|
||||
from src.audit import log_event
|
||||
@@ -288,7 +341,9 @@ def _home_page() -> None:
|
||||
|
||||
pending = [name for name in home_uploads if name not in findings_by_file]
|
||||
|
||||
col_run, col_clear, _ = st.columns([1, 1, 4])
|
||||
# Action bar — Run / Export report (disabled, coming soon) /
|
||||
# Clear results, matching the mockup §action-bar.
|
||||
col_run, col_export, col_clear, _ = st.columns([1, 1, 1, 3])
|
||||
with col_run:
|
||||
run_clicked = st.button(
|
||||
t("upload.run_button"),
|
||||
@@ -297,6 +352,14 @@ def _home_page() -> None:
|
||||
disabled=not pending,
|
||||
width="stretch",
|
||||
)
|
||||
with col_export:
|
||||
st.button(
|
||||
t("upload.export_report"),
|
||||
key="home_export_report",
|
||||
disabled=True,
|
||||
help="Coming soon",
|
||||
width="stretch",
|
||||
)
|
||||
with col_clear:
|
||||
clear_clicked = st.button(
|
||||
t("upload.clear_results"),
|
||||
@@ -340,11 +403,19 @@ def _home_page() -> None:
|
||||
findings = findings_by_file[name]
|
||||
with st.container(border=True):
|
||||
if not findings:
|
||||
st.markdown(f"### 📄 {name}")
|
||||
st.success(t("findings.none"))
|
||||
st.markdown(
|
||||
'<div class="dt-finding-group-head">'
|
||||
'<span class="dt-severity-dot success"></span>'
|
||||
f'<span class="dt-group-filename">{_html.escape(name)}</span>'
|
||||
'<div class="dt-group-counts">'
|
||||
'<span class="dt-count-pill success">no issues</span>'
|
||||
'</div>'
|
||||
'</div>',
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
else:
|
||||
render_findings_panel(
|
||||
findings,
|
||||
header=f"📄 {name}",
|
||||
header=name,
|
||||
key_namespace=name,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user