feat(ui): Material icons in sidebar + stats overview on home

Two pieces of the mockup 2 layout that hadn't landed yet:

1. Sidebar nav icons — emoji glyphs (🧹 ✂️ 🔍 …) swapped for
   Streamlit's ``:material/<name>:`` syntax, picking the outline
   Material Symbol that best matches each mockup SVG:

       Home               → :material/home:
       Fix Missing Values → :material/help_outline:
       Find Unusual Vals  → :material/insights:
       Clean Text         → :material/text_format:
       Standardize Fmts   → :material/format_list_bulleted:
       Find Duplicates    → :material/search:
       Quality Check      → :material/check_circle:
       Map Columns        → :material/view_column:
       Combine Files      → :material/account_tree:
       Auto Workflows     → :material/auto_awesome:
       Activate           → :material/key:
       Close              → :material/close:

   Streamlit injects the icon name as a literal ligature inside a
   first-child ``<span>`` of the nav anchor, expected to render
   through the Material Symbols font. theme.py's base rule was
   forcing Geist on every span under ``stSidebarNav``, turning the
   ligatures back into plain text labels — added a structural
   exception that targets ``[data-testid="stSidebarNavLink"] >
   span:first-child`` (and any descendant), restoring the Material
   font family, neutralizing the inherited ``ss01/cv01/cv11``
   feature settings, and sizing to 18px.

   Also stripped the leading emojis from every page title in the
   en/es i18n packs (``home.title``, ``close_page.title``,
   ``activation.title``, ``tools.*.page_title``) — the icons live
   in the sidebar now, the page H1 no longer needs to carry one.

2. Stats overview on home — new ``_render_stats_overview`` in
   _home.py emits a 4-card grid above the per-file findings panels:
   Files analyzed, Total findings, Warnings (severity ``warn`` ∪
   ``error``), Info (severity ``info``). Card layout follows the
   mockup §stats verbatim — Geist 28px / 600 / -0.03em for the
   numeric value (the "Display number" row in spec §4), tiny
   uppercase tracked label, paper-surface card with the standard
   warm border + faint shadow. The Warnings / Info cards tint the
   number with ``--warn`` / ``--info`` when the count is non-zero.

CSS for ``.dt-stats / .dt-stat / .dt-stat-label / .dt-stat-value /
.dt-stat-unit`` added to ``_DESIGN_TOKENS_CSS``; falls to a
2-column grid below 900px viewport, matching the mockup's media
query.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 00:31:40 +00:00
parent 2501119ac2
commit da7d86f457
7 changed files with 174 additions and 36 deletions

View File

@@ -36,6 +36,61 @@ class _StashedUpload:
return self._data
def _render_stats_overview(findings_by_file: dict) -> None:
"""4-card grid above the per-file findings — summarizes the run.
Card layout follows ``datatools_layout_redesign2.html`` §stats:
Files analyzed, Total findings, Warnings (severity ``warn``
``error``), Info (severity ``info``). The warn + info cards are
tinted via ``.is-warn`` / ``.is-info`` modifiers that read the
severity colors theme.py declares.
"""
import html as _html
n_files = len(findings_by_file)
all_findings = [f for fs in findings_by_file.values() for f in fs]
n_total = len(all_findings)
# Mockup groups errors with warnings on the "to review" card —
# both demand the user act. ``info`` is the lower-priority pile.
n_warn = sum(1 for f in all_findings if f.severity in ("warn", "error"))
n_info = sum(1 for f in all_findings if f.severity == "info")
def _card(label: str, value: int, unit: str = "", kind: str = "") -> str:
cls = "dt-stat" + (f" {kind}" if kind else "")
unit_html = (
f'<span class="dt-stat-unit">{_html.escape(unit)}</span>'
if unit else ""
)
return (
f'<div class="{cls}">'
f'<div class="dt-stat-label">{_html.escape(label)}</div>'
f'<div class="dt-stat-value">{value}{unit_html}</div>'
f"</div>"
)
cards = (
_card("Files analyzed", n_files)
+ _card("Total findings", n_total)
+ _card(
"Warnings",
n_warn,
unit="to review" if n_warn else "",
kind="is-warn" if n_warn else "",
)
+ _card(
"Info",
n_info,
unit="suggestions" if n_info else "",
kind="is-info" if n_info else "",
)
)
st.markdown(
f'<div class="dt-stats">{cards}</div>',
unsafe_allow_html=True,
)
def _sync_uploader_to_home_uploads() -> None:
"""``on_change`` callback for the home-page file_uploader.
@@ -272,6 +327,11 @@ def _home_page() -> None:
if findings_by_file:
st.divider()
# Overview row before drilling into per-file detail. Mockup
# layout (datatools_layout_redesign2.html §stats) puts a
# 4-card summary above the findings panels so the user can
# eyeball the run before expanding any one file.
_render_stats_overview(findings_by_file)
# Preserve the upload-stash order so the user sees results in
# the same order they appear in the file list above.
for name in home_uploads:

View File

@@ -84,7 +84,7 @@ def _build_navigation() -> dict[str, list]:
home = st.Page(
_home_page,
title=_t("nav.home_page_title") or "Home",
icon="🧹",
icon=":material/home:",
default=True,
url_path="home",
)
@@ -99,13 +99,13 @@ def _build_navigation() -> dict[str, list]:
activate = st.Page(
"pages/_Activate.py",
title=_t("nav.activate_title") or "Activate",
icon="🔑",
icon=":material/key:",
url_path="activate",
)
close = st.Page(
"pages/99_Close.py",
title=_t("nav.close_title") or "Close",
icon="🛑",
icon=":material/close:",
url_path="close",
)

View File

@@ -414,6 +414,60 @@ div[data-testid="stContainer"][data-border="true"] {
border: 1px solid var(--border) !important;
overflow: hidden !important;
}
/* ---------- Stats overview ---------- */
/* 4-card grid shown above the per-file findings on the home page,
summarizing the most recent analysis run. Numeric values use the
"Display number" row from geist_spec.md §4 — Geist 28px / 600 /
-0.03em — and the severity-tinted variants pick up ``--warn`` /
``--info`` from theme.py. */
.dt-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin: 8px 0 20px;
}
.dt-stat {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 16px 18px;
box-shadow: 0 1px 2px rgba(28,25,23,0.03);
}
.dt-stat-label {
font-size: 11.5px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--ink-tertiary);
font-weight: 500;
margin-bottom: 6px;
line-height: 1.4;
}
.dt-stat-value {
font-family: var(--font-sans);
font-size: 28px;
font-weight: 600;
letter-spacing: -0.03em;
line-height: 1;
color: var(--ink);
display: flex;
align-items: baseline;
gap: 6px;
}
.dt-stat-unit {
font-family: var(--font-sans);
font-size: 12px;
font-weight: 400;
color: var(--ink-tertiary);
letter-spacing: 0;
}
.dt-stat.is-warn .dt-stat-value { color: var(--warn); }
.dt-stat.is-info .dt-stat-value { color: var(--info); }
.dt-stat.is-success .dt-stat-value { color: var(--success); }
@media (max-width: 900px) {
.dt-stats { grid-template-columns: repeat(2, 1fr); }
}
</style>
"""

