From 1016a4d2c428b0351a5f49a142819667e1360888 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 19 May 2026 02:04:53 +0000 Subject: [PATCH] feat(home,sidebar): brand hero + sidebar = footer style + PNG icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundles a handful of UX cleanups: - Findings-card chevron moved to the LEFT side of the head. CSS still rotates it 90° between collapsed/expanded states. - Tool-link buttons in findings rows (``Clean Text →`` etc.) are now left-justified against the icon column with minimal surrounding whitespace. Action column ratio dropped from 1.8 → 1.4 and the button switched from ``width="stretch"`` (centered text) to ``width="content"`` (shrinks to fit, left-aligned within column). - Home-page hero now mirrors the sidebar brand block: 56px ink "D" chip on the left + "UNALOGIX" eyebrow stacked above "DataTools" wordmark, then the "Clean. Normalize. Transform." tagline beneath. New ``.dt-page-brand / -row / -words / -mark / -eyebrow / -wordmark`` rules in ``_DESIGN_TOKENS_CSS``. Streamlit wraps h1 elements in an emotion-cache div with extra padding; a descendant flattener (``.dt-page-brand-words *`` margin:0 / padding:0) keeps the eyebrow + wordmark stack the same height as the chip so they center-align cleanly. - Sidebar nav restyled to match the sticky-footer Help/Close buttons exactly: 13px / 500 / 1.3 line-height, 5×10px padding, 8px gap between icon and label, transparent background. Active item gets the same ``rgba(0,0,0,0.04)`` tint as the hover state (no white pill, no shadow), only the heavier weight + ink text distinguishes it. - OS app icon (page_icon) switched from SVG to a Pillow-rendered ``datatools_icon_256.png`` so Windows / macOS taskbar+dock pick it up reliably (some OS shells fall back to a default icon for SVG favicons). Rounded-square ink ground with cream "D" centered — same mark as the sidebar chip + hero chip. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/gui/_home.py | 23 +++-- src/gui/assets/datatools_icon_256.png | Bin 0 -> 4199 bytes src/gui/components/_legacy.py | 127 +++++++++++++++++++++----- src/gui/pages/99_Close.py | 2 +- src/gui/pages/_Activate.py | 2 +- 5 files changed, 120 insertions(+), 34 deletions(-) create mode 100644 src/gui/assets/datatools_icon_256.png diff --git a/src/gui/_home.py b/src/gui/_home.py index 4780657..234a6fc 100644 --- a/src/gui/_home.py +++ b/src/gui/_home.py @@ -172,7 +172,7 @@ def _home_page() -> None: from src.i18n import t from pathlib import Path as _Path - _ICON_PATH = str(_Path(__file__).parent / "assets" / "datatools_icon.svg") + _ICON_PATH = str(_Path(__file__).parent / "assets" / "datatools_icon_256.png") st.set_page_config( page_title=t("home.page_title"), page_icon=_ICON_PATH, @@ -182,17 +182,22 @@ def _home_page() -> None: render_sticky_footer() 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. + # Page header — brand block (D icon + "UNALOGIX" eyebrow over + # "DataTools" wordmark + tagline) on the left, privacy pill on + # the right. Matches the sidebar brand chip scaled up for the + # hero. Bottom border replaces the explicit ``st.divider`` that + # used to sit below the caption. privacy_label = _html.escape(t("home.privacy_pill")) st.markdown( '
' - '
' - f'

{_html.escape(t("home.title"))}

' + '
' + '
' + '
D
' + '
' + 'UNALOGIX' + '

DataTools

' + '
' + '
' f'

{_html.escape(t("home.caption"))}

' '
' '' diff --git a/src/gui/assets/datatools_icon_256.png b/src/gui/assets/datatools_icon_256.png new file mode 100644 index 0000000000000000000000000000000000000000..497d2606d18414a180afaba7cdc1342b1a6f0ebf GIT binary patch literal 4199 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 bg}sw^_&v5g&r1^ifdEW?F*Bf?alY|CvbU5Z literal 0 HcmV?d00001 diff --git a/src/gui/components/_legacy.py b/src/gui/components/_legacy.py index 4f0e8a2..2e05e93 100644 --- a/src/gui/components/_legacy.py +++ b/src/gui/components/_legacy.py @@ -267,14 +267,20 @@ body, .stApp { margin: 0 !important; } -/* Nav items — tight padding so the menu lists feel dense and don't - waste vertical space. */ +/* Nav items match the sticky-footer Help/Close button style: ink- + secondary text, transparent surface, soft hover tint, no border or + active-state pill. Sizes line up with ``.datatools-footer-btn`` + (13px / 500 / 1.3 line-height, 5px×10px padding, 8px icon gap) so + the sidebar and footer feel like the same family. */ [data-testid="stSidebarNav"] a[data-testid="stSidebarNavLink"], [data-testid="stSidebarNav"] [data-testid="stSidebarNavLinkContainer"] a { color: var(--ink-secondary) !important; - font-size: 13.5px !important; - line-height: 1.25 !important; - padding: 4px 10px !important; + font-size: 13px !important; + font-weight: 500 !important; + line-height: 1.3 !important; + padding: 5px 10px !important; + gap: 8px !important; + border: none !important; border-radius: var(--r-sm) !important; transition: background 0.12s ease, color 0.12s ease; } @@ -291,13 +297,14 @@ body, .stApp { background: rgba(0,0,0,0.04) !important; color: var(--ink) !important; } -/* Active nav item — white pill with subtle shadow. Streamlit marks the - active anchor with ``aria-current="page"``. */ +/* Active item — soft hover-tint background + ink text + heavier + weight. No white pill, no shadow. Mirrors the footer buttons, + which carry no special "active" treatment. */ [data-testid="stSidebarNav"] a[aria-current="page"] { - background: var(--surface) !important; + background: rgba(0,0,0,0.04) !important; color: var(--ink) !important; - font-weight: 500 !important; - box-shadow: 0 1px 2px rgba(28,25,23,0.04) !important; + font-weight: 600 !important; + box-shadow: none !important; } /* Inline + block code → mono with subtle accent chip. theme.py owns @@ -577,22 +584,89 @@ div[data-testid="stContainer"][data-border="true"] { overflow: hidden !important; } -/* ---------- Page header (title + subtitle + privacy pill) ---------- */ +/* ---------- Page header (brand block + privacy pill) ---------- */ .dt-page-header { display: flex; - align-items: flex-end; + align-items: center; justify-content: space-between; gap: 24px; margin: 0 0 24px; padding-bottom: 22px; border-bottom: 1px solid var(--border); } -.dt-page-header h1 { margin: 0 !important; } +/* The brand block stacks two pieces vertically: the D-chip + words + row up top, then the tagline beneath. The D mark vertically + centres with the words column (eyebrow + wordmark), exactly like + the sidebar chip. */ +.dt-page-brand { + display: flex; + flex-direction: column; + gap: 8px; +} +.dt-page-brand-row { + display: flex; + align-items: center; + gap: 18px; +} +.dt-page-brand-words { + display: flex; + flex-direction: column; + gap: 2px; + line-height: 1; +} +/* Streamlit wraps the h1 in an emotion-cache div that adds ~3px top + padding + ~8px bottom margin. Flatten every descendant so the + eyebrow + wordmark stack hugs the chip height. */ +.dt-page-brand-words *, +.dt-page-brand-words > div { + margin: 0 !important; + padding: 0 !important; +} +.dt-page-brand-words .dt-page-wordmark { + line-height: 1 !important; +} +/* Same "Letter D (sans)" wordmark as the sidebar chip and favicon + — scaled up to hero size. Ink ground, cream D, Geist 700, -0.04em + tracking. */ +.dt-page-brand-mark { + width: 56px; + height: 56px; + border-radius: 14px; + background: var(--ink); + color: var(--accent-fill); + display: inline-flex; + align-items: center; + justify-content: center; + font-family: var(--font-sans); + font-weight: 700; + font-size: 32px; + letter-spacing: -0.04em; + line-height: 1; + flex-shrink: 0; +} +.dt-page-eyebrow { + font-family: var(--font-sans) !important; + font-size: 11.5px; + font-weight: 600; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--ink-tertiary); + line-height: 1.2; +} +.dt-page-wordmark { + margin: 0 !important; + font-family: var(--font-sans) !important; + font-weight: 600 !important; + font-size: 32px !important; + letter-spacing: -0.035em !important; + line-height: 1.1 !important; + color: var(--ink) !important; +} .dt-page-header .dt-page-subtitle { - margin: 6px 0 0; + margin: 4px 0 0; color: var(--ink-secondary) !important; font-size: 14px; - line-height: 1.55; + line-height: 1.5; } .dt-privacy-pill { display: inline-flex; @@ -740,9 +814,9 @@ div[data-testid="stContainer"][data-border="true"] { .dt-finding-group-head:hover { background: var(--accent-fill); } -/* Chevron lives on the right of the head, rotates to indicate state. */ +/* Chevron leads the head as the first flex item; rotates 90° to + indicate expanded state. */ .dt-finding-group-chevron { - margin-left: 8px; color: var(--ink-tertiary); font-family: "Material Symbols Outlined" !important; font-size: 20px !important; @@ -750,6 +824,7 @@ div[data-testid="stContainer"][data-border="true"] { line-height: 1 !important; transition: transform 0.15s ease; flex-shrink: 0; + margin-right: -2px; } .dt-finding-group-head[data-dt-collapsed="false"] .dt-finding-group-chevron { transform: rotate(90deg); @@ -2843,12 +2918,15 @@ def render_findings_panel( f'{n} {label}' ) + # Chevron leads the head — clicking the row toggles + # ``data-dt-collapsed``. ``chevron_right`` (▶) is the collapsed + # rest state; CSS rotates it 90° to point down (▼) when expanded. head_html = ( '
' + 'chevron_right' f'' f'{_html.escape(header)}' f'
{pills_html}
' - 'chevron_right' '
' ) st.markdown(head_html, unsafe_allow_html=True) @@ -2911,10 +2989,13 @@ def _render_finding_row_v2(f, *, row_key: str) -> None: meta_html = " · ".join(meta_parts) # Action button moved to the LEFT of the description per UX - # feedback: ``[icon] [Open →] [description]`` — the action - # is now the prominent affordance in the row, with the description - # taking the wide remaining column. - col_icon, col_action, col_body = st.columns([0.4, 1.8, 8]) + # feedback: ``[icon] [ →] [description]`` — the action is + # the prominent affordance in the row, with the description taking + # the wide remaining column. Tight action-column ratio (1.4) plus + # ``width="content"`` on the button below keeps the link + # left-justified against the icon with minimal surrounding + # whitespace. + col_icon, col_action, col_body = st.columns([0.4, 1.4, 8]) col_icon.markdown( f'
' @@ -2930,7 +3011,7 @@ def _render_finding_row_v2(f, *, row_key: str) -> None: f"{tool_label} →", key=f"_finding_open_{row_key}", type="tertiary", - width="stretch", + width="content", ): st.switch_page(page_slug) diff --git a/src/gui/pages/99_Close.py b/src/gui/pages/99_Close.py index a870ec2..9847350 100644 --- a/src/gui/pages/99_Close.py +++ b/src/gui/pages/99_Close.py @@ -21,7 +21,7 @@ from src.gui.components import hide_streamlit_chrome, shutdown_app from src.i18n import t from pathlib import Path as _Path -_ICON_PATH = str(_Path(__file__).parent.parent / "assets" / "datatools_icon.svg") +_ICON_PATH = str(_Path(__file__).parent.parent / "assets" / "datatools_icon_256.png") st.set_page_config( page_title=t("close_page.page_title"), page_icon=_ICON_PATH, diff --git a/src/gui/pages/_Activate.py b/src/gui/pages/_Activate.py index c0bb2b7..4fbf3db 100644 --- a/src/gui/pages/_Activate.py +++ b/src/gui/pages/_Activate.py @@ -27,7 +27,7 @@ from src.gui.components import ( from src.i18n import t from pathlib import Path as _Path -_ICON_PATH = str(_Path(__file__).parent.parent / "assets" / "datatools_icon.svg") +_ICON_PATH = str(_Path(__file__).parent.parent / "assets" / "datatools_icon_256.png") st.set_page_config( page_title=t("activation.page_title"), page_icon=_ICON_PATH,