diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx index e223b94..e7c6c6c 100644 --- a/web/src/components/Layout.tsx +++ b/web/src/components/Layout.tsx @@ -13,22 +13,20 @@ import { NotificationPrompt } from './NotificationPrompt.tsx'; import { MemberList } from './MemberList.tsx'; import { VoicePanel } from './VoicePanel.tsx'; import { ServerSettingsModal } from './ServerSettingsModal.tsx'; + const STATUS_CYCLE: UserStatus[] = ['online', 'idle', 'dnd', 'offline']; function statusColor(status: UserStatus): string { switch (status) { - case 'online': - return 'bg-gb-green'; - case 'idle': - return 'bg-gb-yellow'; - case 'dnd': - return 'bg-gb-red'; - default: - return 'bg-gb-gray'; + case 'online': return 'bg-gb-green'; + case 'idle': return 'bg-gb-yellow'; + case 'dnd': return 'bg-gb-red'; + default: return 'bg-gb-gray'; } } function statusLabel(status: UserStatus): string { return status.toUpperCase(); } + export function Layout() { const isAuthenticated = useAuthStore((state) => state.isAuthenticated); const isLoading = useAuthStore((state) => state.isLoading); @@ -49,23 +47,22 @@ export function Layout() { const [activeTab, setActiveTab] = useState<'chat' | 'voice'>('chat'); useEffect(() => { - if (currentVoiceRoom) { - setActiveTab('voice'); - } else { - setActiveTab('chat'); - } + if (currentVoiceRoom) setActiveTab('voice'); + else setActiveTab('chat'); }, [currentVoiceRoom]); useEffect(() => { wsConnect(); return () => { wsDisconnect(); }; }, [wsConnect, wsDisconnect]); + useEffect(() => { if (!isLoading && !isAuthenticated && location.pathname !== '/login') { navigate('/login', { replace: true }); } }, [isLoading, isAuthenticated, location.pathname, navigate]); - // Navigate to chat when a channel/DM is selected on mobile + + // Navigate to chat view on mobile when a channel/DM is selected useEffect(() => { const unsub = useChannelStore.subscribe((state, prev) => { if (state.activeChannelId !== prev.activeChannelId && state.activeChannelId) { @@ -74,68 +71,52 @@ export function Layout() { }); return unsub; }, [setMobileView]); - // Also switch to chat on mobile when navigating to a DM + useEffect(() => { if (location.pathname.startsWith('/dm/') && location.pathname !== '/dm') { setMobileView('chat'); } }, [location.pathname, setMobileView]); + const handleStatusChange = async (status: UserStatus) => { setShowStatusMenu(false); - try { - await updateProfile({ status }); - } catch (err) { - // Error is surfaced via auth store; menu closes optimistically. - } + try { await updateProfile({ status }); } catch { /* store surfaces error */ } }; const currentStatus = user?.status ?? 'offline'; + if (isLoading) { - return ( -
- [booting...] -
- ); - } - if (!isAuthenticated) { - return null; + return
[booting...]
; } + if (!isAuthenticated) return null; + return ( -
-
- {/* Top bar */} -
+
+ {/* Outer terminal frame with safe area */} +
+ + {/* Top bar — minimal on mobile */} +
- {/* Mobile: hamburger to show sidebar */} - - DUMPSTER + DUMPSTER
-
+
{showStatusMenu && (
{STATUS_CYCLE.map((s) => ( - @@ -143,76 +124,44 @@ export function Layout() {
)}
- [SETTINGS] + [SETTINGS] {activeServerId && ( - )} - {/* Mobile: members toggle */} - {!isDM && ( - - )} -
- {/* Main content area */} + {/* Main content */}
- {/* Sidebar: ServerBar + ChannelList/ConversationList */} - {/* Desktop: always visible as left columns */} - {/* Mobile: full-screen overlay when mobileView === 'sidebar' */} -
+ {/* Sidebar — desktop: always visible, mobile: overlay when sidebar tab active */} +
{isDM ? : } - {/* Close sidebar on mobile after selection */} -
setMobileView('chat')} - /> + {/* Close overlay on mobile by tapping background */} +
setMobileView('chat')} />
- {/* Chat area: always in DOM, hidden on mobile when sidebar/members shown */} -
+ {/* Chat */} +
{currentVoiceRoom && (
- - + +
)}
@@ -226,32 +175,31 @@ export function Layout() {
- {/* Members panel */} - {/* Desktop: right column */} - {/* Mobile: full-screen overlay when mobileView === 'members' */} + {/* Members — desktop: right column, mobile: overlay when members tab active */} {!isDM && ( -
- {/* Tap background to close on mobile */} -
setMobileView('chat')} - /> +
+
setMobileView('chat')} />
)}
+ {/* Bottom nav — mobile only */} + + {showServerSettings && activeServerId && ( setShowServerSettings(false)} /> )} -
+ + {/* Status bar — desktop only */} +
TERM {__APP_VERSION__} {new Date().toISOString().slice(0, 10)}
@@ -260,3 +208,50 @@ export function Layout() {
); } + +// MobileBottomNav — tab bar for switching between sidebar / chat / members +function MobileBottomNav({ + mobileView, + onViewChange, + isDM, +}: { + mobileView: string; + onViewChange: (v: 'sidebar' | 'chat' | 'members') => void; + isDM: boolean; +}) { + const isConnected = useVoiceStore((s) => s.isConnected); + + return ( +
+ onViewChange('sidebar')}> + [SERVERS] + + onViewChange('chat')}> + [CHAT] + + {!isDM && ( + onViewChange('members')}> + [MEMBERS] + + )} + {isConnected && ( +
+ )} +
+ ); +} + +function NavTab({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) { + return ( + + ); +}