From da7d86f457d6070010f6cef035751e89dad761b0 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 19 May 2026 00:31:40 +0000 Subject: [PATCH] feat(ui): Material icons in sidebar + stats overview on home MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two pieces of the mockup 2 layout that hadn't landed yet: 1. Sidebar nav icons — emoji glyphs (🧹 ✂️ 🔍 …) swapped for Streamlit's ``:material/:`` 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 ```` 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) --- src/gui/_home.py | 60 +++++++++++++++++++++++++++++++++++ src/gui/app.py | 6 ++-- src/gui/components/_legacy.py | 54 +++++++++++++++++++++++++++++++ src/gui/theme.py | 24 ++++++++++++++ src/gui/tools_registry.py | 18 +++++------ src/i18n/packs/en.json | 24 +++++++------- src/i18n/packs/es.json | 24 +++++++------- 7 files changed, 174 insertions(+), 36 deletions(-) diff --git a/src/gui/_home.py b/src/gui/_home.py index 64a202a..b39753b 100644 --- a/src/gui/_home.py +++ b/src/gui/_home.py @@ -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'{_html.escape(unit)}' + if unit else "" + ) + return ( + f'
' + f'
{_html.escape(label)}
' + f'
{value}{unit_html}
' + f"
" + ) + + 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'
{cards}
', + 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: diff --git a/src/gui/app.py b/src/gui/app.py index ad31b80..875c13e 100644 --- a/src/gui/app.py +++ b/src/gui/app.py @@ -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", ) diff --git a/src/gui/components/_legacy.py b/src/gui/components/_legacy.py index cb64e08..68dd18c 100644 --- a/src/gui/components/_legacy.py +++ b/src/gui/components/_legacy.py @@ -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); } +} """ diff --git a/src/gui/theme.py b/src/gui/theme.py index 7bf8725..fbf0bd6 100644 --- a/src/gui/theme.py +++ b/src/gui/theme.py @@ -173,6 +173,30 @@ _CSS = f""" font-size: 0.92em; font-feature-settings: "ss02"; }} + + /* Material icons in the sidebar nav. Streamlit's ``:material/:`` + syntax injects a first-child ```` 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; + }} """ diff --git a/src/gui/tools_registry.py b/src/gui/tools_registry.py index 50a9db9..dbfa93c 100644 --- a/src/gui/tools_registry.py +++ b/src/gui/tools_registry.py @@ -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." diff --git a/src/i18n/packs/en.json b/src/i18n/packs/en.json index 2e5f6c5..521e35e 100644 --- a/src/i18n/packs/en.json +++ b/src/i18n/packs/en.json @@ -5,7 +5,7 @@ }, "home": { "page_title": "DataTools — Data Cleaning Mastery", - "title": "🧹 DataTools — Data Cleaning Mastery", + "title": "DataTools — Data Cleaning Mastery", "caption": "A 9-tool suite for cleaning, standardizing, and validating tabular data. Runs 100% locally.", "findings_badge_one": "{n} finding", "findings_badge_other": "{n} findings" @@ -57,14 +57,14 @@ }, "close_page": { "page_title": "DataTools — Close", - "title": "🛑 Close DataTools", + "title": "Close DataTools", "caption": "Shut down the local app and free the terminal.", "body": "Clicking the button below will terminate the DataTools server. Any unsaved work in other tools will be lost. Once the app shuts down you can close this window.", "button": "Close the app" }, "activation": { "page_title": "DataTools — Activate", - "title": "🔑 Activate DataTools", + "title": "Activate DataTools", "intro": "DataTools needs to be activated before any tools unlock. Enter the name and email tied to your purchase, then paste the license blob from your delivery email.", "name_label": "Full name", "name_help": "Must match the name on your purchase receipt.", @@ -107,55 +107,55 @@ "01_deduplicator": { "name": "Find Duplicates", "description": "Fuzzy matching, normalization, survivor selection, and interactive review.", - "page_title": "🔍 Find Duplicates", + "page_title": "Find Duplicates", "page_caption": "Find and remove duplicate rows in CSV, delimited text, and Excel files." }, "02_text_cleaner": { "name": "Clean Text", "description": "Whitespace trim, multi-space collapse, Unicode normalization, BOM and line-ending handling.", - "page_title": "✂️ Clean Text", + "page_title": "Clean Text", "page_caption": "Trim whitespace, fold smart quotes, strip invisible characters, and normalize line endings. Runs locally — your data never leaves this computer." }, "03_format_standardizer": { "name": "Standardize Formats", "description": "Standardize dates, currencies, names, phone numbers, and addresses.", - "page_title": "📐 Standardize Formats", + "page_title": "Standardize Formats", "page_caption": "Canonicalize dates, phone numbers, currency, names, addresses, and booleans on a per-column basis. Runs locally — your data never leaves this computer." }, "04_missing_handler": { "name": "Fix Missing Values", "description": "Detect disguised nulls, missingness analysis, and imputation strategies.", - "page_title": "🕳️ Fix Missing Values", + "page_title": "Fix Missing Values", "page_caption": "Detect disguised nulls, profile missingness, and apply imputation or drop strategies. Runs locally — your data never leaves this computer." }, "05_column_mapper": { "name": "Map Columns", "description": "Rename columns, enforce a target schema, and coerce types.", - "page_title": "🗂️ Map Columns", + "page_title": "Map Columns", "page_caption": "Rename columns, enforce a target schema, and coerce types. Runs locally — your data never leaves this computer." }, "06_outlier_detector": { "name": "Find Unusual Values", "description": "Z-score, IQR, and MAD detection with domain-rule violations and winsorization.", - "page_title": "📊 Find Unusual Values", + "page_title": "Find Unusual Values", "page_caption": "Detect and handle outliers in numeric columns." }, "07_multi_file_merger": { "name": "Combine Files", "description": "Combine multiple CSV/Excel files with schema alignment.", - "page_title": "📎 Combine Files", + "page_title": "Combine Files", "page_caption": "Combine multiple CSV and Excel files into one dataset." }, "08_validator_reporter": { "name": "Quality Check", "description": "Validate against rules and generate PDF/Excel quality reports.", - "page_title": "✅ Quality Check", + "page_title": "Quality Check", "page_caption": "Validate data against rules and generate quality reports." }, "09_pipeline_runner": { "name": "Automated Workflows", "description": "Chain tools in recommended order and pass output between steps.", - "page_title": "⚙️ Automated Workflows", + "page_title": "Automated Workflows", "page_caption": "Chain DataTools cleaning steps into one repeatable workflow. The pipeline recommends an order; you stay in control." } }, diff --git a/src/i18n/packs/es.json b/src/i18n/packs/es.json index 7e0a5bb..9a7eea7 100644 --- a/src/i18n/packs/es.json +++ b/src/i18n/packs/es.json @@ -5,7 +5,7 @@ }, "home": { "page_title": "DataTools — Maestría en limpieza de datos", - "title": "🧹 DataTools — Maestría en limpieza de datos", + "title": "DataTools — Maestría en limpieza de datos", "caption": "Conjunto de 9 herramientas para limpiar, estandarizar y validar datos tabulares. Se ejecuta 100% en local.", "findings_badge_one": "{n} hallazgo", "findings_badge_other": "{n} hallazgos" @@ -57,14 +57,14 @@ }, "close_page": { "page_title": "DataTools — Cerrar", - "title": "🛑 Cerrar DataTools", + "title": "Cerrar DataTools", "caption": "Detén la aplicación local y libera la terminal.", "body": "Al pulsar el botón de abajo se cerrará el servidor de DataTools. Cualquier trabajo sin guardar en otras herramientas se perderá. Una vez cerrada la app, puedes cerrar esta ventana.", "button": "Cerrar la app" }, "activation": { "page_title": "DataTools — Activar", - "title": "🔑 Activar DataTools", + "title": "Activar DataTools", "intro": "DataTools debe activarse antes de desbloquear cualquier herramienta. Introduce el nombre y correo asociados a tu compra, y luego pega el código de licencia del correo de entrega.", "name_label": "Nombre completo", "name_help": "Debe coincidir con el nombre en el recibo de compra.", @@ -107,55 +107,55 @@ "01_deduplicator": { "name": "Buscar duplicados", "description": "Coincidencia difusa, normalización, selección de superviviente y revisión interactiva.", - "page_title": "🔍 Buscar duplicados", + "page_title": "Buscar duplicados", "page_caption": "Encuentra y elimina filas duplicadas en archivos CSV, texto delimitado y Excel." }, "02_text_cleaner": { "name": "Limpiar texto", "description": "Recorte de espacios, colapso de espacios múltiples, normalización Unicode, manejo de BOM y de finales de línea.", - "page_title": "✂️ Limpiar texto", + "page_title": "Limpiar texto", "page_caption": "Recorta espacios, normaliza comillas tipográficas, elimina caracteres invisibles y unifica saltos de línea. Se ejecuta localmente — tus datos nunca salen de este equipo." }, "03_format_standardizer": { "name": "Estandarizar formatos", "description": "Estandariza fechas, monedas, nombres, números de teléfono y direcciones.", - "page_title": "📐 Estandarizar formatos", + "page_title": "Estandarizar formatos", "page_caption": "Canoniza fechas, números de teléfono, monedas, nombres, direcciones y booleanos columna por columna. Se ejecuta localmente — tus datos nunca salen de este equipo." }, "04_missing_handler": { "name": "Corregir valores faltantes", "description": "Detecta nulos disfrazados, analiza la ausencia de datos y aplica estrategias de imputación.", - "page_title": "🕳️ Corregir valores faltantes", + "page_title": "Corregir valores faltantes", "page_caption": "Detecta nulos disfrazados, perfila la ausencia de datos y aplica imputación o estrategias de descarte. Se ejecuta localmente — tus datos nunca salen de este equipo." }, "05_column_mapper": { "name": "Mapear columnas", "description": "Renombra columnas, aplica un esquema objetivo y fuerza tipos de datos.", - "page_title": "🗂️ Mapear columnas", + "page_title": "Mapear columnas", "page_caption": "Renombra columnas, aplica un esquema objetivo y fuerza tipos. Se ejecuta localmente — tus datos nunca salen de este equipo." }, "06_outlier_detector": { "name": "Detectar valores atípicos", "description": "Detección por Z-score, IQR y MAD con reglas de dominio y winsorización.", - "page_title": "📊 Detectar valores atípicos", + "page_title": "Detectar valores atípicos", "page_caption": "Detecta y trata valores atípicos en columnas numéricas." }, "07_multi_file_merger": { "name": "Combinar archivos", "description": "Combina varios archivos CSV/Excel alineando sus esquemas.", - "page_title": "📎 Combinar archivos", + "page_title": "Combinar archivos", "page_caption": "Combina varios archivos CSV y Excel en un único conjunto de datos." }, "08_validator_reporter": { "name": "Verificación de calidad", "description": "Valida contra reglas y genera informes de calidad en PDF/Excel.", - "page_title": "✅ Verificación de calidad", + "page_title": "Verificación de calidad", "page_caption": "Valida datos contra reglas y genera informes de calidad." }, "09_pipeline_runner": { "name": "Flujos automatizados", "description": "Encadena herramientas en el orden recomendado y pasa la salida entre pasos.", - "page_title": "⚙️ Flujos automatizados", + "page_title": "Flujos automatizados", "page_caption": "Encadena pasos de limpieza de DataTools en un flujo repetible. La canalización recomienda un orden; tú mantienes el control." } },