From f664aafeede9b8031eafb4b3db42a18214eee992 Mon Sep 17 00:00:00 2001 From: ducoterra Date: Mon, 7 Sep 2026 14:16:41 -0400 Subject: [PATCH] fix(ui): give the mobile refresh controls labels and full width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At <=640px the RAG "Sync sources" pill and the History "Refresh" pill squeezed down to tiny icon-only buttons — hard to discover and tap on a phone. They are now full-width labeled pills: - the RAG page-head row wraps so the Sync pill drops below the "Knowledge base" title at full width; the History page-head already wrapped the pill below its title block - the Sync label's min(16rem, 40vw) cap lifts on mobile (min-width: 0 engages the ellipsis) so the live-file text truncates against the full width instead of the 40vw cap - the Refresh glyph joins its visible label (it stays hidden on desktop, where the label carries the pill) This matches the established mobile full-width pill language (New chat / Share / stale-ban Regenerate). The three unit tests that pinned the old icon-only CSS are updated to pin the new behavior. --- frontend/assets/styles.css | 29 ++++++++++++++----------- frontend/index.html | 7 +++--- tests/unit/test_frontend_router.py | 12 +++++----- tests/unit/test_frontend_sync_upload.py | 14 ++++++++---- tests/unit/test_hamburger_nav.py | 1 - 5 files changed, 36 insertions(+), 27 deletions(-) diff --git a/frontend/assets/styles.css b/frontend/assets/styles.css index 6c5452e..aca9571 100644 --- a/frontend/assets/styles.css +++ b/frontend/assets/styles.css @@ -2259,8 +2259,8 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } .history-refresh:hover { background: #f55a72; color: var(--bg); } .history-refresh:disabled { opacity: 0.6; cursor: wait; } /* The refresh glyph is hidden on desktop (the label carries the - pill); it is the whole control below 640px (the <=640 block - mirrors the phase-46 auth-link icon-only convention). */ + pill); below 640px it joins the visible label in the full-width + pill (the <=640 block — the icon-only squeeze is gone). */ .history-refresh svg { width: 16px; height: 16px; display: none; } /* ---------- History page (phase 50) ---------- @@ -3663,17 +3663,20 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } .shared-note { font-size: 0.88rem; } .footer-inner { flex-direction: column; gap: 0.2rem; text-align: center; } main { padding-bottom: env(safe-area-inset-bottom, 0); } - /* Sync button goes icon-only on mobile; the label hides, aria-label - keeps the accessible name. The spinning icon is the visible running - state on a touch screen. */ - .sync-btn { padding: 0.4rem 0.3rem; } - .sync-label { display: none; } - /* Phase 77 task 03: the History refresh pill goes icon-only like - the phase-46 auth pill (the aria-label keeps the accessible - name); the page-head row above wraps the pill below the title - block when the width runs out. */ - .history-refresh { padding: 0.4rem 0.3rem; } - .history-refresh-label { display: none; } + /* Sync + History refresh: full-width LABELED pills on mobile (the + icon-only squeeze is gone — a symbol-only control is too small a + target to discover on a phone). The RAG page-head row wraps so + the Sync pill drops below the "Knowledge base" title at full + width; the History page-head already wraps the pill below its + title block. The Sync label's min(16rem, 40vw) cap lifts so the + live-file text ellipsizes against the FULL width (min-width: 0 + lets the nowrap flex item shrink, which is what engages the + ellipsis); the refresh glyph joins its visible label. The + aria-labels keep the accessible names. */ + .page-head-row { flex-wrap: wrap; } + .sync-btn { width: 100%; } + .sync-label { max-width: none; min-width: 0; } + .history-refresh { width: 100%; } .history-refresh svg { display: block; } .sync-result { position: absolute !important; diff --git a/frontend/index.html b/frontend/index.html index 0bd7feb..d1072af 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -691,9 +691,10 @@ page-head — OUTSIDE the table wrap — so it stays reachable while the empty state is showing. The house inline-SVG refresh glyph (aria-hidden) + the visible - "Refresh" label (the phase-46 auth-link convention: - label visible >=640px, icon-only below; the aria-label - keeps the accessible name in both). history.js binds it + "Refresh" label (visible at every width — below 640px + the pill goes full width and the glyph joins the + label; the aria-label keeps the accessible name in + both). history.js binds it in the admin branch (the view is admin-gated; anonymous sees the gate instead) and announces the outcome in #history-status. --> diff --git a/tests/unit/test_frontend_router.py b/tests/unit/test_frontend_router.py index c1931cf..29c5bab 100644 --- a/tests/unit/test_frontend_router.py +++ b/tests/unit/test_frontend_router.py @@ -721,9 +721,9 @@ def test_history_refresh_button_css_reuses_the_new_chat_language() -> None: hidden on desktop (the label carries the pill) — and the global :focus-visible ring applies (no button-scoped focus override). The page-head flex row is SCOPED to #view-history (the other four - views' page-heads are untouched). Below 640px the pill goes - icon-only (the phase-46 auth-link convention — the aria-label - keeps the accessible name).""" + views' page-heads are untouched). Below 640px the pill goes full + width with the label visible (the icon-only squeeze is gone — + the glyph joins the label).""" css = _asset("styles.css") block = re.search(r"\.history-refresh \{([\s\S]*?)\n\}", css) assert block, "styles.css must style .history-refresh" @@ -746,11 +746,11 @@ def test_history_refresh_button_css_reuses_the_new_chat_language() -> None: mobile = re.search(r"@media \(max-width: 640px\) \{([\s\S]*?)\n\}\n", css) assert mobile, "the 640px media query must exist" mbody = mobile.group(1) - assert ".history-refresh-label { display: none; }" in mbody, ( - "icon-only below 640px (the phase-46 convention)" + assert ".history-refresh { width: 100%; }" in mbody, ( + "full width below 640px (the icon-only squeeze is gone)" ) assert ".history-refresh svg { display: block; }" in mbody, ( - "the glyph is the whole control below 640px" + "the glyph joins the visible label below 640px" ) diff --git a/tests/unit/test_frontend_sync_upload.py b/tests/unit/test_frontend_sync_upload.py index 68096cb..2fb7c94 100644 --- a/tests/unit/test_frontend_sync_upload.py +++ b/tests/unit/test_frontend_sync_upload.py @@ -380,8 +380,10 @@ def test_sync_label_css_ellipsis_truncation() -> None: """.sync-label: the live-file label ellipsizes a long source/relative/path inside the pill (A4) — inline-block with the min(16rem, 40vw) cap, overflow hidden, text-overflow ellipsis, no - wrap, baseline-aligned; the mobile squeeze's display:none override - (icon-only button) stays.""" + wrap, baseline-aligned; on mobile the pill goes full width with + the label visible (the cap lifts, and min-width: 0 engages the + ellipsis against the full width — the icon-only squeeze is + gone).""" css = _css() block = re.search(r"\.sync-label\s*\{([^}]*)\}", css) assert block, "styles.css must style .sync-label" @@ -397,8 +399,12 @@ def test_sync_label_css_ellipsis_truncation() -> None: assert prop in body, f".sync-label must carry {prop!r}" mobile = re.search(r"@media \(max-width: 640px\) \{([\s\S]*?)\n\}", css) assert mobile, "the ≤640px media query must stay" - assert ".sync-label { display: none; }" in mobile.group(1), ( - "the mobile icon-only override must survive the ellipsis rule" + mbody = mobile.group(1) + assert ".sync-btn { width: 100%; }" in mbody, ( + "the mobile Sync pill is full width (the icon-only squeeze is gone)" + ) + assert ".sync-label { max-width: none; min-width: 0; }" in mbody, ( + "the label stays visible and ellipsizes against the full width" ) diff --git a/tests/unit/test_hamburger_nav.py b/tests/unit/test_hamburger_nav.py index e9ac58a..17b9d58 100644 --- a/tests/unit/test_hamburger_nav.py +++ b/tests/unit/test_hamburger_nav.py @@ -326,7 +326,6 @@ def test_mobile_menu_rows_are_comfortable_targets() -> None: for untouched in ( ".new-chat-label { display: none; }", ".auth-label { display: none; }", - ".sync-label { display: none; }", ":root { --header-h: 58px; }", ): assert untouched in mobile, f"the ≤640px block lost {untouched!r}"