feat(ui): warm editorial redesign — Fraunces + Geist + stone palette
Lifts ideas from the ``datatools_layout_redesign.html`` mockup
(artistic licence, not literal). Two changes:
1. ``.streamlit/config.toml`` ``[theme]`` block — cream paper bg
(#fafaf7), warm sidebar (#f5f4ef), stone ink (#1c1917), burnt
orange primary (#c2410c). Streamlit threads these through its
chrome (focus rings, file-uploader accents, link colors).
2. ``_DESIGN_TOKENS_CSS`` injected by ``hide_streamlit_chrome`` on
every page. Imports Fraunces (display serif), Geist (body sans),
Geist Mono. Restyles, scoped through ``--dt-*`` custom properties:
- Page surface + sidebar — warm cream backgrounds, soft warm
borders, no harsh white.
- Sidebar nav — section labels in tiny uppercase tracking, nav
items with soft hover, active item as a white pill with subtle
shadow.
- Typography — H1/H2/H3 in Fraunces with tightened tracking;
body Geist; inline code Geist Mono with orange-on-cream chip.
- Buttons — primary = dark ink (``#1c1917``) with white text;
secondary = paper surface with warm border; disabled = muted
cream.
- Containers / expanders — editorial cards: 14px radius, 1px
warm border, faint shadow, warm-cream summary headers.
- File uploader — cream dropzone with dashed border + per-file
paper chips.
- Alerts — soft tinted fills (info=sky, success=mint, warn=amber,
error=rose) over the kind-specific palette.
- Inputs, tabs, dataframes — paper surfaces with rounded warm
borders.
Verified at 1920x1050 + 1400x900 on home page (empty + with file
uploaded + with findings rendered) and Clean Text tool page; no
regressions in the white-bar fix from 65b663b.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,3 +9,17 @@ gatherUsageStats = false
|
||||
# reads "Limit 1024MB per file" — matches the analyzer + gate's stated
|
||||
# 1 GB efficiency target. See docs/REQUIREMENTS.md §1.1.
|
||||
maxUploadSize = 1024
|
||||
|
||||
# Warm, editorial palette inspired by the
|
||||
# ``datatools_layout_redesign.html`` mockup — cream paper background,
|
||||
# stone ink, burnt-orange accent. Streamlit reads these on startup and
|
||||
# threads them through its widget chrome (file uploader, focus rings,
|
||||
# primary buttons, links). Heavier visual restyling rides on the CSS
|
||||
# in ``_legacy.py:_DESIGN_TOKENS_CSS``.
|
||||
[theme]
|
||||
base = "light"
|
||||
primaryColor = "#c2410c"
|
||||
backgroundColor = "#fafaf7"
|
||||
secondaryBackgroundColor = "#f5f4ef"
|
||||
textColor = "#1c1917"
|
||||
font = "sans serif"
|
||||
|
||||
@@ -137,6 +137,319 @@ hr { margin-top: 0.4rem !important; margin-bottom: 0.4rem !important; }
|
||||
"""
|
||||
|
||||
|
||||
# Warm editorial palette + typography, lifted from the
|
||||
# ``datatools_layout_redesign.html`` mockup. Applied on every page via
|
||||
# ``hide_streamlit_chrome``. Tokens are scoped through CSS custom
|
||||
# properties so individual rules read cleanly and a future tweak only
|
||||
# has to touch the ``:root`` block.
|
||||
_DESIGN_TOKENS_CSS = """
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap');
|
||||
|
||||
:root {
|
||||
--dt-bg: #fafaf7;
|
||||
--dt-bg-sidebar: #f5f4ef;
|
||||
--dt-surface: #ffffff;
|
||||
--dt-surface-hover: #f8f7f3;
|
||||
--dt-border: #e7e5dc;
|
||||
--dt-border-strong: #d6d3c7;
|
||||
--dt-ink: #1c1917;
|
||||
--dt-ink-secondary: #57534e;
|
||||
--dt-ink-tertiary: #a8a29e;
|
||||
--dt-accent: #c2410c;
|
||||
--dt-accent-hover: #9a3412;
|
||||
--dt-accent-fill: #fef4ed;
|
||||
--dt-warn: #b45309;
|
||||
--dt-warn-fill: #fef3c7;
|
||||
--dt-info: #0369a1;
|
||||
--dt-info-fill: #e0f2fe;
|
||||
--dt-success: #15803d;
|
||||
--dt-success-fill: #dcfce7;
|
||||
--dt-danger: #b91c1c;
|
||||
--dt-danger-fill: #fee2e2;
|
||||
--dt-r-sm: 6px;
|
||||
--dt-r-md: 10px;
|
||||
--dt-r-lg: 14px;
|
||||
--dt-font-display: "Fraunces", Georgia, serif;
|
||||
--dt-font-body: "Geist", -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
--dt-font-mono: "Geist Mono", "SF Mono", Menlo, monospace;
|
||||
}
|
||||
|
||||
/* ---------- Page surface ---------- */
|
||||
body, .stApp {
|
||||
background: var(--dt-bg) !important;
|
||||
font-family: var(--dt-font-body) !important;
|
||||
color: var(--dt-ink) !important;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ---------- Sidebar — cream paper, soft right edge ---------- */
|
||||
[data-testid="stSidebar"] {
|
||||
background: var(--dt-bg-sidebar) !important;
|
||||
border-right: 1px solid var(--dt-border) !important;
|
||||
}
|
||||
[data-testid="stSidebar"] > div:first-child {
|
||||
background: var(--dt-bg-sidebar) !important;
|
||||
}
|
||||
|
||||
/* Section labels in the page-nav: tiny uppercase tracking. Streamlit
|
||||
renders these as <span> nodes with class ``st-emotion-cache-…``
|
||||
inside ``stSidebarNav`` — class hashes are unstable across versions,
|
||||
so we lean on the structural position (the bare span/h2 directly
|
||||
inside the nav list) rather than emotion classes. */
|
||||
[data-testid="stSidebarNav"] h2,
|
||||
[data-testid="stSidebarNav"] h3,
|
||||
[data-testid="stSidebarNavSeparator"] span,
|
||||
[data-testid="stSidebarNavSectionHeader"] {
|
||||
font-family: var(--dt-font-body) !important;
|
||||
font-size: 10.5px !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: 0.08em !important;
|
||||
color: var(--dt-ink-tertiary) !important;
|
||||
font-weight: 500 !important;
|
||||
padding-top: 14px !important;
|
||||
padding-bottom: 4px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* Nav items: comfortable padding, soft hover. */
|
||||
[data-testid="stSidebarNav"] a[data-testid="stSidebarNavLink"],
|
||||
[data-testid="stSidebarNav"] [data-testid="stSidebarNavLinkContainer"] a {
|
||||
color: var(--dt-ink-secondary) !important;
|
||||
font-size: 13.5px !important;
|
||||
line-height: 1.3 !important;
|
||||
padding: 7px 10px !important;
|
||||
border-radius: var(--dt-r-sm) !important;
|
||||
transition: background 0.12s ease, color 0.12s ease;
|
||||
}
|
||||
[data-testid="stSidebarNav"] a[data-testid="stSidebarNavLink"]:hover,
|
||||
[data-testid="stSidebarNav"] [data-testid="stSidebarNavLinkContainer"] a:hover {
|
||||
background: rgba(0,0,0,0.04) !important;
|
||||
color: var(--dt-ink) !important;
|
||||
}
|
||||
/* Active nav item — white pill with subtle shadow. Streamlit marks the
|
||||
active anchor with ``aria-current="page"``. */
|
||||
[data-testid="stSidebarNav"] a[aria-current="page"] {
|
||||
background: var(--dt-surface) !important;
|
||||
color: var(--dt-ink) !important;
|
||||
font-weight: 500 !important;
|
||||
box-shadow: 0 1px 2px rgba(28,25,23,0.04) !important;
|
||||
}
|
||||
|
||||
/* ---------- Typography ---------- */
|
||||
.stApp h1, [data-testid="stMain"] h1 {
|
||||
font-family: var(--dt-font-display) !important;
|
||||
font-weight: 500 !important;
|
||||
font-size: 32px !important;
|
||||
letter-spacing: -0.025em !important;
|
||||
line-height: 1.1 !important;
|
||||
color: var(--dt-ink) !important;
|
||||
}
|
||||
.stApp h2, [data-testid="stMain"] h2 {
|
||||
font-family: var(--dt-font-display) !important;
|
||||
font-weight: 500 !important;
|
||||
font-size: 22px !important;
|
||||
letter-spacing: -0.02em !important;
|
||||
color: var(--dt-ink) !important;
|
||||
}
|
||||
.stApp h3, [data-testid="stMain"] h3 {
|
||||
font-family: var(--dt-font-display) !important;
|
||||
font-weight: 500 !important;
|
||||
font-size: 18px !important;
|
||||
letter-spacing: -0.015em !important;
|
||||
color: var(--dt-ink) !important;
|
||||
}
|
||||
/* st.caption + subheadings are body-sans, secondary ink. */
|
||||
[data-testid="stCaption"],
|
||||
[data-testid="stCaptionContainer"] {
|
||||
color: var(--dt-ink-secondary) !important;
|
||||
font-size: 13.5px !important;
|
||||
}
|
||||
/* Inline + block code in user-facing text → Geist Mono. */
|
||||
[data-testid="stMarkdownContainer"] code,
|
||||
[data-testid="stCode"] pre {
|
||||
font-family: var(--dt-font-mono) !important;
|
||||
background: var(--dt-accent-fill) !important;
|
||||
color: var(--dt-accent-hover) !important;
|
||||
padding: 1px 5px !important;
|
||||
border-radius: 4px !important;
|
||||
font-size: 0.92em !important;
|
||||
}
|
||||
[data-testid="stCode"] pre {
|
||||
padding: 12px 14px !important;
|
||||
background: var(--dt-surface-hover) !important;
|
||||
color: var(--dt-ink) !important;
|
||||
border: 1px solid var(--dt-border) !important;
|
||||
border-radius: var(--dt-r-md) !important;
|
||||
}
|
||||
|
||||
/* ---------- Buttons — ink primary, outlined secondary ---------- */
|
||||
[data-testid="stButton"] button,
|
||||
[data-testid="stDownloadButton"] button {
|
||||
border-radius: var(--dt-r-md) !important;
|
||||
font-family: var(--dt-font-body) !important;
|
||||
font-weight: 500 !important;
|
||||
font-size: 13.5px !important;
|
||||
line-height: 1 !important;
|
||||
padding: 9px 16px !important;
|
||||
transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
|
||||
}
|
||||
/* Primary = dark ink (matches mockup's ``btn-primary``). */
|
||||
[data-testid="stButton"] button[kind="primary"],
|
||||
[data-testid="stButton"] button[data-testid="stBaseButton-primary"],
|
||||
[data-testid="stDownloadButton"] button[kind="primary"] {
|
||||
background: var(--dt-ink) !important;
|
||||
color: var(--dt-bg) !important;
|
||||
border: 1px solid var(--dt-ink) !important;
|
||||
}
|
||||
[data-testid="stButton"] button[kind="primary"]:hover,
|
||||
[data-testid="stButton"] button[data-testid="stBaseButton-primary"]:hover,
|
||||
[data-testid="stDownloadButton"] button[kind="primary"]:hover {
|
||||
background: #292524 !important;
|
||||
border-color: #292524 !important;
|
||||
color: var(--dt-bg) !important;
|
||||
}
|
||||
/* Secondary = surface + warm border. */
|
||||
[data-testid="stButton"] button[kind="secondary"],
|
||||
[data-testid="stButton"] button[data-testid="stBaseButton-secondary"],
|
||||
[data-testid="stDownloadButton"] button[kind="secondary"] {
|
||||
background: var(--dt-surface) !important;
|
||||
color: var(--dt-ink) !important;
|
||||
border: 1px solid var(--dt-border-strong) !important;
|
||||
}
|
||||
[data-testid="stButton"] button[kind="secondary"]:hover,
|
||||
[data-testid="stButton"] button[data-testid="stBaseButton-secondary"]:hover {
|
||||
background: var(--dt-surface-hover) !important;
|
||||
border-color: var(--dt-ink-tertiary) !important;
|
||||
}
|
||||
/* Disabled state — same look both kinds, low-contrast. */
|
||||
[data-testid="stButton"] button:disabled {
|
||||
background: var(--dt-surface-hover) !important;
|
||||
color: var(--dt-ink-tertiary) !important;
|
||||
border-color: var(--dt-border) !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
/* ---------- File uploader — soft cream dropzone ---------- */
|
||||
[data-testid="stFileUploader"] section,
|
||||
[data-testid="stFileUploaderDropzone"] {
|
||||
background: var(--dt-surface-hover) !important;
|
||||
border: 1px dashed var(--dt-border-strong) !important;
|
||||
border-radius: var(--dt-r-md) !important;
|
||||
}
|
||||
[data-testid="stFileUploader"] button {
|
||||
border-radius: var(--dt-r-md) !important;
|
||||
}
|
||||
/* The per-file chip rows the uploader emits after a file is staged. */
|
||||
[data-testid="stFileUploaderFile"] {
|
||||
background: var(--dt-surface) !important;
|
||||
border: 1px solid var(--dt-border) !important;
|
||||
border-radius: var(--dt-r-sm) !important;
|
||||
}
|
||||
|
||||
/* ---------- Expanders + bordered containers → editorial cards ---------- */
|
||||
[data-testid="stExpander"] details,
|
||||
[data-testid="stExpander"] {
|
||||
background: var(--dt-surface) !important;
|
||||
border: 1px solid var(--dt-border) !important;
|
||||
border-radius: var(--dt-r-lg) !important;
|
||||
overflow: hidden !important;
|
||||
box-shadow: 0 1px 2px rgba(28,25,23,0.03);
|
||||
}
|
||||
[data-testid="stExpander"] details > summary {
|
||||
background: var(--dt-surface-hover) !important;
|
||||
border-bottom: 1px solid var(--dt-border) !important;
|
||||
padding: 12px 16px !important;
|
||||
font-weight: 500 !important;
|
||||
color: var(--dt-ink) !important;
|
||||
}
|
||||
[data-testid="stExpander"] details[open] > summary {
|
||||
border-bottom: 1px solid var(--dt-border) !important;
|
||||
}
|
||||
[data-testid="stExpander"] details > div {
|
||||
padding: 14px 16px !important;
|
||||
}
|
||||
|
||||
/* ``st.container(border=True)`` — same card treatment. */
|
||||
[data-testid="stVerticalBlockBorderWrapper"],
|
||||
div[data-testid="stContainer"][data-border="true"] {
|
||||
background: var(--dt-surface) !important;
|
||||
border: 1px solid var(--dt-border) !important;
|
||||
border-radius: var(--dt-r-lg) !important;
|
||||
box-shadow: 0 1px 2px rgba(28,25,23,0.03);
|
||||
}
|
||||
|
||||
/* ---------- Alerts — soft fills, no harsh borders ---------- */
|
||||
[data-testid="stAlert"] [data-testid="stAlertContainer"],
|
||||
[data-testid="stAlertContainer"] {
|
||||
border-radius: var(--dt-r-md) !important;
|
||||
border: 1px solid transparent !important;
|
||||
padding: 10px 14px !important;
|
||||
font-size: 13.5px !important;
|
||||
}
|
||||
/* Streamlit tags each alert kind on the wrapper; target both the
|
||||
legacy class hooks and the newer per-kind ``data-baseweb-color``. */
|
||||
[data-testid="stAlertContainer"][kind="info"],
|
||||
.stAlert[data-baseweb="notification"][kind="info"] {
|
||||
background: var(--dt-info-fill) !important;
|
||||
color: var(--dt-info) !important;
|
||||
}
|
||||
[data-testid="stAlertContainer"][kind="success"],
|
||||
.stAlert[data-baseweb="notification"][kind="success"] {
|
||||
background: var(--dt-success-fill) !important;
|
||||
color: var(--dt-success) !important;
|
||||
}
|
||||
[data-testid="stAlertContainer"][kind="warning"],
|
||||
.stAlert[data-baseweb="notification"][kind="warning"] {
|
||||
background: var(--dt-warn-fill) !important;
|
||||
color: var(--dt-warn) !important;
|
||||
}
|
||||
[data-testid="stAlertContainer"][kind="error"],
|
||||
.stAlert[data-baseweb="notification"][kind="error"] {
|
||||
background: var(--dt-danger-fill) !important;
|
||||
color: var(--dt-danger) !important;
|
||||
}
|
||||
|
||||
/* ---------- Inputs (text, select, multiselect) — paper surface ---------- */
|
||||
[data-testid="stTextInput"] input,
|
||||
[data-testid="stTextArea"] textarea,
|
||||
[data-testid="stNumberInput"] input,
|
||||
[data-testid="stSelectbox"] div[role="combobox"],
|
||||
[data-testid="stMultiSelect"] div[role="combobox"],
|
||||
[data-baseweb="select"] > div {
|
||||
background: var(--dt-surface) !important;
|
||||
border-radius: var(--dt-r-sm) !important;
|
||||
border-color: var(--dt-border-strong) !important;
|
||||
font-family: var(--dt-font-body) !important;
|
||||
}
|
||||
|
||||
/* Divider — softer warm gray instead of cool Streamlit default. */
|
||||
[data-testid="stMarkdownContainer"] hr,
|
||||
.stApp hr {
|
||||
border-color: var(--dt-border) !important;
|
||||
}
|
||||
|
||||
/* Tabs — pill-style with active underline in accent. */
|
||||
[data-testid="stTabs"] [role="tab"] {
|
||||
font-family: var(--dt-font-body) !important;
|
||||
font-size: 13.5px !important;
|
||||
color: var(--dt-ink-secondary) !important;
|
||||
}
|
||||
[data-testid="stTabs"] [role="tab"][aria-selected="true"] {
|
||||
color: var(--dt-ink) !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
/* DataFrame surface — warm card, mono cells. */
|
||||
[data-testid="stDataFrame"] {
|
||||
border-radius: var(--dt-r-md) !important;
|
||||
border: 1px solid var(--dt-border) !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
|
||||
|
||||
def hide_streamlit_chrome(*, gate_license: bool = True) -> None:
|
||||
"""Inject CSS to hide Streamlit's default header, menu, and footer.
|
||||
|
||||
@@ -154,6 +467,7 @@ def hide_streamlit_chrome(*, gate_license: bool = True) -> None:
|
||||
can render its own form without recursion.
|
||||
"""
|
||||
st.markdown(_HIDE_CHROME_CSS, unsafe_allow_html=True)
|
||||
st.markdown(_DESIGN_TOKENS_CSS, unsafe_allow_html=True)
|
||||
# Stamp a session-start record into the audit log the first time
|
||||
# any page renders. Idempotent — subsequent calls are no-ops.
|
||||
# Wrapped because a broken audit log MUST NOT take the GUI down.
|
||||
|
||||
Reference in New Issue
Block a user