feat(home,sidebar): brand hero + sidebar = footer style + PNG icon

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 02:04:53 +00:00
parent 6c3939d21b
commit 1016a4d2c4
5 changed files with 120 additions and 34 deletions

View File

@@ -172,7 +172,7 @@ def _home_page() -> None:
from src.i18n import t from src.i18n import t
from pathlib import Path as _Path 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( st.set_page_config(
page_title=t("home.page_title"), page_title=t("home.page_title"),
page_icon=_ICON_PATH, page_icon=_ICON_PATH,
@@ -182,17 +182,22 @@ def _home_page() -> None:
render_sticky_footer() render_sticky_footer()
import html as _html import html as _html
# Page header — h1 + body subtitle on the left, privacy pill on # Page header — brand block (D icon + "UNALOGIX" eyebrow over
# the right (mockup §page-header). Rendered as a single HTML block # "DataTools" wordmark + tagline) on the left, privacy pill on
# so the title/subtitle/pill share one flex row; ``st.title`` + # the right. Matches the sidebar brand chip scaled up for the
# ``st.caption`` + ``st.divider`` would stack vertically and lose # hero. Bottom border replaces the explicit ``st.divider`` that
# the right-aligned pill. Bottom border replaces the explicit # used to sit below the caption.
# ``st.divider`` that used to sit below the caption.
privacy_label = _html.escape(t("home.privacy_pill")) privacy_label = _html.escape(t("home.privacy_pill"))
st.markdown( st.markdown(
'<header class="dt-page-header">' '<header class="dt-page-header">'
'<div>' '<div class="dt-page-brand">'
f'<h1>{_html.escape(t("home.title"))}</h1>' '<div class="dt-page-brand-row">'
'<div class="dt-page-brand-mark">D</div>'
'<div class="dt-page-brand-words">'
'<span class="dt-page-eyebrow">UNALOGIX</span>'
'<h1 class="dt-page-wordmark">DataTools</h1>'
'</div>'
'</div>'
f'<p class="dt-page-subtitle">{_html.escape(t("home.caption"))}</p>' f'<p class="dt-page-subtitle">{_html.escape(t("home.caption"))}</p>'
'</div>' '</div>'
'<span class="dt-privacy-pill">' '<span class="dt-privacy-pill">'

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -267,14 +267,20 @@ body, .stApp {
margin: 0 !important; margin: 0 !important;
} }
/* Nav items — tight padding so the menu lists feel dense and don't /* Nav items match the sticky-footer Help/Close button style: ink-
waste vertical space. */ 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"] a[data-testid="stSidebarNavLink"],
[data-testid="stSidebarNav"] [data-testid="stSidebarNavLinkContainer"] a { [data-testid="stSidebarNav"] [data-testid="stSidebarNavLinkContainer"] a {
color: var(--ink-secondary) !important; color: var(--ink-secondary) !important;
font-size: 13.5px !important; font-size: 13px !important;
line-height: 1.25 !important; font-weight: 500 !important;
padding: 4px 10px !important; line-height: 1.3 !important;
padding: 5px 10px !important;
gap: 8px !important;
border: none !important;
border-radius: var(--r-sm) !important; border-radius: var(--r-sm) !important;
transition: background 0.12s ease, color 0.12s ease; transition: background 0.12s ease, color 0.12s ease;
} }
@@ -291,13 +297,14 @@ body, .stApp {
background: rgba(0,0,0,0.04) !important; background: rgba(0,0,0,0.04) !important;
color: var(--ink) !important; color: var(--ink) !important;
} }
/* Active nav item — white pill with subtle shadow. Streamlit marks the /* Active item — soft hover-tint background + ink text + heavier
active anchor with ``aria-current="page"``. */ weight. No white pill, no shadow. Mirrors the footer buttons,
which carry no special "active" treatment. */
[data-testid="stSidebarNav"] a[aria-current="page"] { [data-testid="stSidebarNav"] a[aria-current="page"] {
background: var(--surface) !important; background: rgba(0,0,0,0.04) !important;
color: var(--ink) !important; color: var(--ink) !important;
font-weight: 500 !important; font-weight: 600 !important;
box-shadow: 0 1px 2px rgba(28,25,23,0.04) !important; box-shadow: none !important;
} }
/* Inline + block code → mono with subtle accent chip. theme.py owns /* 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; overflow: hidden !important;
} }
/* ---------- Page header (title + subtitle + privacy pill) ---------- */ /* ---------- Page header (brand block + privacy pill) ---------- */
.dt-page-header { .dt-page-header {
display: flex; display: flex;
align-items: flex-end; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 24px; gap: 24px;
margin: 0 0 24px; margin: 0 0 24px;
padding-bottom: 22px; padding-bottom: 22px;
border-bottom: 1px solid var(--border); 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 { .dt-page-header .dt-page-subtitle {
margin: 6px 0 0; margin: 4px 0 0;
color: var(--ink-secondary) !important; color: var(--ink-secondary) !important;
font-size: 14px; font-size: 14px;
line-height: 1.55; line-height: 1.5;
} }
.dt-privacy-pill { .dt-privacy-pill {
display: inline-flex; display: inline-flex;
@@ -740,9 +814,9 @@ div[data-testid="stContainer"][data-border="true"] {
.dt-finding-group-head:hover { .dt-finding-group-head:hover {
background: var(--accent-fill); 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 { .dt-finding-group-chevron {
margin-left: 8px;
color: var(--ink-tertiary); color: var(--ink-tertiary);
font-family: "Material Symbols Outlined" !important; font-family: "Material Symbols Outlined" !important;
font-size: 20px !important; font-size: 20px !important;
@@ -750,6 +824,7 @@ div[data-testid="stContainer"][data-border="true"] {
line-height: 1 !important; line-height: 1 !important;
transition: transform 0.15s ease; transition: transform 0.15s ease;
flex-shrink: 0; flex-shrink: 0;
margin-right: -2px;
} }
.dt-finding-group-head[data-dt-collapsed="false"] .dt-finding-group-chevron { .dt-finding-group-head[data-dt-collapsed="false"] .dt-finding-group-chevron {
transform: rotate(90deg); transform: rotate(90deg);
@@ -2843,12 +2918,15 @@ def render_findings_panel(
f'<span class="dt-count-pill {sev}">{n} {label}</span>' f'<span class="dt-count-pill {sev}">{n} {label}</span>'
) )
# 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 = ( head_html = (
'<div class="dt-finding-group-head" data-dt-collapsed="true">' '<div class="dt-finding-group-head" data-dt-collapsed="true">'
'<span class="dt-finding-group-chevron">chevron_right</span>'
f'<span class="dt-severity-dot {worst}"></span>' f'<span class="dt-severity-dot {worst}"></span>'
f'<span class="dt-group-filename">{_html.escape(header)}</span>' f'<span class="dt-group-filename">{_html.escape(header)}</span>'
f'<div class="dt-group-counts">{pills_html}</div>' f'<div class="dt-group-counts">{pills_html}</div>'
'<span class="dt-finding-group-chevron">chevron_right</span>'
'</div>' '</div>'
) )
st.markdown(head_html, unsafe_allow_html=True) 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) meta_html = " · ".join(meta_parts)
# Action button moved to the LEFT of the description per UX # Action button moved to the LEFT of the description per UX
# feedback: ``[icon] [Open <Tool> →] [description]`` — the action # feedback: ``[icon] [<Tool> →] [description]`` — the action is
# is now the prominent affordance in the row, with the description # the prominent affordance in the row, with the description taking
# taking the wide remaining column. # the wide remaining column. Tight action-column ratio (1.4) plus
col_icon, col_action, col_body = st.columns([0.4, 1.8, 8]) # ``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( col_icon.markdown(
f'<div class="dt-finding-icon {f.severity}">' f'<div class="dt-finding-icon {f.severity}">'
@@ -2930,7 +3011,7 @@ def _render_finding_row_v2(f, *, row_key: str) -> None:
f"{tool_label}", f"{tool_label}",
key=f"_finding_open_{row_key}", key=f"_finding_open_{row_key}",
type="tertiary", type="tertiary",
width="stretch", width="content",
): ):
st.switch_page(page_slug) st.switch_page(page_slug)

View File

@@ -21,7 +21,7 @@ from src.gui.components import hide_streamlit_chrome, shutdown_app
from src.i18n import t from src.i18n import t
from pathlib import Path as _Path 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( st.set_page_config(
page_title=t("close_page.page_title"), page_title=t("close_page.page_title"),
page_icon=_ICON_PATH, page_icon=_ICON_PATH,

View File

@@ -27,7 +27,7 @@ from src.gui.components import (
from src.i18n import t from src.i18n import t
from pathlib import Path as _Path 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( st.set_page_config(
page_title=t("activation.page_title"), page_title=t("activation.page_title"),
page_icon=_ICON_PATH, page_icon=_ICON_PATH,