fix(ux): add expand control for update summary panel (#4705)

Move scrolling to an inner container and add an Expand/Collapse toggle
so long generated summaries are readable on narrow viewports.

Fixes #4705
This commit is contained in:
nankingjing 2026-06-29 22:43:12 +08:00 committed by nesquena-hermes
parent 3fde698232
commit cabbb9a841
5 changed files with 78 additions and 6 deletions

View file

@ -400,8 +400,13 @@
<span id="updateMsg"></span>
<div id="updateWhatsNewLinks" style="display:none;font-size:11px;margin-left:8px;white-space:nowrap"></div>
<div id="updateSummaryPanel" style="display:none;font-size:12px;line-height:1.45;margin-top:6px;padding:8px 10px;border:1px solid var(--border2);border-radius:8px;background:rgba(255,255,255,.04);max-width:720px;white-space:normal">
<div id="updateSummaryText"></div>
<div id="updateSummaryDiffLinks" style="display:none;font-size:11px;margin-top:8px"></div>
<div id="updateSummaryToolbar" style="display:none;justify-content:flex-end;margin-bottom:6px">
<button type="button" id="btnUpdateSummaryExpand" class="linklike update-summary-toggle" aria-expanded="false" onclick="toggleUpdateSummaryExpanded()">Expand summary</button>
</div>
<div id="updateSummaryScroll">
<div id="updateSummaryText"></div>
<div id="updateSummaryDiffLinks" style="display:none;font-size:11px;margin-top:8px"></div>
</div>
</div>
<div id="updateError" style="display:none;font-size:12px;color:var(--error,#e05);margin-top:4px;word-break:break-word"></div>
</div>

View file

@ -1931,8 +1931,11 @@
/* ── Update banner ── */
.update-banner{display:none;background:var(--surface);border:1px solid var(--accent);border-radius:10px;padding:10px 16px;margin:10px auto;max-width:780px;width:calc(100% - 24px);box-sizing:border-box;font-size:13px;color:var(--accent-text);align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;overflow-wrap:anywhere;}
.update-banner.visible{display:flex;}
#updateMsg,#updateWhatsNewLinks,#updateSummaryPanel,#updateSummaryDiffLinks{max-width:100%;overflow-wrap:anywhere;word-break:break-word;}
#updateSummaryPanel{max-height:min(34vh,260px);overflow:auto;overscroll-behavior:contain;scrollbar-gutter:stable;scrollbar-width:thin;scrollbar-color:var(--accent) transparent;}
#updateMsg,#updateWhatsNewLinks,#updateSummaryPanel,#updateSummaryScroll,#updateSummaryDiffLinks{max-width:100%;overflow-wrap:anywhere;word-break:break-word;}
#updateSummaryScroll{max-height:min(34vh,260px);overflow:auto;overscroll-behavior:contain;scrollbar-gutter:stable;scrollbar-width:thin;scrollbar-color:var(--accent) transparent;}
#updateSummaryPanel.update-summary-expanded #updateSummaryScroll{max-height:min(75vh,560px);}
@media (max-width:600px){#updateSummaryPanel.update-summary-expanded #updateSummaryScroll{max-height:min(82vh,640px);}}
.update-summary-toggle{font-size:11px;color:var(--accent);text-decoration:underline;background:none;border:0;padding:0;cursor:pointer;}
#updateWhatsNewLinks{white-space:normal!important;margin-left:0!important;}
.update-banner>div:last-child{margin-left:auto;}
@media (max-width:600px){.update-banner{width:calc(100% - 16px);padding:10px 12px;gap:10px;}.update-banner>div:last-child{width:100%;justify-content:flex-end;}}

View file

@ -8234,10 +8234,29 @@ function _hideUpdateSummaryPanel(){
const panel=$('updateSummaryPanel');
const text=$('updateSummaryText');
const links=$('updateSummaryDiffLinks');
if(panel) panel.style.display='none';
const toolbar=$('updateSummaryToolbar');
if(panel){
panel.style.display='none';
panel.classList.remove('update-summary-expanded');
}
if(toolbar) toolbar.style.display='none';
_syncUpdateSummaryExpandButton(false);
if(text) text.textContent='';
if(links){links.replaceChildren();links.style.display='none';}
}
function _syncUpdateSummaryExpandButton(expanded){
const btn=$('btnUpdateSummaryExpand');
if(!btn) return;
btn.setAttribute('aria-expanded',expanded?'true':'false');
btn.textContent=expanded?'Collapse summary':'Expand summary';
}
function toggleUpdateSummaryExpanded(){
const panel=$('updateSummaryPanel');
if(!panel||panel.style.display==='none') return;
const expanded=!panel.classList.contains('update-summary-expanded');
panel.classList.toggle('update-summary-expanded',expanded);
_syncUpdateSummaryExpandButton(expanded);
}
const WHATS_NEW_SUMMARY_STORAGE_KEY='hermes-whats-new-generated-summaries';
function _loadStoredUpdateSummaries(){
window._whatsNewGeneratedSummaries=window._whatsNewGeneratedSummaries||{};
@ -8290,8 +8309,12 @@ function _renderUpdateSummaryPanel(payload,data,targetKey){
const panel=$('updateSummaryPanel');
const text=$('updateSummaryText');
const links=$('updateSummaryDiffLinks');
const toolbar=$('updateSummaryToolbar');
if(!panel||!text) return;
panel.style.display='block';
panel.classList.remove('update-summary-expanded');
_syncUpdateSummaryExpandButton(false);
if(toolbar) toolbar.style.display='flex';
const sections=Array.isArray(payload&&payload.summary_sections)?payload.summary_sections:null;
text.replaceChildren();
if(sections&&sections.length){

View file

@ -0,0 +1,40 @@
"""Static-analysis tests for #4705 (update summary panel expand control)."""
from pathlib import Path
ROOT = Path(__file__).parent.parent
INDEX_HTML = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
class TestIssue4705UpdateSummaryExpand:
def test_summary_panel_has_expand_control(self):
assert 'id="updateSummaryToolbar"' in INDEX_HTML
assert 'id="btnUpdateSummaryExpand"' in INDEX_HTML
assert 'id="updateSummaryScroll"' in INDEX_HTML
assert 'onclick="toggleUpdateSummaryExpanded()"' in INDEX_HTML
def test_toggle_function_toggles_expanded_class(self):
assert "function toggleUpdateSummaryExpanded()" in UI_JS
assert "update-summary-expanded" in UI_JS
assert "_syncUpdateSummaryExpandButton" in UI_JS
def test_render_shows_toolbar_and_resets_collapsed(self):
idx = UI_JS.find("function _renderUpdateSummaryPanel(")
assert idx >= 0
body = UI_JS[idx:idx + 1200]
assert "updateSummaryToolbar" in body
assert "classList.remove('update-summary-expanded')" in body
assert "_syncUpdateSummaryExpandButton(false)" in body
def test_hide_clears_expanded_state(self):
idx = UI_JS.find("function _hideUpdateSummaryPanel(")
assert idx >= 0
body = UI_JS[idx:idx + 700]
assert "classList.remove('update-summary-expanded')" in body
assert "_syncUpdateSummaryExpandButton(false)" in body
def test_expanded_css_uses_larger_viewport_height(self):
assert "#updateSummaryScroll{max-height:min(34vh,260px)" in STYLE_CSS
assert "#updateSummaryPanel.update-summary-expanded #updateSummaryScroll{max-height:min(75vh,560px);}" in STYLE_CSS
assert "@media (max-width:600px){#updateSummaryPanel.update-summary-expanded #updateSummaryScroll{max-height:min(82vh,640px);}}" in STYLE_CSS

View file

@ -2120,7 +2120,8 @@ class TestWhatsNewSummaryToggle:
def test_update_summary_panel_is_scrollable_for_long_summaries(self):
style = read('static/style.css')
assert '#updateSummaryPanel{max-height:min(34vh,260px);overflow:auto;overscroll-behavior:contain;scrollbar-gutter:stable;scrollbar-width:thin;scrollbar-color:var(--accent) transparent;}' in style
assert '#updateSummaryScroll{max-height:min(34vh,260px);overflow:auto;overscroll-behavior:contain;scrollbar-gutter:stable;scrollbar-width:thin;scrollbar-color:var(--accent) transparent;}' in style
assert '#updateSummaryPanel.update-summary-expanded #updateSummaryScroll{max-height:min(75vh,560px);}' in style
def test_update_summary_many_updates_caps_commit_input_and_discloses_scope(self, monkeypatch):
import api.updates as upd