Mobile chat UX: error boundary + viewport-safe responsive layout (#59) #60

Merged
westfarn merged 1 commits from fix/mobile-conversation-drawer-ux into master 2026-07-29 05:04:33 -07:00
@@ -2,7 +2,7 @@ import React, { useContext, useEffect, useRef, useState } from "react";
import styled, { ThemeContext } from "styled-components";
import { Formik, Form, Field } from "formik";
import * as Yup from "yup";
import { AttachFile, Delete, Send, Menu } from "@mui/icons-material"; // Keeping icons for now, can replace later if needed
import { AttachFile, Delete, Send, Menu, Close } from "@mui/icons-material"; // Keeping icons for now, can replace later if needed
import { Tooltip } from "@mui/material";
import Markdown from "markdown-to-jsx";
@@ -44,10 +44,15 @@ const Sidebar = styled.div<{ $isOpen: boolean }>`
transition: transform 0.3s ease;
@media (max-width: 768px) {
position: absolute;
position: fixed;
top: 0;
left: 0;
width: min(85vw, 320px);
height: 100vh;
transform: ${({ $isOpen }) => $isOpen ? 'translateX(0)' : 'translateX(-100%)'};
background: ${({ theme }) => theme.darkMode ? 'rgba(0, 0, 0, 0.95)' : 'rgba(255, 255, 255, 0.95)'};
box-shadow: ${({ $isOpen }) => $isOpen ? '0 0 20px rgba(0,0,0,0.5)' : 'none'};
z-index: 120;
}
`;
@@ -88,7 +93,7 @@ const Overlay = styled.div<{ $isOpen: boolean }>`
height: 100vh;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(2px);
z-index: 15;
z-index: 110;
opacity: ${({ $isOpen }) => $isOpen ? 1 : 0};
pointer-events: ${({ $isOpen }) => $isOpen ? 'auto' : 'none'};
transition: opacity 0.3s ease;
@@ -98,6 +103,36 @@ const Overlay = styled.div<{ $isOpen: boolean }>`
}
`;
const MobileSidebarHeader = styled.div`
display: none;
@media (max-width: 768px) {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
color: ${({ theme }) => theme.colors.text};
font-weight: 700;
font-size: 1rem;
}
`;
const MobileSidebarCloseButton = styled.button`
display: none;
@media (max-width: 768px) {
display: flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
color: ${({ theme }) => theme.colors.text};
cursor: pointer;
border-radius: 999px;
padding: 0.3rem;
}
`;
const MainContent = styled.div`
flex: 1;
height: 100%;
@@ -384,6 +419,12 @@ const AsyncDashboardInner = (): JSX.Element => {
</MobileSidebarToggle>
<Sidebar $isOpen={isSidebarOpen}>
<MobileSidebarHeader>
<span>Conversations</span>
<MobileSidebarCloseButton onClick={() => setIsSidebarOpen(false)} aria-label="Close conversations drawer">
<Close fontSize="small" />
</MobileSidebarCloseButton>
</MobileSidebarHeader>
<NewChatButton onClick={() => {
setSelectedConversation(undefined);
setIsSidebarOpen(false);
@@ -391,30 +432,41 @@ const AsyncDashboardInner = (): JSX.Element => {
+ New Chat
</NewChatButton>
<div style={{ overflowY: 'auto', flex: 1 }}>
{conversations.map((convo) => (
<ConversationItem
key={convo.id}
$active={convo.id === selectedConversation}
onClick={() => {
setSelectedConversation(convo.id);
setIsSidebarOpen(false);
}}
>
<ConversationTitle>{convo.title || "New Conversation"}</ConversationTitle>
{convo.id === selectedConversation && (
<IconButton
as="div"
onClick={(e) => {
e.stopPropagation();
deleteConversation(convo.id);
}}
style={{ padding: 4, width: 'auto', height: 'auto' }}
>
<Delete fontSize="small" />
</IconButton>
)}
</ConversationItem>
))}
{conversations.length === 0 ? (
<div style={{
color: theme?.darkMode ? 'rgba(255,255,255,0.55)' : 'rgba(0,0,0,0.55)',
fontSize: '0.9rem',
lineHeight: 1.5,
padding: '0.25rem 0.5rem'
}}>
No conversations yet. Start with <strong>+ New Chat</strong>.
</div>
) : (
conversations.map((convo) => (
<ConversationItem
key={convo.id}
$active={convo.id === selectedConversation}
onClick={() => {
setSelectedConversation(convo.id);
setIsSidebarOpen(false);
}}
>
<ConversationTitle>{convo.title || "New Conversation"}</ConversationTitle>
{convo.id === selectedConversation && (
<IconButton
as="div"
onClick={(e) => {
e.stopPropagation();
deleteConversation(convo.id);
}}
style={{ padding: 4, width: 'auto', height: 'auto' }}
>
<Delete fontSize="small" />
</IconButton>
)}
</ConversationItem>
))
)}
</div>
</Sidebar>