committed by
Nathan Esquenazi
parent
b394efce17
commit
db392bd532
@@ -63,7 +63,7 @@ actions. The topbar remains focused on conversation context and the workspace/fi
|
|||||||
panels.js Cron, skills, memory, workspace, profiles, todo, settings (~974 lines)
|
panels.js Cron, skills, memory, workspace, profiles, todo, settings (~974 lines)
|
||||||
commands.js Slash command registry, parser, autocomplete dropdown (~156 lines)
|
commands.js Slash command registry, parser, autocomplete dropdown (~156 lines)
|
||||||
onboarding.js First-run wizard overlay, provider setup flow, and settings/workspace orchestration.
|
onboarding.js First-run wizard overlay, provider setup flow, and settings/workspace orchestration.
|
||||||
boot.js Event wiring, mobile nav, voice input, boot IIFE (~338 lines)
|
boot.js Event wiring, mobile sidebar/workspace nav, voice input, boot IIFE (~338 lines)
|
||||||
tests/
|
tests/
|
||||||
conftest.py Isolated test server (port 8788, separate HERMES_HOME) (~240 lines)
|
conftest.py Isolated test server (port 8788, separate HERMES_HOME) (~240 lines)
|
||||||
test_sprint{1-20b}.py Feature tests per sprint (21 files, 415 test functions)
|
test_sprint{1-20b}.py Feature tests per sprint (21 files, 415 test functions)
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -277,8 +277,8 @@ WireGuard. Install it on your server and your phone, and they join the same
|
|||||||
private network -- no port forwarding, no SSH tunnels, no public exposure.
|
private network -- no port forwarding, no SSH tunnels, no public exposure.
|
||||||
|
|
||||||
The Hermes Web UI is fully responsive with a mobile-optimized layout
|
The Hermes Web UI is fully responsive with a mobile-optimized layout
|
||||||
(hamburger sidebar, bottom navigation bar, touch-friendly controls), so it
|
(hamburger sidebar, sidebar top tabs in the drawer, touch-friendly controls),
|
||||||
works well as a daily-driver agent interface from your phone.
|
so it works well as a daily-driver agent interface from your phone.
|
||||||
|
|
||||||
**Setup:**
|
**Setup:**
|
||||||
|
|
||||||
@@ -451,10 +451,10 @@ across 53 test files.
|
|||||||
|
|
||||||
### Mobile responsive
|
### Mobile responsive
|
||||||
- Hamburger sidebar -- slide-in overlay on mobile (<640px)
|
- Hamburger sidebar -- slide-in overlay on mobile (<640px)
|
||||||
- Bottom navigation bar -- 5-tab iOS-style fixed bar
|
- Sidebar top tabs stay available on mobile; no fixed bottom nav stealing chat height
|
||||||
- Files slide-over panel from right edge
|
- Files slide-over panel from right edge
|
||||||
- Touch targets minimum 44px on all interactive elements
|
- Touch targets minimum 44px on all interactive elements
|
||||||
- Composer positioned above bottom nav
|
- Full-height chat/composer on phones without bottom-nav spacing
|
||||||
- Desktop layout completely unchanged
|
- Desktop layout completely unchanged
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -542,7 +542,7 @@ A run of focused quality-of-life improvements: terminal tool approval prompts th
|
|||||||
Added the 7th built-in theme: pure black backgrounds with warm accents tuned to reduce burn-in risk. Small diff, big impact for anyone on an OLED display.
|
Added the 7th built-in theme: pure black backgrounds with warm accents tuned to reduce burn-in risk. Small diff, big impact for anyone on an OLED display.
|
||||||
|
|
||||||
**[@Bobby9228](https://github.com/Bobby9228)** — Mobile Profiles button + Android Chrome fixes (PRs #253, #263, #265)
|
**[@Bobby9228](https://github.com/Bobby9228)** — Mobile Profiles button + Android Chrome fixes (PRs #253, #263, #265)
|
||||||
Added the Profiles tab to the mobile bottom navigation bar, making profile switching reachable on phones, plus a set of Android Chrome-specific fixes for the profile dropdown.
|
Added the Profiles entry to the mobile navigation flow, making profile switching reachable on phones, plus a set of Android Chrome-specific fixes for the profile dropdown.
|
||||||
|
|
||||||
**[@franksong2702](https://github.com/franksong2702)** — Session title guard + breadcrumb nav (PRs #301, #302)
|
**[@franksong2702](https://github.com/franksong2702)** — Session title guard + breadcrumb nav (PRs #301, #302)
|
||||||
Two clean bug fixes / features: the session title guard that stops `title_from()` from overwriting user-renamed sessions after every turn, and clickable breadcrumb navigation in the workspace file preview panel.
|
Two clean bug fixes / features: the session title guard that stops `title_from()` from overwriting user-renamed sessions after every turn, and clickable breadcrumb navigation in the workspace file preview panel.
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
| Sprint 18 | Thinking display + workspace tree | File preview auto-close, thinking/reasoning cards, expandable directory tree (#22) | 318 |
|
| Sprint 18 | Thinking display + workspace tree | File preview auto-close, thinking/reasoning cards, expandable directory tree (#22) | 318 |
|
||||||
| Sprint 19 | Auth + security hardening | Password auth (off by default), login page, security headers, 20MB body limit (#23) | 328 |
|
| Sprint 19 | Auth + security hardening | Password auth (off by default), login page, security headers, 20MB body limit (#23) | 328 |
|
||||||
| Sprint 20 | Voice input + send button | Voice input (Web Speech API), send button icon-circle with pop-in animation | 415 |
|
| Sprint 20 | Voice input + send button | Voice input (Web Speech API), send button icon-circle with pop-in animation | 415 |
|
||||||
| Sprint 21 | Mobile responsive + Docker | Hamburger sidebar, bottom nav, files slide-over, Docker support (#21, #7) | 415 |
|
| Sprint 21 | Mobile responsive + Docker | Hamburger sidebar, mobile nav, files slide-over, Docker support (#21, #7) | 415 |
|
||||||
| Sprint 22 | Multi-profile support | Profile picker, management panel, seamless switching, per-session tracking (#28) | 415 |
|
| Sprint 22 | Multi-profile support | Profile picker, management panel, seamless switching, per-session tracking (#28) | 415 |
|
||||||
| Sprint 23 | Agentic transparency | Token/cost display, subagent cards, skill picker in cron, skill linked files, workspace tree persistence, timestamp fixes | 424 |
|
| Sprint 23 | Agentic transparency | Token/cost display, subagent cards, skill picker in cron, skill linked files, workspace tree persistence, timestamp fixes | 424 |
|
||||||
| v0.44.0 patch | Fix batch: approval card, login CSP, update diagnostics, Lucide icons | PRs #221 #225 #226 #227 #228 | 579 |
|
| v0.44.0 patch | Fix batch: approval card, login CSP, update diagnostics, Lucide icons | PRs #221 #225 #226 #227 #228 | 579 |
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
| v0.48.0 | Gateway session sync | Real-time Telegram/Discord/Slack sessions in sidebar via SSE + DB polling (#274 @bergeouss); +10 tests | 658 |
|
| v0.48.0 | Gateway session sync | Real-time Telegram/Discord/Slack sessions in sidebar via SSE + DB polling (#274 @bergeouss); +10 tests | 658 |
|
||||||
| v0.48.1 | Table inline formatting | `inlineMd()` in table cells — **bold**, *italic*, `code`, links render correctly (PR #278); 0 new tests | 658 |
|
| v0.48.1 | Table inline formatting | `inlineMd()` in table cells — **bold**, *italic*, `code`, links render correctly (PR #278); 0 new tests | 658 |
|
||||||
| v0.48.2 | Provider mismatch warning | Toast warning + auth_mismatch error type for provider/model mismatches (#283, fixes #266); +21 tests | 679 |
|
| v0.48.2 | Provider mismatch warning | Toast warning + auth_mismatch error type for provider/model mismatches (#283, fixes #266); +21 tests | 679 |
|
||||||
| v0.49.1 | Docker docs + mobile Profiles button | Two-container Docker compose (#291/#288); Profiles button in mobile bottom nav with mobileSwitchPanel, data-panel, correct SVG size and position (#297/#265 @gabogabucho); +3 tests | 700 |
|
| v0.49.1 | Docker docs + mobile Profiles button | Two-container Docker compose (#291/#288); Profiles added to the mobile navigation flow with correct panel wiring and SVG sizing (#297/#265 @gabogabucho); +3 tests | 700 |
|
||||||
| v0.49.0 | First-run onboarding wizard + self-update hardening | One-shot bootstrap + guided setup wizard; provider config persisted to config.yaml + .env; OpenRouter/Anthropic/OpenAI/Custom; wizard hidden after completion (#285); self-update stderr/split-ref/conflict fixes (#287); skip flaky redaction test (#289); +18 tests | 697 |
|
| v0.49.0 | First-run onboarding wizard + self-update hardening | One-shot bootstrap + guided setup wizard; provider config persisted to config.yaml + .env; OpenRouter/Anthropic/OpenAI/Custom; wizard hidden after completion (#285); self-update stderr/split-ref/conflict fixes (#287); skip flaky redaction test (#289); +18 tests | 697 |
|
||||||
| v0.32 | Auto-compaction handling | Compression detection, /compact command, real context window indicator | 424 |
|
| v0.32 | Auto-compaction handling | Compression detection, /compact command, real context window indicator | 424 |
|
||||||
| v0.33 | /insights sync | Opt-in state.db sync so `hermes /insights` includes WebUI sessions | 424 |
|
| v0.33 | /insights sync | Opt-in state.db sync so `hermes /insights` includes WebUI sessions | 424 |
|
||||||
@@ -223,7 +223,7 @@
|
|||||||
- [x] Voice input via Web Speech API (Sprint 20)
|
- [x] Voice input via Web Speech API (Sprint 20)
|
||||||
|
|
||||||
### Mobile
|
### Mobile
|
||||||
- [x] Mobile responsive layout — hamburger sidebar, bottom nav, files slide-over (Sprint 21)
|
- [x] Mobile responsive layout — hamburger sidebar, sidebar tabs on phones, files slide-over (Sprint 21 + later mobile nav simplification)
|
||||||
|
|
||||||
### Profiles
|
### Profiles
|
||||||
- [x] Multi-profile support — create, switch, delete profiles (Sprint 22, Issue #28)
|
- [x] Multi-profile support — create, switch, delete profiles (Sprint 22, Issue #28)
|
||||||
|
|||||||
@@ -1715,12 +1715,13 @@ Each has automated API-level tests in `tests/test_sprint{N}.py`.
|
|||||||
- Open on mobile viewport (<640px): hamburger icon visible in topbar.
|
- Open on mobile viewport (<640px): hamburger icon visible in topbar.
|
||||||
- Tap hamburger → sidebar slides in from left with backdrop overlay.
|
- Tap hamburger → sidebar slides in from left with backdrop overlay.
|
||||||
- Tap outside sidebar → closes. Tap a session → closes and loads session.
|
- Tap outside sidebar → closes. Tap a session → closes and loads session.
|
||||||
- Bottom navigation bar: 5 tabs (Chat, Tasks, Skills, Memory, Spaces).
|
- Sidebar top nav remains visible inside the mobile drawer; includes Chat/Tasks/Skills/Memory/Spaces/Profile tabs.
|
||||||
- Tap "Tasks" in bottom nav → sidebar opens showing Tasks panel.
|
- Tap "Tasks" in the drawer nav → Tasks panel opens in the sidebar drawer.
|
||||||
- Tap "Chat" in bottom nav → sidebar closes (chat is in main area).
|
- Tap "Chat" in the drawer nav → sidebar closes and chat is unobstructed in the main area.
|
||||||
- Files button in topbar → right panel slides in from right.
|
- Files button in topbar → right panel slides in from right.
|
||||||
|
- No fixed mobile bottom nav; chat transcript and composer use the reclaimed vertical space.
|
||||||
- All touch targets are at least 44px (session items, buttons, icons).
|
- All touch targets are at least 44px (session items, buttons, icons).
|
||||||
- Desktop viewport (>640px): no hamburger, no bottom nav, no mobile elements.
|
- Desktop viewport (>640px): no hamburger or mobile overlay; desktop layout unchanged.
|
||||||
- Docker: `docker compose up -d` starts server on port 8787.
|
- Docker: `docker compose up -d` starts server on port 8787.
|
||||||
- Docker: session data persists across container restarts (named volume).
|
- Docker: session data persists across container restarts (named volume).
|
||||||
|
|
||||||
|
|||||||
@@ -155,11 +155,7 @@ function toggleWorkspacePanel(force){
|
|||||||
openWorkspacePanel(nextMode);
|
openWorkspacePanel(nextMode);
|
||||||
}
|
}
|
||||||
function mobileSwitchPanel(name){
|
function mobileSwitchPanel(name){
|
||||||
// Switch the panel content view
|
|
||||||
switchPanel(name);
|
switchPanel(name);
|
||||||
// For non-chat panels (tasks, skills, memory, spaces), open the sidebar
|
|
||||||
// so the panel is visible. For 'chat', the content is in the main area —
|
|
||||||
// just close the sidebar so the chat view is unobstructed.
|
|
||||||
if(name==='chat'){
|
if(name==='chat'){
|
||||||
closeMobileSidebar();
|
closeMobileSidebar();
|
||||||
} else {
|
} else {
|
||||||
@@ -170,10 +166,6 @@ function mobileSwitchPanel(name){
|
|||||||
if(overlay)overlay.classList.add('visible');
|
if(overlay)overlay.classList.add('visible');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Update bottom nav active state
|
|
||||||
document.querySelectorAll('.mobile-nav-btn').forEach(btn=>{
|
|
||||||
btn.classList.toggle('active',btn.dataset.panel===name);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$('btnSend').onclick=()=>{
|
$('btnSend').onclick=()=>{
|
||||||
|
|||||||
@@ -553,32 +553,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mobile-overlay" id="mobileOverlay" onclick="closeMobileSidebar()"></div>
|
<div class="mobile-overlay" id="mobileOverlay" onclick="closeMobileSidebar()"></div>
|
||||||
<nav class="mobile-bottom-nav" id="mobileBottomNav">
|
|
||||||
<button class="mobile-nav-btn active" data-panel="chat" onclick="mobileSwitchPanel('chat')">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
|
||||||
<span data-i18n="tab_chat">Chat</span>
|
|
||||||
</button>
|
|
||||||
<button class="mobile-nav-btn" data-panel="tasks" onclick="mobileSwitchPanel('tasks')">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
|
|
||||||
<span data-i18n="tab_tasks">Tasks</span>
|
|
||||||
</button>
|
|
||||||
<button class="mobile-nav-btn" data-panel="skills" onclick="mobileSwitchPanel('skills')">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
|
||||||
<span data-i18n="tab_skills">Skills</span>
|
|
||||||
</button>
|
|
||||||
<button class="mobile-nav-btn" data-panel="memory" onclick="mobileSwitchPanel('memory')">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2a7 7 0 0 1 7 7c0 2.5-1.3 4.7-3.2 6H8.2C6.3 13.7 5 11.5 5 9a7 7 0 0 1 7-7z"/><line x1="9" y1="17" x2="15" y2="17"/><line x1="10" y1="20" x2="14" y2="20"/></svg>
|
|
||||||
<span data-i18n="tab_memory">Memory</span>
|
|
||||||
</button>
|
|
||||||
<button class="mobile-nav-btn" data-panel="workspaces" onclick="mobileSwitchPanel('workspaces')">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h8l2 2h10v14H2z"/></svg>
|
|
||||||
<span data-i18n="tab_workspaces">Spaces</span>
|
|
||||||
</button>
|
|
||||||
<button class="mobile-nav-btn" data-panel="profiles" onclick="mobileSwitchPanel('profiles')">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
|
||||||
<span data-i18n="tab_profiles">Profiles</span>
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
<div class="app-dialog-overlay" id="appDialogOverlay" style="display:none" aria-hidden="true">
|
<div class="app-dialog-overlay" id="appDialogOverlay" style="display:none" aria-hidden="true">
|
||||||
<div class="app-dialog" id="appDialog" role="dialog" aria-modal="true" aria-labelledby="appDialogTitle" aria-describedby="appDialogDesc">
|
<div class="app-dialog" id="appDialog" role="dialog" aria-modal="true" aria-labelledby="appDialogTitle" aria-describedby="appDialogDesc">
|
||||||
<div class="app-dialog-header">
|
<div class="app-dialog-header">
|
||||||
|
|||||||
@@ -557,7 +557,6 @@
|
|||||||
.mobile-hamburger{display:none;}
|
.mobile-hamburger{display:none;}
|
||||||
.mobile-files-btn{display:none!important;}
|
.mobile-files-btn{display:none!important;}
|
||||||
.mobile-overlay{display:none;}
|
.mobile-overlay{display:none;}
|
||||||
.mobile-bottom-nav{display:none;}
|
|
||||||
|
|
||||||
@media(min-width:901px){
|
@media(min-width:901px){
|
||||||
.layout.workspace-panel-collapsed .rightpanel{width:0 !important;opacity:0;transform:translateX(14px);border-left-color:transparent;pointer-events:none;}
|
.layout.workspace-panel-collapsed .rightpanel{width:0 !important;opacity:0;transform:translateX(14px);border-left-color:transparent;pointer-events:none;}
|
||||||
@@ -593,20 +592,6 @@
|
|||||||
box-shadow:-4px 0 24px rgba(0,0,0,.4);}
|
box-shadow:-4px 0 24px rgba(0,0,0,.4);}
|
||||||
.rightpanel.mobile-open{right:0;}
|
.rightpanel.mobile-open{right:0;}
|
||||||
.rightpanel .resize-handle{display:none;}
|
.rightpanel .resize-handle{display:none;}
|
||||||
/* Bottom navigation bar */
|
|
||||||
.mobile-bottom-nav{display:flex;position:fixed;bottom:0;left:0;right:0;
|
|
||||||
background:var(--sidebar);border-top:1px solid var(--border);
|
|
||||||
z-index:150;padding:4px 0 env(safe-area-inset-bottom,0);
|
|
||||||
justify-content:space-around;align-items:center;}
|
|
||||||
.mobile-nav-btn{display:flex;flex-direction:column;align-items:center;gap:2px;
|
|
||||||
background:none;border:none;color:var(--muted);font-size:9px;padding:6px 4px;
|
|
||||||
cursor:pointer;min-width:44px;min-height:44px;justify-content:center;
|
|
||||||
-webkit-tap-highlight-color:transparent;transition:color .15s;}
|
|
||||||
.mobile-nav-btn.active{color:var(--blue);}
|
|
||||||
.mobile-nav-btn:hover{color:var(--text);}
|
|
||||||
.mobile-nav-btn svg{flex-shrink:0;}
|
|
||||||
/* Hide sidebar nav tabs (replaced by bottom nav) */
|
|
||||||
.sidebar-nav{display:none;}
|
|
||||||
/* Keep the Hermes control available at the bottom of the mobile sidebar */
|
/* Keep the Hermes control available at the bottom of the mobile sidebar */
|
||||||
.sidebar-bottom{display:block;padding:10px;}
|
.sidebar-bottom{display:block;padding:10px;}
|
||||||
/* Topbar adjustments */
|
/* Topbar adjustments */
|
||||||
@@ -620,13 +605,10 @@
|
|||||||
.settings-tab{flex-shrink:0;}
|
.settings-tab{flex-shrink:0;}
|
||||||
.settings-main{padding:18px 16px;}
|
.settings-main{padding:18px 16px;}
|
||||||
.hermes-action-grid{grid-template-columns:1fr;}
|
.hermes-action-grid{grid-template-columns:1fr;}
|
||||||
/* Messages area — account for bottom nav */
|
|
||||||
.messages{padding-bottom:60px;}
|
|
||||||
.messages-inner{padding:12px 10px 20px;}
|
.messages-inner{padding:12px 10px 20px;}
|
||||||
.msg-body{padding-left:0;max-width:100%;}
|
.msg-body{padding-left:0;max-width:100%;}
|
||||||
.msg-role{font-size:12px;}
|
.msg-role{font-size:12px;}
|
||||||
/* Composer — above bottom nav */
|
.composer-wrap{padding:8px 10px 12px!important;}
|
||||||
.composer-wrap{padding:8px 10px 12px!important;margin-bottom:56px;}
|
|
||||||
.composer-box{border-radius:12px;}
|
.composer-box{border-radius:12px;}
|
||||||
.composer-box textarea{font-size:16px;min-height:40px;}
|
.composer-box textarea{font-size:16px;min-height:40px;}
|
||||||
.composer-footer{padding:6px 8px 8px!important;gap:8px;}
|
.composer-footer{padding:6px 8px 8px!important;gap:8px;}
|
||||||
|
|||||||
25
tests/test_issue_code_syntax_highlight.py
Normal file
25
tests/test_issue_code_syntax_highlight.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""Regression tests for fenced code block syntax highlighting."""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
UI_JS = Path(__file__).resolve().parent.parent / "static" / "ui.js"
|
||||||
|
|
||||||
|
|
||||||
|
def _read_ui_js() -> str:
|
||||||
|
return UI_JS.read_text()
|
||||||
|
|
||||||
|
|
||||||
|
def test_fenced_code_blocks_add_prism_language_class():
|
||||||
|
js = _read_ui_js()
|
||||||
|
assert 'class="language-${esc(normalizedLang)}"' in js, (
|
||||||
|
"Fenced code blocks should add Prism language-* classes so syntax highlighting works"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fenced_code_blocks_keep_existing_pre_header_layout():
|
||||||
|
js = _read_ui_js()
|
||||||
|
assert 'return `${h}<pre><code${langAttr}>${esc(code.replace(/\\n$/,' in js, (
|
||||||
|
"The syntax-highlight fix should preserve the existing fenced code block layout"
|
||||||
|
)
|
||||||
|
assert '<div class="code-block">' not in js, (
|
||||||
|
"This fix should not introduce a new wrapper around fenced code blocks"
|
||||||
|
)
|
||||||
@@ -9,7 +9,7 @@ They are static checks (no server needed) that catch common regressions:
|
|||||||
- Right panel slide-over markup and CSS intact
|
- Right panel slide-over markup and CSS intact
|
||||||
- Profile dropdown not clipped by overflow on mobile
|
- Profile dropdown not clipped by overflow on mobile
|
||||||
- Composer footer chips scroll correctly on narrow viewports
|
- Composer footer chips scroll correctly on narrow viewports
|
||||||
- Mobile bottom nav and overlay markup present
|
- Mobile sidebar navigation stays available on phones
|
||||||
- No full-viewport overflow that would break scroll
|
- No full-viewport overflow that would break scroll
|
||||||
|
|
||||||
Run as part of the standard test suite:
|
Run as part of the standard test suite:
|
||||||
@@ -61,12 +61,20 @@ def test_mobile_overlay_present():
|
|||||||
".mobile-overlay CSS rule missing from style.css"
|
".mobile-overlay CSS rule missing from style.css"
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_bottom_nav_present():
|
def test_sidebar_nav_present():
|
||||||
"""Mobile bottom navigation bar must be present."""
|
"""Sidebar top navigation tabs must be present."""
|
||||||
assert "mobile-bottom-nav" in HTML or "mobile-nav-btn" in HTML, \
|
assert 'class="sidebar-nav"' in HTML, \
|
||||||
"Mobile bottom nav (.mobile-bottom-nav or .mobile-nav-btn) missing from index.html"
|
".sidebar-nav missing from index.html"
|
||||||
assert "mobile-bottom-nav" in CSS, \
|
assert ".sidebar-nav{" in CSS or ".sidebar-nav {" in CSS, \
|
||||||
".mobile-bottom-nav CSS rule missing from style.css"
|
".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."""
|
||||||
|
mobile_block = re.search(r'@media\(max-width:640px\)\{(.*)\n\s*\}', CSS, re.DOTALL)
|
||||||
|
assert mobile_block, "Missing @media(max-width:640px) block in style.css"
|
||||||
|
assert ".sidebar-nav{display:none" not in mobile_block.group(1).replace(" ", ""), \
|
||||||
|
".sidebar-nav must stay visible on mobile"
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_files_button_present():
|
def test_mobile_files_button_present():
|
||||||
@@ -222,34 +230,20 @@ def test_composer_textarea_font_size_mobile():
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ── Profiles button in mobile bottom nav ─────────────────────────────────────
|
# ── Sidebar tabs on mobile ───────────────────────────────────────────────────
|
||||||
|
|
||||||
def test_mobile_profiles_button_present():
|
def test_profiles_sidebar_tab_present():
|
||||||
"""Mobile bottom nav must include a Profiles button (PR #265)."""
|
"""Sidebar tab strip must include Profiles."""
|
||||||
assert 'data-panel="profiles"' in HTML and 'mobileSwitchPanel' in HTML, \
|
assert 'class="nav-tab" data-panel="profiles"' in HTML, \
|
||||||
"Mobile nav must have a Profiles button with data-panel='profiles' and mobileSwitchPanel"
|
"Sidebar nav must have a Profiles tab"
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_profiles_button_uses_mobileSwitchPanel():
|
def test_mobile_bottom_nav_removed():
|
||||||
"""Profiles mobile nav button must use mobileSwitchPanel, not raw switchPanel."""
|
"""The old fixed mobile bottom nav should not be present anymore."""
|
||||||
import re
|
assert "mobile-bottom-nav" not in HTML, \
|
||||||
match = re.search(
|
"mobile-bottom-nav markup should be removed from index.html"
|
||||||
r'<button[^>]*mobile-nav-btn[^>]*data-panel="profiles"[^>]*>|'
|
assert "mobile-bottom-nav" not in CSS, \
|
||||||
r'<button[^>]*data-panel="profiles"[^>]*mobile-nav-btn[^>]*>',
|
"mobile-bottom-nav CSS should be removed from style.css"
|
||||||
HTML
|
|
||||||
)
|
|
||||||
assert match, "Could not find mobile-nav-btn with data-panel='profiles'"
|
|
||||||
btn_html = HTML[match.start():match.start()+300]
|
|
||||||
assert "mobileSwitchPanel('profiles')" in btn_html, \
|
|
||||||
"Profiles mobile nav button must call mobileSwitchPanel('profiles')"
|
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_profiles_button_is_last_in_nav():
|
|
||||||
"""Profiles button must appear after Spaces in the mobile bottom nav."""
|
|
||||||
spaces_pos = HTML.find('data-panel="workspaces"')
|
|
||||||
profiles_pos = HTML.rfind('data-panel="profiles"')
|
|
||||||
assert spaces_pos > 0 and profiles_pos > spaces_pos, \
|
|
||||||
"Profiles button must appear after Spaces button in the mobile nav"
|
|
||||||
|
|
||||||
|
|
||||||
# ── Mobile Enter key inserts newline (PR #315, fixes #269) ───────────────────
|
# ── Mobile Enter key inserts newline (PR #315, fixes #269) ───────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user