fix(home): swap the home surface on a fold whatever width the page loaded at

Folding a phone on the home screen left the welcome screen up when the page
had loaded wide (#702): the phone overview's breakpoint listener was wired
from its first show, which a wide-loaded page never reaches. Both home
screens' breakpoint listeners are now installed once from init() and act
only while a home surface is up.

A rail render queued just before the fold could also unhide the desktop
rail behind the phone overview; the debounced refresh now asks again when
it fires.

Tests: test/home-breakpoint-listeners.test.ts (gate) and
test/home-breakpoint-swap.browser.test.ts (browser suite: wide-first and
narrow-first across 599/600 and 767/768, plus a height-only resize).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-09 02:29:51 +01:00
1 parent 86502b6904
commit 7602b6db21
6 files changed
+366 -32

No files matched your search

+5
View File
@@ -1232,6 +1232,11 @@ class CodemanApp {
// and then applies the sidebar variant on top — do not call both.
this.applySessionListLayout();
this.applyTabOrientation();
// The home screens' breakpoint listeners, once, before any home surface is
// shown: a fold or unfold must swap the surface whichever width the page
// loaded at (#702). Both self-gate on a home screen being up.
this._wireMobileOverviewBreakpoint?.();
this._wireHomeSessionsBreakpoint?.();
this.initTabRailResize?.();
this.applyMonitorVisibility();
this.applyLineageLineSettings?.();
+38 -18
View File
@@ -126,20 +126,31 @@ Object.assign(CodemanApp.prototype, {
this._stopHomeSessionsClock();
},
/** Re-render only when showing (called from the tab renderer's tail). */
/**
* Re-render only when showing (called from the tab renderer's tail). Asked
* again when the debounce fires: `renderHomeSessions()` unhides the column,
* so a render queued just before a fold to phone width hid it (#702) would
* otherwise bring it back behind the phone overview.
*/
_refreshHomeSessionsIfVisible() {
if (!this.isHomeSessionsVisible()) return;
this._debouncedCall('homeSessions', () => this.renderHomeSessions(), 150);
this._debouncedCall(
'homeSessions',
() => {
if (this.isHomeSessionsVisible() && this.shouldShowHomeSessions()) this.renderHomeSessions();
},
150
);
},
/**
* One delegated click listener for every row, plus a width listener so
* resizing the window while on the home screen adds or drops the column
* instead of leaving it overlapping the content it was sized to clear.
* One delegated click listener for every row. The width listener lives in
* `_wireHomeSessionsBreakpoint()`, installed at startup.
*/
_wireHomeSessions(el) {
if (this._homeSessionsWired) return;
this._homeSessionsWired = true;
this._wireHomeSessionsBreakpoint();
el.addEventListener('click', (event) => {
const target = event.target?.closest?.('[data-hs-action]');
@@ -150,20 +161,29 @@ Object.assign(CodemanApp.prototype, {
void this.openWebview?.(target.dataset.hsWebview);
}
});
},
if (window.matchMedia) {
const mq = window.matchMedia(`(min-width: ${HOME_SESSIONS_MIN_WIDTH}px)`);
const onChange = () => {
// Only relevant while the welcome screen is up; entering a session
// re-decides through hideWelcome()/showWelcome() anyway.
if (this.activeSessionId) return;
const overlay = document.getElementById('welcomeOverlay');
if (!overlay || !overlay.classList.contains('visible')) return;
this.showHomeSessions();
};
if (mq.addEventListener) mq.addEventListener('change', onChange);
else if (mq.addListener) mq.addListener(onChange);
}
/**
* Add or drop the column when the window crosses its width gate, so it never
* overlaps the content it was sized to clear. Installed ONCE at startup
* (`init()` in app.js) like the phone overview's breakpoint listener, so a
* page that loads on a phone and is unfolded to desktop width gets it too
* (#702). Idempotent.
*/
_wireHomeSessionsBreakpoint() {
if (this._homeSessionsBreakpointWired || !window.matchMedia) return;
this._homeSessionsBreakpointWired = true;
const mq = window.matchMedia(`(min-width: ${HOME_SESSIONS_MIN_WIDTH}px)`);
const onChange = () => {
// Only relevant while the welcome screen is up; entering a session
// re-decides through hideWelcome()/showWelcome() anyway.
if (this.activeSessionId) return;
const overlay = document.getElementById('welcomeOverlay');
if (!overlay || !overlay.classList.contains('visible')) return;
this.showHomeSessions();
};
if (mq.addEventListener) mq.addEventListener('change', onChange);
else if (mq.addListener) mq.addListener(onChange);
},
// ═══════════════════════════════════════════════════════════════
+30 -14
View File
@@ -385,13 +385,13 @@ Object.assign(CodemanApp.prototype, {
},
/**
* One delegated click listener for every row, plus a breakpoint listener so
* rotating or unfolding while on the home screen swaps to the right surface
* instead of stranding a phone layout on a tablet-width viewport.
* One delegated click listener for every row. The breakpoint listener lives
* in `_wireMobileOverviewBreakpoint()`, installed at startup.
*/
_wireMobileOverview(el) {
if (this._mobileOverviewWired) return;
this._mobileOverviewWired = true;
this._wireMobileOverviewBreakpoint();
el.addEventListener('click', (event) => {
const target = event.target && event.target.closest && event.target.closest('[data-mo-action]');
@@ -426,18 +426,34 @@ Object.assign(CodemanApp.prototype, {
this.showWebviewModal();
}
});
},
if (window.matchMedia) {
const mq = window.matchMedia(MOBILE_OVERVIEW_PHONE_QUERY);
const onChange = () => {
// Only relevant while a home surface is up; entering a session re-decides
// through hideWelcome()/showWelcome() anyway.
if (this.activeSessionId) return;
if (typeof this.showWelcome === 'function') this.showWelcome();
};
if (mq.addEventListener) mq.addEventListener('change', onChange);
else if (mq.addListener) mq.addListener(onChange);
}
/**
* Swap the home surface when the width crosses the phone breakpoint (rotate,
* fold, unfold), so a phone layout is never stranded on a tablet-width
* viewport and the welcome screen never on a phone-width one.
*
* Installed ONCE at startup (`init()` in app.js), not on the first show: a
* page that loads wide never shows the phone overview, so a listener wired
* from there was missing on exactly the fold that needed it (#702). Idempotent,
* so the call from `_wireMobileOverview()` adds nothing after startup.
*/
_wireMobileOverviewBreakpoint() {
if (this._mobileOverviewBreakpointWired || !window.matchMedia) return;
this._mobileOverviewBreakpointWired = true;
const mq = window.matchMedia(MOBILE_OVERVIEW_PHONE_QUERY);
const onChange = () => {
// Only relevant while a home surface is up; entering a session re-decides
// through hideWelcome()/showWelcome() anyway, and before the first
// showWelcome() there is nothing on screen to swap.
if (this.activeSessionId) return;
const overlay = document.getElementById('welcomeOverlay');
const welcomeUp = !!overlay && overlay.classList.contains('visible');
if (!welcomeUp && !this.isMobileOverviewVisible()) return;
if (typeof this.showWelcome === 'function') this.showWelcome();
};
if (mq.addEventListener) mq.addEventListener('change', onChange);
else if (mq.addListener) mq.addListener(onChange);
},
_toggleMobileOverviewRunMenu() {