Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

- **PR #2222** by @franksong2702 — Settings → Appearance now wraps the "Load older messages while scrolling up" checkbox in its own `<label>` AND moves it into its own `settings-field` div instead of leaving it orphaned after the session-jump description with a stray closing `</label>`. Stage-353 maintainer resolution adopted PR #2227's stronger structural variant (each preference in its own `settings-field`) over PR #2222's smaller in-place wrap. Regression test `test_session_endless_scroll.py` pins the new per-label per-settings-field contract.

- **Issue #2231** by @franksong2702 — Phone panel navigation now uses the tablet-like vertical rail instead of cramming the panel icons into a horizontal top tab row. The phone rail keeps 44×44px icon targets available at the left edge, reserves matching main-content space, slides the sidebar panel out beside the rail, and keeps `.panel-icon-btn` controls at 44×44px minimum touch targets.

### Added

- **PR #2225** by @franksong2702 (refs #2224) — Adds an Extra Large option to Settings → Appearance → Font size for tablet and large-desktop readability. The new `xlarge` value is accepted by the persisted settings contract, appears alongside the existing Small / Default / Large picker options, and scales the same key UI text surfaces already covered by the font-size preference: sidebar session rows, chat message bodies/headings/code/tables, the composer textarea, workspace file rows, and app-level em/rem text. The picker grid now uses `repeat(auto-fit, minmax(96px,1fr))` instead of a fixed 3-column grid so the fourth option doesn't crowd narrow viewports.
Expand Down
Binary file added docs/pr-media/2231/after-phone.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2231/after-tablet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2231/before-phone.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2231/before-tablet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions static/panels.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,16 @@ async function switchPanel(name, opts = {}) {
switchSettingsSection(_currentSettingsSection);
loadSettingsPanel();
}
if (opts.fromRailClick && typeof _isDesktopWidth === 'function' && !_isDesktopWidth()) {
if (nextPanel === 'chat') {
if (typeof closeMobileSidebar === 'function') closeMobileSidebar();
} else {
const sidebar = document.querySelector('.sidebar');
const overlay = document.getElementById('mobileOverlay');
if (sidebar) sidebar.classList.add('mobile-open');
if (overlay) overlay.classList.add('visible');
}
}
syncAppTitlebar();
return true;
}
Expand Down
15 changes: 14 additions & 1 deletion static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -895,7 +895,7 @@
.msg-body td{padding:5px 10px;border:1px solid rgba(255,255,255,.06);}
.msg-body tr:nth-child(even){background:rgba(255,255,255,.03);}
/* #486: inline code inside table cells needs scaled sizing to avoid overflow/clipping */
.msg-body td code,.msg-body th code{font-size:0.85em;padding:1px 4px;vertical-align:baseline;}
.msg-body td p,.msg-body th p{margin:0;}.msg-body td code,.msg-body th code{font-size:0.85em;padding:1px 4px;vertical-align:baseline;}
/* KaTeX math rendering */
.katex-block{display:block;text-align:center;margin:12px 0;overflow-x:auto;}
.katex-inline{display:inline;}
Expand Down Expand Up @@ -1522,6 +1522,19 @@
.composer-terminal-action{padding:5px 7px;font-size:10px;white-space:nowrap;}
#terminalWorkspaceLabel{max-width:110px;}
#terminalDockWorkspaceLabel{max-width:96px;}
/* Phone navigation uses the same vertical rail model as tablet/desktop,
keeping primary panel icons reachable without a cramped top tab row. */
.rail{display:flex;position:fixed;left:0;top:calc(38px + var(--app-titlebar-safe-top));bottom:0;width:52px;z-index:201;padding:8px 4px;}
.rail-btn{width:44px;height:44px;border-radius:10px;}
.sidebar > .sidebar-nav{display:none;}
.main{margin-left:52px;}
.app-titlebar{justify-content:center;}
.app-titlebar-hamburger,.app-titlebar-spacer{display:none;}
.app-titlebar-inner{flex:0 1 auto;}
.sidebar{--mobile-sidebar-width:min(280px, calc(100vw - 52px));left:calc(-1 * var(--mobile-sidebar-width));top:calc(38px + var(--app-titlebar-safe-top));width:var(--mobile-sidebar-width);}
.sidebar.mobile-open{left:52px;}
.mobile-overlay{left:52px;top:calc(38px + var(--app-titlebar-safe-top));}
.panel-icon-btn{min-width:44px;min-height:44px;width:auto;height:auto;}
/* Touch targets — minimum 44px */
.icon-btn,.mic-btn,.voice-mode-btn{min-width:44px;min-height:44px;}
.session-item{min-height:44px;padding:10px 40px 10px 12px;}
Expand Down
71 changes: 67 additions & 4 deletions tests/test_mobile_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,12 +391,75 @@ def test_sidebar_nav_present():
".sidebar-nav CSS rule missing from style.css"


