From 32a6f46481a51f3d39dbb9c743e4ac5f95e79adb Mon Sep 17 00:00:00 2001 From: Fredrik Johansson Date: Mon, 29 Jun 2026 18:52:26 +0200 Subject: [PATCH] feat: responsive mobile layout with slide-over sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On screens ≤600px the sidebar collapses off-screen. A hamburger button in the message header opens it as a slide-over drawer with a dim overlay; tapping a room/network or the ✕ button closes it. Desktop layout is unchanged. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 1 + web/src/App.css | 61 ++++++++++++++++++++++++++++++ web/src/components/MessagePane.tsx | 7 +++- web/src/components/Sidebar.tsx | 7 ++-- web/src/pages/Chat.tsx | 8 ++-- 5 files changed, 76 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 42eca75..9d75ce1 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ build-daemon.sh deploy-web.sh deploy-daemon.sh serve-web.sh +push.sh web/public/config.js cmd/app/frontend/dist/* !cmd/app/frontend/dist/.gitkeep diff --git a/web/src/App.css b/web/src/App.css index 97ae738..0e7c800 100644 --- a/web/src/App.css +++ b/web/src/App.css @@ -164,3 +164,64 @@ details summary { color: var(--muted); font-size: 12px; cursor: pointer; } .file-entry-icon { font-size: 12px; flex-shrink: 0; } .history-divider { display: flex; align-items: center; gap: 8px; margin: 10px 0 6px; color: var(--muted); font-size: 11px; } .history-divider::before, .history-divider::after { content: ''; flex: 1; height: 1px; background: var(--border); } + +/* ── mobile hamburger / close buttons ── */ +.menu-btn-mobile { display: none; background: none; color: var(--muted); font-size: 18px; padding: 0 8px 0 0; line-height: 1; } +.menu-btn-mobile:hover { color: var(--text); background: none; } +.sidebar-close-mobile { display: none; background: none; color: var(--muted); font-size: 14px; padding: 2px 4px; } +.sidebar-close-mobile:hover { color: var(--text); background: none; } + +/* ── responsive layout ── */ +@media (max-width: 600px) { + :root { --sidebar-w: 80vw; } + + .chat-layout { + grid-template-columns: 1fr; + position: relative; + } + + /* sidebar slides in over the top */ + .sidebar { + position: fixed; + top: 0; left: 0; + width: var(--sidebar-w); + height: 100%; + z-index: 100; + transform: translateX(-100%); + transition: transform 0.22s ease; + box-shadow: 4px 0 24px rgba(0,0,0,0.5); + } + .chat-layout.sidebar-open .sidebar { + transform: translateX(0); + } + + /* dim overlay behind open sidebar */ + .chat-layout.sidebar-open::before { + content: ''; + position: fixed; + inset: 0; + background: rgba(0,0,0,0.45); + z-index: 99; + } + + .menu-btn-mobile { display: inline-block; } + .sidebar-close-mobile { display: inline-block; } + + /* message pane fills full width */ + .chat-layout > .message-pane { grid-column: 1; } + + /* file browser stacks below on mobile */ + .chat-layout.has-file-browser { grid-template-columns: 1fr; } + .chat-layout.has-file-browser > .file-browser { border-left: none; border-top: 1px solid var(--border); max-height: 40vh; overflow-y: auto; } + + /* slightly larger tap targets */ + .sidebar-item { padding: 8px 12px; font-size: 14px; } + .peer-row { padding: 6px 12px; } + .peer-row-actions { opacity: 1; } + .peer-action { font-size: 18px; padding: 2px 6px; } + + /* message layout: stack alias above text on very narrow screens */ + .message { flex-wrap: wrap; } + .message-ts { width: auto; min-width: 56px; } + .message-alias { width: auto; } +} diff --git a/web/src/components/MessagePane.tsx b/web/src/components/MessagePane.tsx index 536605c..12e5c61 100644 --- a/web/src/components/MessagePane.tsx +++ b/web/src/components/MessagePane.tsx @@ -12,7 +12,7 @@ function formatTs(ts: number): string { return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' + time } -export function MessagePane() { +export function MessagePane({ onMenuClick }: { onMenuClick: () => void }) { const { messages, historyCutoff, activeRoom, activeNetworkId, localPeer, connectedPeers, knownPeers, send } = useWaste() const [draft, setDraft] = useState('') const bottomRef = useRef(null) @@ -64,7 +64,10 @@ export function MessagePane() { return (
-
{roomLabel}
+
+ + {roomLabel} +
{dividerIdx === 0 && ( diff --git a/web/src/components/Sidebar.tsx b/web/src/components/Sidebar.tsx index 8ba37c2..c506274 100644 --- a/web/src/components/Sidebar.tsx +++ b/web/src/components/Sidebar.tsx @@ -26,7 +26,7 @@ function formatTs(ts: number | undefined): string { return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) } -export function Sidebar() { +export function Sidebar({ onClose }: { onClose: () => void }) { const { localPeer, masterId, masterAlias, networks, activeNetworkId, activeRoom, @@ -118,6 +118,7 @@ export function Sidebar() { {displayId.slice(0, 16).replace(/(.{4})/g, '$1 ').trim()}
+
@@ -134,7 +135,7 @@ export function Sidebar() { @@ -173,7 +174,7 @@ export function Sidebar() { diff --git a/web/src/pages/Chat.tsx b/web/src/pages/Chat.tsx index c219d97..e2a3156 100644 --- a/web/src/pages/Chat.tsx +++ b/web/src/pages/Chat.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react' import { Sidebar } from '../components/Sidebar' import { MessagePane } from '../components/MessagePane' import { FileBrowser } from '../components/FileBrowser' @@ -5,10 +6,11 @@ import { useWaste } from '../store' export function Chat() { const { activeFilePeer } = useWaste() + const [sidebarOpen, setSidebarOpen] = useState(false) return ( -
- - +
+ setSidebarOpen(false)} /> + setSidebarOpen(v => !v)} /> {activeFilePeer && }
)