Merge pull request #40 from nesquena/sprint-21-mobile-docker
Sprint 21: Mobile responsive layout + Docker support (Issues #21, #7)
This commit is contained in:
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.git
|
||||||
|
.pytest_cache
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
tests/
|
||||||
|
.env*
|
||||||
34
CHANGELOG.md
34
CHANGELOG.md
@@ -5,6 +5,38 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [v0.23] Sprint 21 -- Mobile Responsive + Docker
|
||||||
|
*April 3, 2026 | 415 tests*
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- **Mobile responsive layout (Issue #21).** Full mobile experience with
|
||||||
|
hamburger sidebar (slide-in overlay), bottom navigation bar (5-tab iOS
|
||||||
|
pattern), and files slide-over panel. Touch targets minimum 44px. Composer
|
||||||
|
positioned above bottom nav. Session clicks auto-close sidebar. Desktop
|
||||||
|
layout completely unchanged — all mobile elements hidden via `@media`.
|
||||||
|
- **Docker support (Issue #7).** Dockerfile (`python:3.12-slim`), docker-compose.yml
|
||||||
|
with named volume for state persistence, optional `~/.hermes` mount for
|
||||||
|
agent features. Binds to `127.0.0.1` by default for security.
|
||||||
|
|
||||||
|
### Bug Fixes (from review)
|
||||||
|
- **CSS cascade broke mobile slide-in.** `position:relative` rules after the
|
||||||
|
media query overrode `position:fixed` on mobile. Wrapped in `@media(min-width:641px)`.
|
||||||
|
- **mobileSwitchPanel() always reopened sidebar.** Chat tab now closes sidebar
|
||||||
|
instead of reopening it over the main chat area.
|
||||||
|
- **Dockerfile missing pip install.** Added `pip install -r requirements.txt`.
|
||||||
|
- **No .dockerignore.** Added exclusions for `.git`, `tests/`, `.env*`.
|
||||||
|
- **docker-compose tilde expansion.** Changed `~/.hermes` default to
|
||||||
|
`${HOME}/.hermes` (Docker Compose doesn't shell-expand `~`).
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
- Mobile navigation functions in `boot.js`: `toggleMobileSidebar()`,
|
||||||
|
`closeMobileSidebar()`, `toggleMobileFiles()`, `mobileSwitchPanel()`.
|
||||||
|
- `sessions.js`: `closeMobileSidebar()` called after session click.
|
||||||
|
- 69 new CSS lines in `@media(max-width:640px)` block.
|
||||||
|
- New files: `Dockerfile`, `docker-compose.yml`, `.dockerignore`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [v0.22] Sprint 20 -- Voice Input + Send Button Polish
|
## [v0.22] Sprint 20 -- Voice Input + Send Button Polish
|
||||||
*April 3, 2026 | 415 tests*
|
*April 3, 2026 | 415 tests*
|
||||||
|
|
||||||
@@ -716,4 +748,4 @@ Three-panel layout: sessions sidebar, chat area, workspace panel.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Last updated: v0.22, April 3, 2026 | Tests: 415*
|
*Last updated: v0.23, April 3, 2026 | Tests: 415*
|
||||||
|
|||||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
LABEL maintainer="nesquena"
|
||||||
|
LABEL description="Hermes Web UI — browser interface for Hermes Agent"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy source
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Default to binding all interfaces (required for container networking)
|
||||||
|
ENV HERMES_WEBUI_HOST=0.0.0.0
|
||||||
|
ENV HERMES_WEBUI_PORT=8787
|
||||||
|
|
||||||
|
# State directory (mount as volume for persistence)
|
||||||
|
ENV HERMES_WEBUI_STATE_DIR=/data
|
||||||
|
|
||||||
|
EXPOSE 8787
|
||||||
|
|
||||||
|
CMD ["python", "server.py"]
|
||||||
31
README.md
31
README.md
@@ -35,6 +35,37 @@ That is it. The script will:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
Run with Docker Compose (recommended):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Or build and run manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t hermes-webui .
|
||||||
|
docker run -d -p 8787:8787 -v ~/.hermes:/root/.hermes:ro hermes-webui
|
||||||
|
```
|
||||||
|
|
||||||
|
Open http://localhost:8787 in your browser.
|
||||||
|
|
||||||
|
To enable password protection:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 8787:8787 -e HERMES_WEBUI_PASSWORD=your-secret -v ~/.hermes:/root/.hermes:ro hermes-webui
|
||||||
|
```
|
||||||
|
|
||||||
|
Session data persists in a named volume (`hermes-data`) across restarts.
|
||||||
|
|
||||||
|
> **Note:** By default, Docker Compose binds to `127.0.0.1` (localhost only).
|
||||||
|
> To expose on a network, change the port to `"8787:8787"` in `docker-compose.yml`
|
||||||
|
> and set `HERMES_WEBUI_PASSWORD` to enable authentication.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## What start.sh discovers automatically
|
## What start.sh discovers automatically
|
||||||
|
|
||||||
| Thing | How it finds it |
|
| Thing | How it finds it |
|
||||||
|
|||||||
40
SPRINTS.md
40
SPRINTS.md
@@ -1,6 +1,6 @@
|
|||||||
# Hermes Web UI -- Forward Sprint Plan
|
# Hermes Web UI -- Forward Sprint Plan
|
||||||
|
|
||||||
> Current state: v0.22 | 415 tests | Daily driver ready
|
> Current state: v0.23 | 415 tests | Daily driver ready
|
||||||
> This document plans the path from here to two targets:
|
> This document plans the path from here to two targets:
|
||||||
>
|
>
|
||||||
> Target A: 1:1 feature parity with the Hermes CLI (everything you can do from the
|
> Target A: 1:1 feature parity with the Hermes CLI (everything you can do from the
|
||||||
@@ -421,16 +421,36 @@ UX was a low-effort high-impact polish opportunity that pairs naturally.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sprint 21 -- Mobile Responsive (PLANNED)
|
## Sprint 21 -- Mobile Responsive + Docker (COMPLETED)
|
||||||
|
|
||||||
**Theme:** A genuinely good mobile experience, not just responsive CSS.
|
**Theme:** Mobile experience + containerized deployment.
|
||||||
|
|
||||||
|
**Why now:** Issue #21 (mobile) was the most-requested UX gap. Issue #7 (Docker)
|
||||||
|
enables deployment beyond localhost. Both were achievable without new dependencies.
|
||||||
|
|
||||||
|
### Track A: Bugs (from review)
|
||||||
|
- **CSS cascade broke mobile slide-in.** `position:relative` after the media query
|
||||||
|
overrode `position:fixed`. Wrapped in `@media(min-width:641px)`.
|
||||||
|
- **mobileSwitchPanel() always reopened sidebar.** Chat tab now closes it.
|
||||||
|
- **Dockerfile missing pip install.** Container failed on startup.
|
||||||
|
- **No .dockerignore.** `.git`, `tests/`, `.env*` leaked into images.
|
||||||
|
- **docker-compose tilde expansion.** `~` doesn't expand in Compose defaults.
|
||||||
|
|
||||||
### Track B: Features
|
### Track B: Features
|
||||||
- **Collapsible sidebar.** Hamburger menu replaces the always-visible sidebar.
|
- **Hamburger sidebar.** Slide-in overlay on mobile, tap outside to close.
|
||||||
- **Touch-friendly session list.** Tap to navigate, swipe gestures.
|
- **Bottom navigation bar.** 5-tab iOS-style bar replaces sidebar tabs.
|
||||||
- **Right panel as tab.** Files panel hidden by default, accessible via tab.
|
- **Files slide-over.** Right panel opens as slide-over from right edge.
|
||||||
- **Composer focus behavior.** Expands on focus, keyboard-aware.
|
- **Touch targets.** Minimum 44px on all interactive elements.
|
||||||
- Consider a separate mobile-optimized layout rather than just media queries.
|
- **Docker support.** Dockerfile, docker-compose.yml, .dockerignore.
|
||||||
|
|
||||||
|
### Track C: Architecture
|
||||||
|
- Mobile nav functions in `boot.js`. Session click auto-closes sidebar.
|
||||||
|
- 69 new CSS lines scoped to `@media(max-width:640px)`.
|
||||||
|
- Desktop layout untouched — all mobile elements `display:none` by default.
|
||||||
|
|
||||||
|
**Tests:** 0 new (CSS/DOM changes). Total: 415.
|
||||||
|
**Hermes CLI parity impact:** Low
|
||||||
|
**Claude parity impact:** High (Claude has mobile layout)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -540,5 +560,5 @@ UX was a low-effort high-impact polish opportunity that pairs naturally.
|
|||||||
---
|
---
|
||||||
|
|
||||||
*Last updated: April 3, 2026*
|
*Last updated: April 3, 2026*
|
||||||
*Current version: v0.22 | 415 tests*
|
*Current version: v0.23 | 415 tests*
|
||||||
*Next sprint: Sprint 21 (Mobile Responsive)*
|
*Next sprint: Sprint 22 (Multi-Profile Support)*
|
||||||
|
|||||||
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
hermes-webui:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8787:8787"
|
||||||
|
volumes:
|
||||||
|
# Persist session data, settings, and projects across restarts
|
||||||
|
- hermes-data:/data
|
||||||
|
# Mount hermes-agent for full agent features (optional)
|
||||||
|
- ${HERMES_HOME:-${HOME}/.hermes}:/root/.hermes:ro
|
||||||
|
environment:
|
||||||
|
- HERMES_WEBUI_HOST=0.0.0.0
|
||||||
|
- HERMES_WEBUI_PORT=8787
|
||||||
|
- HERMES_WEBUI_STATE_DIR=/data
|
||||||
|
# Optional: set a password for remote access
|
||||||
|
# - HERMES_WEBUI_PASSWORD=your-secret-password
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
hermes-data:
|
||||||
@@ -8,6 +8,48 @@ async function cancelStream(){
|
|||||||
}catch(e){setStatus('Cancel failed: '+e.message);}
|
}catch(e){setStatus('Cancel failed: '+e.message);}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Mobile navigation ──────────────────────────────────────────────────────
|
||||||
|
function toggleMobileSidebar(){
|
||||||
|
const sidebar=document.querySelector('.sidebar');
|
||||||
|
const overlay=$('mobileOverlay');
|
||||||
|
if(!sidebar)return;
|
||||||
|
const isOpen=sidebar.classList.contains('mobile-open');
|
||||||
|
if(isOpen){closeMobileSidebar();}
|
||||||
|
else{sidebar.classList.add('mobile-open');if(overlay)overlay.classList.add('visible');}
|
||||||
|
}
|
||||||
|
function closeMobileSidebar(){
|
||||||
|
const sidebar=document.querySelector('.sidebar');
|
||||||
|
const overlay=$('mobileOverlay');
|
||||||
|
if(sidebar)sidebar.classList.remove('mobile-open');
|
||||||
|
if(overlay)overlay.classList.remove('visible');
|
||||||
|
}
|
||||||
|
function toggleMobileFiles(){
|
||||||
|
const panel=document.querySelector('.rightpanel');
|
||||||
|
if(!panel)return;
|
||||||
|
panel.classList.toggle('mobile-open');
|
||||||
|
}
|
||||||
|
function mobileSwitchPanel(name){
|
||||||
|
// Switch the panel content view
|
||||||
|
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'){
|
||||||
|
closeMobileSidebar();
|
||||||
|
} else {
|
||||||
|
const sidebar=document.querySelector('.sidebar');
|
||||||
|
const overlay=$('mobileOverlay');
|
||||||
|
if(sidebar){
|
||||||
|
sidebar.classList.add('mobile-open');
|
||||||
|
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=()=>{if(window._micActive)_stopMic();send();};
|
$('btnSend').onclick=()=>{if(window._micActive)_stopMic();send();};
|
||||||
$('btnAttach').onclick=()=>$('fileInput').click();
|
$('btnAttach').onclick=()=>$('fileInput').click();
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="sidebar-header"><div class="logo">H</div><div><h1 style="margin:0;font-size:15px;font-weight:700;letter-spacing:-.01em">Hermes</h1><div style="font-size:10px;color:var(--muted);opacity:.8;margin-top:1px">v0.22</div></div></div>
|
<div class="sidebar-header"><div class="logo">H</div><div><h1 style="margin:0;font-size:15px;font-weight:700;letter-spacing:-.01em">Hermes</h1><div style="font-size:10px;color:var(--muted);opacity:.8;margin-top:1px">v0.23</div></div></div>
|
||||||
<div class="sidebar-nav">
|
<div class="sidebar-nav">
|
||||||
<button class="nav-tab active" data-panel="chat" data-label="Chat" onclick="switchPanel('chat')" title="Chat">💬</button>
|
<button class="nav-tab active" data-panel="chat" data-label="Chat" onclick="switchPanel('chat')" title="Chat">💬</button>
|
||||||
<button class="nav-tab" data-panel="tasks" data-label="Tasks" onclick="switchPanel('tasks')" title="Tasks">📅</button>
|
<button class="nav-tab" data-panel="tasks" data-label="Tasks" onclick="switchPanel('tasks')" title="Tasks">📅</button>
|
||||||
@@ -143,6 +143,9 @@
|
|||||||
</aside>
|
</aside>
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<div class="topbar">
|
<div class="topbar">
|
||||||
|
<button class="mobile-hamburger" id="btnHamburger" onclick="toggleMobileSidebar()" title="Menu">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||||||
|
</button>
|
||||||
<div style="flex:1;min-width:0;overflow:hidden"><div class="topbar-title" id="topbarTitle">Hermes</div><div class="topbar-meta" id="topbarMeta">Start a new conversation</div></div>
|
<div style="flex:1;min-width:0;overflow:hidden"><div class="topbar-title" id="topbarTitle">Hermes</div><div class="topbar-meta" id="topbarMeta">Start a new conversation</div></div>
|
||||||
<div class="topbar-chips">
|
<div class="topbar-chips">
|
||||||
<div class="chip model" id="modelChip">GPT-5.4 Mini</div>
|
<div class="chip model" id="modelChip">GPT-5.4 Mini</div>
|
||||||
@@ -152,6 +155,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button class="chip clear-btn" id="btnClearConv" onclick="clearConversation()" title="Clear all messages in this conversation" style="display:none">🗑 Clear</button>
|
<button class="chip clear-btn" id="btnClearConv" onclick="clearConversation()" title="Clear all messages in this conversation" style="display:none">🗑 Clear</button>
|
||||||
<button class="chip gear-btn" id="btnSettings" onclick="toggleSettings()" title="Settings">⚙</button>
|
<button class="chip gear-btn" id="btnSettings" onclick="toggleSettings()" title="Settings">⚙</button>
|
||||||
|
<button class="chip mobile-files-btn" id="btnMobileFiles" onclick="toggleMobileFiles()" title="Files">📁</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="messages" id="messages">
|
<div class="messages" id="messages">
|
||||||
@@ -301,6 +305,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>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>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>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>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>Spaces</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
<div class="toast" id="toast"></div>
|
<div class="toast" id="toast"></div>
|
||||||
<script src="/static/ui.js"></script>
|
<script src="/static/ui.js"></script>
|
||||||
<script src="/static/workspace.js"></script>
|
<script src="/static/workspace.js"></script>
|
||||||
|
|||||||
@@ -346,6 +346,7 @@ function renderSessionListFromCache(){
|
|||||||
_clickTimer=null;
|
_clickTimer=null;
|
||||||
if(_renamingSid) return;
|
if(_renamingSid) return;
|
||||||
await loadSession(s.session_id);renderSessionListFromCache();
|
await loadSession(s.session_id);renderSessionListFromCache();
|
||||||
|
if(typeof closeMobileSidebar==='function')closeMobileSidebar();
|
||||||
}, 220);
|
}, 220);
|
||||||
};
|
};
|
||||||
el.ondblclick=async(e)=>{
|
el.ondblclick=async(e)=>{
|
||||||
|
|||||||
@@ -260,38 +260,87 @@
|
|||||||
::-webkit-scrollbar-track{background:transparent}
|
::-webkit-scrollbar-track{background:transparent}
|
||||||
::-webkit-scrollbar-thumb{background:rgba(255,255,255,.1);border-radius:99px;transition:background .2s}
|
::-webkit-scrollbar-thumb{background:rgba(255,255,255,.1);border-radius:99px;transition:background .2s}
|
||||||
::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.22)}
|
::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.22)}
|
||||||
@media(max-width:900px){.rightpanel{display:none}}
|
/* ── Desktop: hide mobile-only elements ── */
|
||||||
|
.mobile-hamburger{display:none;}
|
||||||
|
.mobile-files-btn{display:none!important;}
|
||||||
|
.mobile-overlay{display:none;}
|
||||||
|
.mobile-bottom-nav{display:none;}
|
||||||
|
|
||||||
|
@media(max-width:900px){.rightpanel{display:none}.mobile-files-btn{display:inline-flex!important;}}
|
||||||
|
|
||||||
@media(max-width:640px){
|
@media(max-width:640px){
|
||||||
.sidebar{display:none}
|
/* ── Sidebar: slide-in overlay instead of hidden ── */
|
||||||
/* Topbar: stack title + chips vertically, allow wrapping */
|
.sidebar{position:fixed;left:-300px;top:0;bottom:0;width:280px;z-index:200;
|
||||||
.topbar{padding:8px 12px;gap:6px;flex-wrap:wrap;}
|
transition:left .25s ease;box-shadow:4px 0 24px rgba(0,0,0,.4);}
|
||||||
.topbar-left{min-width:0;flex:1 1 100%;}
|
.sidebar.mobile-open{left:0;}
|
||||||
|
.sidebar .resize-handle{display:none;}
|
||||||
|
/* Hamburger button */
|
||||||
|
.mobile-hamburger{display:flex;align-items:center;justify-content:center;
|
||||||
|
background:none;border:none;color:var(--muted);cursor:pointer;padding:4px;
|
||||||
|
flex-shrink:0;-webkit-tap-highlight-color:transparent;}
|
||||||
|
.mobile-hamburger:hover{color:var(--text);}
|
||||||
|
/* Overlay backdrop */
|
||||||
|
.mobile-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);
|
||||||
|
z-index:199;-webkit-tap-highlight-color:transparent;}
|
||||||
|
.mobile-overlay.visible{display:block;}
|
||||||
|
/* Files button in topbar */
|
||||||
|
.mobile-files-btn{display:inline-flex!important;}
|
||||||
|
/* Right panel: slide-over from right */
|
||||||
|
.rightpanel{display:flex!important;position:fixed;right:-320px;top:0;bottom:0;
|
||||||
|
width:300px;z-index:200;transition:right .25s ease;
|
||||||
|
box-shadow:-4px 0 24px rgba(0,0,0,.4);}
|
||||||
|
.rightpanel.mobile-open{right:0;}
|
||||||
|
.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;}
|
||||||
|
/* Hide sidebar bottom section on mobile (model select, workspace) */
|
||||||
|
.sidebar-bottom{display:none;}
|
||||||
|
/* Topbar adjustments */
|
||||||
|
.topbar{padding:8px 12px;gap:8px;}
|
||||||
.topbar-title{font-size:14px;}
|
.topbar-title{font-size:14px;}
|
||||||
.topbar-meta{font-size:10px;}
|
.topbar-meta{display:none;}
|
||||||
.topbar-chips{flex-wrap:wrap;gap:4px;}
|
.topbar-chips{flex-wrap:nowrap;gap:4px;overflow-x:auto;-webkit-overflow-scrolling:touch;}
|
||||||
.topbar-chips .chip,.topbar-chips .ws-chip,.topbar-chips button{font-size:11px!important;padding:3px 8px!important;}
|
.topbar-chips .chip,.topbar-chips .ws-chip,.topbar-chips button{font-size:11px!important;padding:3px 8px!important;white-space:nowrap;}
|
||||||
/* Messages area */
|
/* 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 */
|
/* 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;}
|
||||||
.send-btn{width:32px;height:32px;}
|
.send-btn{width:32px;height:32px;}
|
||||||
|
/* Touch targets — minimum 44px */
|
||||||
|
.icon-btn,.mic-btn{min-width:44px;min-height:44px;}
|
||||||
|
.session-item{min-height:44px;padding:10px 12px;}
|
||||||
/* Empty state */
|
/* Empty state */
|
||||||
.empty-state h2{font-size:18px;}
|
.empty-state h2{font-size:18px;}
|
||||||
.empty-state p{font-size:13px;}
|
.empty-state p{font-size:13px;}
|
||||||
.suggestion-grid{max-width:100%!important;}
|
.suggestion-grid{max-width:100%!important;}
|
||||||
.suggestion-btn{font-size:12px;padding:8px 10px;}
|
.suggestion{font-size:12px;padding:10px 12px;}
|
||||||
/* Approval card */
|
/* Approval card */
|
||||||
.approval-card{padding:0 10px 8px;}
|
.approval-card{padding:0 10px 8px;}
|
||||||
.approval-btns{gap:6px;}
|
.approval-btns{gap:6px;}
|
||||||
.approval-btn{padding:5px 10px;font-size:11px;}
|
.approval-btn{padding:8px 12px;font-size:12px;min-height:44px;}
|
||||||
/* Tool cards */
|
/* Tool cards */
|
||||||
.tool-card{margin-left:0!important;font-size:12px;}
|
.tool-card{margin-left:0!important;font-size:12px;}
|
||||||
/* Settings modal */
|
/* Settings modal */
|
||||||
.settings-panel{width:95vw;max-width:95vw;}
|
.settings-panel{width:95vw;max-width:95vw;}
|
||||||
|
/* Login page responsive */
|
||||||
|
.card{width:90vw;max-width:320px;padding:28px 24px;}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Workspace dropdown (topbar) ── */
|
/* ── Workspace dropdown (topbar) ── */
|
||||||
@@ -476,9 +525,14 @@
|
|||||||
transition:background .15s;
|
transition:background .15s;
|
||||||
}
|
}
|
||||||
.resize-handle:hover,.resize-handle.dragging{background:rgba(124,185,255,.35);}
|
.resize-handle:hover,.resize-handle.dragging{background:rgba(124,185,255,.35);}
|
||||||
|
/* Desktop-only: position:relative for sidebar/rightpanel resize handles.
|
||||||
|
Must be scoped to min-width:641px so it doesn't override the mobile
|
||||||
|
position:fixed slide-in overlay set in the max-width:640px @media block above. */
|
||||||
|
@media(min-width:641px){
|
||||||
.sidebar{position:relative;}
|
.sidebar{position:relative;}
|
||||||
.sidebar .resize-handle{right:-2px;}
|
|
||||||
.rightpanel{position:relative;}
|
.rightpanel{position:relative;}
|
||||||
|
}
|
||||||
|
.sidebar .resize-handle{right:-2px;}
|
||||||
.rightpanel .resize-handle{left:-2px;}
|
.rightpanel .resize-handle{left:-2px;}
|
||||||
/* Prevent text selection during drag */
|
/* Prevent text selection during drag */
|
||||||
body.resizing{user-select:none;cursor:col-resize;}
|
body.resizing{user-select:none;cursor:col-resize;}
|
||||||
|
|||||||
Reference in New Issue
Block a user