def test_mobile_does_not_hide_sidebar_nav():
"""Phone breakpoint must keep the sidebar top navigation visible."""
def test_mobile_keeps_panel_navigation_available():
"""Phone breakpoint must keep panel navigation available via the rail."""
mobile_css = "\n".join(_max_width_media_blocks(640))
assert mobile_css, "Missing @media(max-width:640px) block in style.css"
assert ".sidebar-nav{display:none" not in mobile_css.replace(" ", ""), \
".sidebar-nav must stay visible on mobile"
assert re.search(r'\.rail\{[^}]*display:\s*flex', mobile_css), \
"Phone panel navigation must remain available through the vertical rail"


def test_mobile_uses_vertical_rail_navigation_with_44px_targets():
"""Phone panel navigation should use the tablet-like vertical rail.

The old phone-only sidebar top tab row cramped 10+ icons into one row.
Phones should expose the same rail model as tablet/desktop, with 44px
touch targets and the sidebar panel sliding out beside the rail.
"""
mobile_css = "\n".join(_max_width_media_blocks(640))
assert re.search(r'\.rail\{[^}]*display:\s*flex', mobile_css), (
"Phone breakpoint must display the primary vertical rail"
)
assert re.search(r'\.rail\{[^}]*position:\s*fixed', mobile_css), (
"Phone rail should be fixed so it stays available while panels scroll"
)
assert re.search(r'\.rail\{[^}]*width:\s*52px', mobile_css), (
"Phone rail should reserve stable side space for 44px icon targets"
)
assert re.search(r'\.rail-btn\{[^}]*width:\s*44px', mobile_css), (
".rail-btn must be 44px wide on phone"
)
assert re.search(r'\.rail-btn\{[^}]*height:\s*44px', mobile_css), (
".rail-btn must be 44px tall on phone"
)
assert re.search(r'\.sidebar\s*>\s*\.sidebar-nav\{[^}]*display:\s*none', mobile_css), (
"Phone breakpoint should hide the cramped horizontal sidebar tab row"
)
assert re.search(r'\.main\{[^}]*margin-left:\s*52px', mobile_css), (
"Main content should reserve room for the fixed phone rail"
)
assert re.search(r'\.sidebar\.mobile-open\{[^}]*left:\s*52px', mobile_css), (
"Open phone sidebar should slide out beside the rail, not cover it"
)
assert re.search(r'\.panel-icon-btn\{[^}]*min-width:\s*44px', mobile_css), (
".panel-icon-btn must min-width:44px on phone"
)
assert re.search(r'\.panel-icon-btn\{[^}]*min-height:\s*44px', mobile_css), (
".panel-icon-btn must min-height:44px on phone"
)
assert re.search(r'\.panel-icon-btn\{[^}]*width:\s*auto', mobile_css), (
".panel-icon-btn must override its base 24px width on phone"
)
assert re.search(r'\.panel-icon-btn\{[^}]*height:\s*auto', mobile_css), (
".panel-icon-btn must override its base 24px height on phone"
)


def test_mobile_rail_click_opens_sidebar_for_non_chat_panels():
"""Rail clicks on phone must reveal the selected sidebar panel."""
panels_js = (REPO / "static" / "panels.js").read_text(encoding="utf-8")
assert "opts.fromRailClick" in panels_js, (
"switchPanel() should distinguish rail clicks from programmatic switches"
)
assert "!_isDesktopWidth()" in panels_js, (
"Rail-click sidebar opening must be limited to mobile widths"
)
assert "sidebar.classList.add('mobile-open')" in panels_js, (
"Phone rail clicks on non-chat panels should open the sidebar panel"
)
assert "overlay.classList.add('visible')" in panels_js, (
"Phone rail clicks should show the overlay behind the opened sidebar"
)


def test_mobile_files_button_present():
Expand Down