From b43c1b466011dc380067802d446ecaa1ae3f2f6d Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sun, 8 Feb 2026 13:56:04 -0800 Subject: [PATCH] responsive for mobile --- src/client/components/AppDetail.tsx | 10 ++++- src/client/components/Dashboard.tsx | 4 +- src/client/components/Sidebar.tsx | 57 +---------------------------- src/client/components/index.ts | 1 + src/client/components/modal.tsx | 3 +- src/client/index.tsx | 9 ++++- src/client/modals/AppSelector.tsx | 17 +++++++++ src/client/modals/index.ts | 1 + src/client/state.ts | 5 +++ src/client/styles/index.ts | 1 + src/client/styles/layout.ts | 14 +++++++ 11 files changed, 61 insertions(+), 61 deletions(-) create mode 100644 src/client/modals/AppSelector.tsx diff --git a/src/client/components/AppDetail.tsx b/src/client/components/AppDetail.tsx index a9be58a..84b7442 100644 --- a/src/client/components/AppDetail.tsx +++ b/src/client/components/AppDetail.tsx @@ -1,10 +1,11 @@ import { define } from '@because/forge' import type { App } from '../../shared/types' import { restartApp, startApp, stopApp } from '../api' -import { openDeleteAppModal, openRenameAppModal } from '../modals' -import { apps, getSelectedTab } from '../state' +import { openAppSelectorModal, openDeleteAppModal, openRenameAppModal } from '../modals' +import { apps, getSelectedTab, isNarrow } from '../state' import { ActionBar, + AppSelectorChevron, Button, ClickableAppName, HeaderActions, @@ -53,6 +54,11 @@ export function AppDetail({ app, render }: { app: App, render: () => void }) { {app.icon}   openRenameAppModal(app)}>{app.name} + {isNarrow && ( + openAppSelectorModal(render)}> + ▼ + + )} diff --git a/src/client/components/Dashboard.tsx b/src/client/components/Dashboard.tsx index 356ff74..4c436be 100644 --- a/src/client/components/Dashboard.tsx +++ b/src/client/components/Dashboard.tsx @@ -1,6 +1,6 @@ import { Styles } from '@because/forge' import { Modal } from './modal' -import { apps, selectedApp } from '../state' +import { apps, isNarrow, selectedApp } from '../state' import { EmptyState, Layout } from '../styles' import { AppDetail } from './AppDetail' import { Sidebar } from './Sidebar' @@ -11,7 +11,7 @@ export function Dashboard({ render }: { render: () => void }) { return ( - + {!isNarrow && } {selected ? ( ) : ( diff --git a/src/client/components/Sidebar.tsx b/src/client/components/Sidebar.tsx index d9dd3f4..c3987e7 100644 --- a/src/client/components/Sidebar.tsx +++ b/src/client/components/Sidebar.tsx @@ -1,47 +1,24 @@ import { openNewAppModal } from '../modals' import { - apps, - selectedApp, - setSelectedApp, setSidebarCollapsed, - setSidebarSection, sidebarCollapsed, - sidebarSection, } from '../state' import { - AppItem, - AppList, HamburgerButton, HamburgerLine, Logo, NewAppButton, - SectionSwitcher, - SectionTab, Sidebar as SidebarContainer, SidebarFooter, - StatusDot, } from '../styles' +import { AppSelector } from './AppSelector' export function Sidebar({ render }: { render: () => void }) { - const selectApp = (name: string) => { - setSelectedApp(name) - render() - } - const toggleSidebar = () => { setSidebarCollapsed(!sidebarCollapsed) render() } - const switchSection = (section: 'apps' | 'tools') => { - setSidebarSection(section) - render() - } - - const regularApps = apps.filter(app => !app.tool) - const toolApps = apps.filter(app => app.tool) - const activeApps = sidebarSection === 'apps' ? regularApps : toolApps - return ( @@ -52,37 +29,7 @@ export function Sidebar({ render }: { render: () => void }) { - {!sidebarCollapsed && toolApps.length > 0 && ( - - switchSection('apps')}> - Apps - - switchSection('tools')}> - Tools - - - )} - - {activeApps.map(app => ( - selectApp(app.name)} - selected={app.name === selectedApp ? true : undefined} - style={sidebarCollapsed ? { justifyContent: 'center', padding: '10px 12px' } : undefined} - title={sidebarCollapsed ? app.name : undefined} - > - {sidebarCollapsed ? ( - {app.icon} - ) : ( - <> - {app.icon} - {app.name} - - - )} - - ))} - + {!sidebarCollapsed && ( + New App diff --git a/src/client/components/index.ts b/src/client/components/index.ts index 318742c..c6f1f1c 100644 --- a/src/client/components/index.ts +++ b/src/client/components/index.ts @@ -1,4 +1,5 @@ export { AppDetail } from './AppDetail' +export { AppSelector } from './AppSelector' export { Dashboard } from './Dashboard' export { Nav } from './Nav' export { Sidebar } from './Sidebar' diff --git a/src/client/components/modal.tsx b/src/client/components/modal.tsx index 4629cd8..d513285 100644 --- a/src/client/components/modal.tsx +++ b/src/client/components/modal.tsx @@ -41,8 +41,9 @@ const ModalBackdrop = define('ModalBackdrop', { inset: 0, background: 'rgba(0, 0, 0, 0.5)', display: 'flex', - alignItems: 'center', + alignItems: 'flex-start', justifyContent: 'center', + paddingTop: '20vh', zIndex: 1000, }) diff --git a/src/client/index.tsx b/src/client/index.tsx index cdc8fc0..33236b5 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -1,6 +1,6 @@ import { render as renderApp } from 'hono/jsx/dom' import { Dashboard } from './components' -import { apps, getSelectedTab, selectedApp, setApps, setSelectedApp } from './state' +import { apps, getSelectedTab, selectedApp, setApps, setIsNarrow, setSelectedApp } from './state' import { initModal } from './components/modal' import { initToolIframes, updateToolIframes } from './tool-iframes' import { initUpdate } from './update' @@ -34,6 +34,13 @@ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () // Set initial theme setTheme() +// Listen for narrow screen changes +const narrowQuery = window.matchMedia('(max-width: 768px)') +narrowQuery.addEventListener('change', e => { + setIsNarrow(e.matches) + render() +}) + // SSE connection const events = new EventSource('/api/apps/stream') events.onmessage = e => { diff --git a/src/client/modals/AppSelector.tsx b/src/client/modals/AppSelector.tsx new file mode 100644 index 0000000..53917d9 --- /dev/null +++ b/src/client/modals/AppSelector.tsx @@ -0,0 +1,17 @@ +import { closeModal, openModal } from '../components/modal' +import { AppSelector } from '../components/AppSelector' + +let renderFn: () => void + +export function openAppSelectorModal(render: () => void) { + renderFn = render + + openModal('Select App', () => ( + + )) +} diff --git a/src/client/modals/index.ts b/src/client/modals/index.ts index 16d10cd..48017df 100644 --- a/src/client/modals/index.ts +++ b/src/client/modals/index.ts @@ -1,3 +1,4 @@ +export { openAppSelectorModal } from './AppSelector' export { openDeleteAppModal } from './DeleteApp' export { openNewAppModal } from './NewApp' export { openRenameAppModal } from './RenameApp' diff --git a/src/client/state.ts b/src/client/state.ts index a62dd31..bc59146 100644 --- a/src/client/state.ts +++ b/src/client/state.ts @@ -1,6 +1,7 @@ import type { App } from '../shared/types' // UI state (survives re-renders) +export let isNarrow: boolean = window.matchMedia('(max-width: 768px)').matches export let selectedApp: string | null = localStorage.getItem('selectedApp') export let sidebarCollapsed: boolean = localStorage.getItem('sidebarCollapsed') === 'true' export let sidebarSection: 'apps' | 'tools' = (localStorage.getItem('sidebarSection') as 'apps' | 'tools') || 'apps' @@ -21,6 +22,10 @@ export function setSelectedApp(name: string | null) { } } +export function setIsNarrow(narrow: boolean) { + isNarrow = narrow +} + export function setSidebarCollapsed(collapsed: boolean) { sidebarCollapsed = collapsed localStorage.setItem('sidebarCollapsed', String(collapsed)) diff --git a/src/client/styles/index.ts b/src/client/styles/index.ts index eb8b995..42a6847 100644 --- a/src/client/styles/index.ts +++ b/src/client/styles/index.ts @@ -3,6 +3,7 @@ export { Form, FormActions, FormCheckbox, FormCheckboxField, FormCheckboxLabel, export { AppItem, AppList, + AppSelectorChevron, ClickableAppName, HamburgerButton, HamburgerLine, diff --git a/src/client/styles/layout.ts b/src/client/styles/layout.ts index 347188d..593e65b 100644 --- a/src/client/styles/layout.ts +++ b/src/client/styles/layout.ts @@ -147,6 +147,20 @@ export const MainTitle = define('MainTitle', { margin: 0, }) +export const AppSelectorChevron = define('AppSelectorChevron', { + base: 'button', + background: 'none', + border: 'none', + cursor: 'pointer', + padding: '4px 8px', + marginLeft: 4, + fontSize: 14, + color: theme('colors-textMuted'), + selectors: { + '&:hover': { color: theme('colors-text') }, + }, +}) + export const ClickableAppName = define('ClickableAppName', { cursor: 'pointer', borderRadius: theme('radius-md'),