View File

@@ -173,6 +173,30 @@ _CSS = f"""
font-size: 0.92em;
font-feature-settings: "ss02";
}}
/* Material icons in the sidebar nav. Streamlit's ``:material/<name>:``
syntax injects a first-child ``<span>`` whose text is the icon's
Material Symbols ligature (e.g. ``home``), expected to be rendered
by the Material Symbols font. Our base rule above forces
``var(--font-sans)`` on every span inside ``stSidebarNav``, which
turns those spans into literal text labels. Override the icon
span back to the Material font here. Targeting by ``:first-child``
of the nav-link anchor is stable across Streamlit versions —
emotion-class hashes are not. */
[data-testid="stSidebarNavLink"] > span:first-child,
[data-testid="stSidebarNavLink"] > span:first-child * {{
font-family: "Material Symbols Outlined", "Material Symbols Rounded",
"Material Icons" !important;
font-feature-settings: normal !important;
font-weight: 400 !important;
font-size: 18px !important;
line-height: 1 !important;
color: var(--ink-secondary);
}}
[data-testid="stSidebarNavLink"][aria-current="page"] > span:first-child,
[data-testid="stSidebarNavLink"][aria-current="page"] > span:first-child * {{
color: var(--ink) !important;
}}
</style>
"""

View File

@@ -49,7 +49,7 @@ class Tool:
TOOLS: list[Tool] = [
Tool(
tool_id="04_missing_handler",
icon="🕳️",
icon=":material/help_outline:",
name="Fix Missing Values",
description=(
"Detect disguised nulls, missingness analysis, and imputation strategies."
@@ -60,7 +60,7 @@ TOOLS: list[Tool] = [
),
Tool(
tool_id="06_outlier_detector",
icon="📊",
icon=":material/insights:",
name="Find Unusual Values",
description=(
"Z-score, IQR, and MAD detection with domain-rule violations and "
@@ -72,7 +72,7 @@ TOOLS: list[Tool] = [
),
Tool(
tool_id="02_text_cleaner",
icon="✂️",
icon=":material/text_format:",
name="Clean Text",
description=(
"Whitespace trim, multi-space collapse, Unicode normalization, "
@@ -84,7 +84,7 @@ TOOLS: list[Tool] = [
),
Tool(
tool_id="03_format_standardizer",
icon="📐",
icon=":material/format_list_bulleted:",
name="Standardize Formats",
description=(
"Standardize dates, currencies, names, phone numbers, and addresses."
@@ -95,7 +95,7 @@ TOOLS: list[Tool] = [
),
Tool(
tool_id="01_deduplicator",
icon="🔍",
icon=":material/search:",
name="Find Duplicates",
description=(
"Fuzzy matching, normalization, survivor selection, and "
@@ -107,7 +107,7 @@ TOOLS: list[Tool] = [
),
Tool(
tool_id="08_validator_reporter",
icon="",
icon=":material/check_circle:",
name="Quality Check",
description=(
"Validate against rules and generate PDF/Excel quality reports."
@@ -118,7 +118,7 @@ TOOLS: list[Tool] = [
),
Tool(
tool_id="05_column_mapper",
icon="🗂️",
icon=":material/view_column:",
name="Map Columns",
description="Rename columns, enforce a target schema, and coerce types.",
page_slug="5_Column_Mapper",
@@ -127,7 +127,7 @@ TOOLS: list[Tool] = [
),
Tool(
tool_id="07_multi_file_merger",
icon="📎",
icon=":material/account_tree:",
name="Combine Files",
description="Combine multiple CSV/Excel files with schema alignment.",
page_slug="7_Multi_File_Merger",
@@ -136,7 +136,7 @@ TOOLS: list[Tool] = [
),
Tool(
tool_id="09_pipeline_runner",
icon="⚙️",
icon=":material/auto_awesome:",
name="Automated Workflows",
description=(
"Chain tools in recommended order and pass output between steps."