diff --git a/src/gui/_home.py b/src/gui/_home.py index a30af7a..b59aa8e 100644 --- a/src/gui/_home.py +++ b/src/gui/_home.py @@ -279,7 +279,16 @@ def _home_page() -> None: digest = hashlib.sha1( name.encode("utf-8"), usedforsecurity=False, ).hexdigest()[:10] - col_name, col_size, col_x = st.columns([8, 1.6, 0.55]) + # X button on the LEFT of the row per UX feedback — + # ``✕ | filename + chip | size``. + col_x, col_name, col_size = st.columns([0.55, 8, 1.6]) + if col_x.button( + "✕", + key=f"_home_remove_{digest}", + help=f"Remove {name}", + type="tertiary", + ): + to_remove = name col_name.markdown( '
' f'{_DOC_SVG}' @@ -294,13 +303,6 @@ def _home_page() -> None: '
', unsafe_allow_html=True, ) - if col_x.button( - "✕", - key=f"_home_remove_{digest}", - help=f"Remove {name}", - type="tertiary", - ): - to_remove = name # In-card "Add more files" — clicks the (off-screen) # ``stFileUploaderDropzoneInput`` so the OS file picker opens. # Inline ``onclick`` would be cleanest but Streamlit's HTML diff --git a/src/gui/components/_legacy.py b/src/gui/components/_legacy.py index 2661bc2..67b1fb5 100644 --- a/src/gui/components/_legacy.py +++ b/src/gui/components/_legacy.py @@ -179,6 +179,50 @@ body, .stApp { background: #f5f4ef !important; } +/* Brand block at the top of the sidebar (mockup §brand) — a 28px + ink-filled rounded square with the wordmark "D" + "DataTools" + text. Injected into ``[data-testid="stSidebarHeader"]`` by the JS + below; ``stLogoSpacer`` is hidden so the brand block takes its + place flush against the left edge of the sidebar header. */ +.dt-brand { + display: flex !important; + align-items: center; + gap: 10px; + padding: 0 0 0 4px; + margin: 0; + height: 100%; + flex: 1; +} +.dt-brand-mark { + width: 28px; + height: 28px; + border-radius: 7px; + background: var(--ink); + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--accent-fill); + font-family: var(--font-sans); + font-weight: 600; + font-size: 16px; + letter-spacing: -0.02em; + line-height: 1; + flex-shrink: 0; +} +.dt-brand-name { + font-family: var(--font-sans); + font-weight: 600; + font-size: 16px; + letter-spacing: -0.02em; + color: var(--ink); + line-height: 1; +} +/* The stock Streamlit logo placeholder takes 100x32 of space; hide + it so the injected brand has room to breathe. */ +[data-testid="stLogoSpacer"]:not(:has(.dt-brand)) { + display: none !important; +} + /* Section labels in the page-nav: tiny uppercase tracking — the "Eyebrow" row from spec §4. Streamlit renders these as nodes with class ``st-emotion-cache-…`` inside ``stSidebarNav`` — class @@ -200,16 +244,25 @@ body, .stApp { margin: 0 !important; } -/* Nav items: comfortable padding, soft hover. */ +/* Nav items — tight padding so the menu lists feel dense and don't + waste vertical space. */ [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.3 !important; - padding: 7px 10px !important; + line-height: 1.25 !important; + padding: 4px 10px !important; border-radius: var(--r-sm) !important; transition: background 0.12s ease, color 0.12s ease; } +[data-testid="stSidebarNav"] li, +[data-testid="stSidebarNavItems"] > li { + margin-bottom: 1px !important; +} +[data-testid="stSidebarNavSectionHeader"] { + padding-top: 10px !important; + padding-bottom: 2px !important; +} [data-testid="stSidebarNav"] a[data-testid="stSidebarNavLink"]:hover, [data-testid="stSidebarNav"] [data-testid="stSidebarNavLinkContainer"] a:hover { background: rgba(0,0,0,0.04) !important; @@ -393,6 +446,12 @@ div[data-testid="stContainer"][data-border="true"] { border-radius: var(--r-lg) !important; box-shadow: 0 1px 2px rgba(28,25,23,0.03); } +/* Tighten the inter-row gap inside bordered containers — applies to + the Files card rows after import and the findings-card rows alike, + so the dense card body has less wasted vertical whitespace. */ +[data-testid="stVerticalBlockBorderWrapper"] [data-testid="stVerticalBlock"] { + gap: 0.25rem !important; +} /* ---------- Alerts — soft fills, no harsh borders ---------- */ [data-testid="stAlert"] [data-testid="stAlertContainer"], @@ -438,6 +497,39 @@ div[data-testid="stContainer"][data-border="true"] { font-family: var(--font-sans) !important; } +/* Sidebar widget labels — render as the "Eyebrow" row from spec §4 + (tiny uppercase tracking, tertiary ink) so the ``Language`` / + ``Core · 1820 days left`` blocks at the bottom of the sidebar + match the section-title rhythm of the nav above. */ +[data-testid="stSidebar"] [data-testid="stWidgetLabel"] p, +[data-testid="stSidebar"] label[data-testid="stWidgetLabel"] { + font-size: 11.5px !important; + font-weight: 500 !important; + text-transform: uppercase !important; + letter-spacing: 0.08em !important; + color: var(--ink-tertiary) !important; + margin-bottom: 4px !important; +} +/* Sidebar selectbox — quiet outline, cream surface that reads as + part of the sidebar rather than a Streamlit-default white island. */ +[data-testid="stSidebar"] [data-testid="stSelectbox"] div[role="combobox"], +[data-testid="stSidebar"] [data-baseweb="select"] > div { + background: var(--surface) !important; + border: 1px solid var(--border) !important; + border-radius: var(--r-sm) !important; + font-size: 13px !important; + min-height: 32px !important; +} +[data-testid="stSidebar"] [data-testid="stSelectbox"] div[role="combobox"]:hover, +[data-testid="stSidebar"] [data-baseweb="select"] > div:hover { + border-color: var(--border-strong) !important; +} +/* Streamlit pads the selectbox internals; tighten the chevron column + so the control isn't taller than the nav items above it. */ +[data-testid="stSidebar"] [data-baseweb="select"] > div > div { + padding: 4px 8px !important; +} + /* Divider — softer warm gray instead of cool Streamlit default. */ [data-testid="stMarkdownContainer"] hr, .stApp hr { @@ -605,15 +697,57 @@ div[data-testid="stContainer"][data-border="true"] { display: flex; align-items: center; gap: 12px; - padding: 14px 18px; + /* Generous left/right padding so the filename + counts have visible + breathing room against the card's rounded edges — the head bleeds + out to those edges via the negative margin below, so without the + extra padding the content sits flush against the border. */ + padding: 16px 22px; border-bottom: 1px solid var(--border); background: var(--surface-hover); /* -1rem on top/sides bleeds the head to the card edges (the parent - ``st.container(border=True)`` has 1rem padding). +0.75rem on the - bottom is the breathing room before the first finding row — - without it the row sits flush against the head's bottom border. */ - margin: -1rem -1rem 0.75rem; + ``st.container(border=True)`` has 1rem padding). +1.5rem on the + bottom is breathing room before the first finding row — without + it the row sits flush against the head's bottom border. */ + margin: -1rem -1rem 1.5rem; border-radius: var(--r-lg) var(--r-lg) 0 0; + cursor: pointer; + user-select: none; + transition: background 0.12s ease; +} +.dt-finding-group-head:hover { + background: var(--accent-fill); +} +/* Chevron lives on the right of the head, rotates to indicate state. */ +.dt-finding-group-chevron { + margin-left: 8px; + color: var(--ink-tertiary); + font-family: "Material Symbols Outlined" !important; + font-size: 20px !important; + font-feature-settings: normal !important; + line-height: 1 !important; + transition: transform 0.15s ease; + flex-shrink: 0; +} +.dt-finding-group-head[data-dt-collapsed="false"] .dt-finding-group-chevron { + transform: rotate(90deg); +} +/* Collapsed = body rows hidden + head tucks tight against card bottom. + The head's siblings inside the bordered container are the + ``stHorizontalBlock``s emitted by each ``st.columns`` row — when the + head carries ``data-dt-collapsed="true"`` they collapse to nothing + and the head's bottom border becomes the card's bottom edge. */ +.dt-finding-group-head[data-dt-collapsed="true"] { + margin: -1rem -1rem -1rem; + border-bottom: none; + border-radius: var(--r-lg); +} +/* Hide every sibling that comes AFTER the head's element-container + (the rows are emitted as ``stLayoutWrapper`` or + ``stElementContainer`` siblings depending on Streamlit's internal + layout reducer; ``~ *`` matches both and survives future renames). */ +[data-testid="stElementContainer"]:has(.dt-finding-group-head[data-dt-collapsed="true"]) + ~ * { + display: none !important; } .dt-severity-dot { width: 8px; height: 8px; @@ -770,6 +904,85 @@ div[data-testid="stContainer"][data-border="true"] { # script walks the dropzone buttons after first paint and rewrites the # label to "Import" — and re-runs on Streamlit's component-rerender # DOM mutations so the swap survives navigation and reruns. +# Injects the sidebar brand block (mockup §brand) at the top of +# Streamlit's ``stSidebarHeader``: the 28px ink-filled rounded square +# with the "D" wordmark followed by the "DataTools" word. Streamlit's +# ``stLogoSpacer`` reserves the slot but doesn't render anything +# without a ``st.logo()`` call; we replace its content rather than +# call ``st.logo`` because the brand wants both a chip AND wordmark +# in one block, which ``st.logo`` can't do without shipping a static +# image asset. MutationObserver re-injects when Streamlit remounts +# the sidebar header. +_INJECT_BRAND_JS = """ + +""" + + +# Toggle a ``.dt-finding-group-head``'s ``data-dt-collapsed`` attribute +# on click. CSS handles the visual collapse (hide siblings, tuck the +# head against the card bottom) — all this script does is flip the +# attribute. MutationObserver re-binds when Streamlit remounts heads. +_WIRE_COLLAPSIBLE_FINDINGS_JS = """ + +""" + + _RENAME_UPLOAD_BUTTON_JS = """