responsive for mobile

This commit is contained in:
Chris Wanstrath 2026-02-08 13:56:04 -08:00
parent 396f214eae
commit b43c1b4660
11 changed files with 61 additions and 61 deletions

View File

@ -1,10 +1,11 @@
import { define } from '@because/forge' import { define } from '@because/forge'
import type { App } from '../../shared/types' import type { App } from '../../shared/types'
import { restartApp, startApp, stopApp } from '../api' import { restartApp, startApp, stopApp } from '../api'
import { openDeleteAppModal, openRenameAppModal } from '../modals' import { openAppSelectorModal, openDeleteAppModal, openRenameAppModal } from '../modals'
import { apps, getSelectedTab } from '../state' import { apps, getSelectedTab, isNarrow } from '../state'
import { import {
ActionBar, ActionBar,
AppSelectorChevron,
Button, Button,
ClickableAppName, ClickableAppName,
HeaderActions, HeaderActions,
@ -53,6 +54,11 @@ export function AppDetail({ app, render }: { app: App, render: () => void }) {
<OpenEmojiPicker app={app} render={render}>{app.icon}</OpenEmojiPicker> <OpenEmojiPicker app={app} render={render}>{app.icon}</OpenEmojiPicker>
&nbsp; &nbsp;
<ClickableAppName onClick={() => openRenameAppModal(app)}>{app.name}</ClickableAppName> <ClickableAppName onClick={() => openRenameAppModal(app)}>{app.name}</ClickableAppName>
{isNarrow && (
<AppSelectorChevron onClick={() => openAppSelectorModal(render)}>
</AppSelectorChevron>
)}
</MainTitle> </MainTitle>
<HeaderActions> <HeaderActions>
<Button variant="danger" onClick={() => openDeleteAppModal(app)}>Delete</Button> <Button variant="danger" onClick={() => openDeleteAppModal(app)}>Delete</Button>

View File

@ -1,6 +1,6 @@
import { Styles } from '@because/forge' import { Styles } from '@because/forge'
import { Modal } from './modal' import { Modal } from './modal'
import { apps, selectedApp } from '../state' import { apps, isNarrow, selectedApp } from '../state'
import { EmptyState, Layout } from '../styles' import { EmptyState, Layout } from '../styles'
import { AppDetail } from './AppDetail' import { AppDetail } from './AppDetail'
import { Sidebar } from './Sidebar' import { Sidebar } from './Sidebar'
@ -11,7 +11,7 @@ export function Dashboard({ render }: { render: () => void }) {
return ( return (
<Layout> <Layout>
<Styles /> <Styles />
<Sidebar render={render} /> {!isNarrow && <Sidebar render={render} />}
{selected ? ( {selected ? (
<AppDetail app={selected} render={render} /> <AppDetail app={selected} render={render} />
) : ( ) : (

View File

@ -1,47 +1,24 @@
import { openNewAppModal } from '../modals' import { openNewAppModal } from '../modals'
import { import {
apps,
selectedApp,
setSelectedApp,
setSidebarCollapsed, setSidebarCollapsed,
setSidebarSection,
sidebarCollapsed, sidebarCollapsed,
sidebarSection,
} from '../state' } from '../state'
import { import {
AppItem,
AppList,
HamburgerButton, HamburgerButton,
HamburgerLine, HamburgerLine,
Logo, Logo,
NewAppButton, NewAppButton,
SectionSwitcher,
SectionTab,
Sidebar as SidebarContainer, Sidebar as SidebarContainer,
SidebarFooter, SidebarFooter,
StatusDot,
} from '../styles' } from '../styles'
import { AppSelector } from './AppSelector'
export function Sidebar({ render }: { render: () => void }) { export function Sidebar({ render }: { render: () => void }) {
const selectApp = (name: string) => {
setSelectedApp(name)
render()
}
const toggleSidebar = () => { const toggleSidebar = () => {
setSidebarCollapsed(!sidebarCollapsed) setSidebarCollapsed(!sidebarCollapsed)
render() 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 ( return (
<SidebarContainer style={sidebarCollapsed ? { width: 'auto' } : undefined}> <SidebarContainer style={sidebarCollapsed ? { width: 'auto' } : undefined}>
<Logo> <Logo>
@ -52,37 +29,7 @@ export function Sidebar({ render }: { render: () => void }) {
<HamburgerLine /> <HamburgerLine />
</HamburgerButton> </HamburgerButton>
</Logo> </Logo>
{!sidebarCollapsed && toolApps.length > 0 && ( <AppSelector render={render} collapsed={sidebarCollapsed} />
<SectionSwitcher>
<SectionTab active={sidebarSection === 'apps' ? true : undefined} onClick={() => switchSection('apps')}>
Apps
</SectionTab>
<SectionTab active={sidebarSection === 'tools' ? true : undefined} onClick={() => switchSection('tools')}>
Tools
</SectionTab>
</SectionSwitcher>
)}
<AppList>
{activeApps.map(app => (
<AppItem
key={app.name}
onClick={() => selectApp(app.name)}
selected={app.name === selectedApp ? true : undefined}
style={sidebarCollapsed ? { justifyContent: 'center', padding: '10px 12px' } : undefined}
title={sidebarCollapsed ? app.name : undefined}
>
{sidebarCollapsed ? (
<span style={{ fontSize: 18 }}>{app.icon}</span>
) : (
<>
<span style={{ fontSize: 14 }}>{app.icon}</span>
{app.name}
<StatusDot state={app.state} style={{ marginLeft: 'auto' }} />
</>
)}
</AppItem>
))}
</AppList>
{!sidebarCollapsed && ( {!sidebarCollapsed && (
<SidebarFooter> <SidebarFooter>
<NewAppButton onClick={openNewAppModal}>+ New App</NewAppButton> <NewAppButton onClick={openNewAppModal}>+ New App</NewAppButton>

View File

@ -1,4 +1,5 @@
export { AppDetail } from './AppDetail' export { AppDetail } from './AppDetail'
export { AppSelector } from './AppSelector'
export { Dashboard } from './Dashboard' export { Dashboard } from './Dashboard'
export { Nav } from './Nav' export { Nav } from './Nav'
export { Sidebar } from './Sidebar' export { Sidebar } from './Sidebar'

View File

@ -41,8 +41,9 @@ const ModalBackdrop = define('ModalBackdrop', {
inset: 0, inset: 0,
background: 'rgba(0, 0, 0, 0.5)', background: 'rgba(0, 0, 0, 0.5)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'flex-start',
justifyContent: 'center', justifyContent: 'center',
paddingTop: '20vh',
zIndex: 1000, zIndex: 1000,
}) })

View File

@ -1,6 +1,6 @@
import { render as renderApp } from 'hono/jsx/dom' import { render as renderApp } from 'hono/jsx/dom'
import { Dashboard } from './components' 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 { initModal } from './components/modal'
import { initToolIframes, updateToolIframes } from './tool-iframes' import { initToolIframes, updateToolIframes } from './tool-iframes'
import { initUpdate } from './update' import { initUpdate } from './update'
@ -34,6 +34,13 @@ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ()
// Set initial theme // Set initial theme
setTheme() setTheme()
// Listen for narrow screen changes
const narrowQuery = window.matchMedia('(max-width: 768px)')
narrowQuery.addEventListener('change', e => {
setIsNarrow(e.matches)
render()
})
// SSE connection // SSE connection
const events = new EventSource('/api/apps/stream') const events = new EventSource('/api/apps/stream')
events.onmessage = e => { events.onmessage = e => {

View File

@ -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', () => (
<AppSelector
render={renderFn}
onSelect={closeModal}
switcherStyle={{ padding: '0 0 12px', marginLeft: -20, marginRight: -20, paddingLeft: 20, paddingRight: 20, marginBottom: 8 }}
listStyle={{ maxHeight: 300, overflow: 'auto' }}
/>
))
}

View File

@ -1,3 +1,4 @@
export { openAppSelectorModal } from './AppSelector'
export { openDeleteAppModal } from './DeleteApp' export { openDeleteAppModal } from './DeleteApp'
export { openNewAppModal } from './NewApp' export { openNewAppModal } from './NewApp'
export { openRenameAppModal } from './RenameApp' export { openRenameAppModal } from './RenameApp'

View File

@ -1,6 +1,7 @@
import type { App } from '../shared/types' import type { App } from '../shared/types'
// UI state (survives re-renders) // 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 selectedApp: string | null = localStorage.getItem('selectedApp')
export let sidebarCollapsed: boolean = localStorage.getItem('sidebarCollapsed') === 'true' export let sidebarCollapsed: boolean = localStorage.getItem('sidebarCollapsed') === 'true'
export let sidebarSection: 'apps' | 'tools' = (localStorage.getItem('sidebarSection') as 'apps' | 'tools') || 'apps' 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) { export function setSidebarCollapsed(collapsed: boolean) {
sidebarCollapsed = collapsed sidebarCollapsed = collapsed
localStorage.setItem('sidebarCollapsed', String(collapsed)) localStorage.setItem('sidebarCollapsed', String(collapsed))

View File

@ -3,6 +3,7 @@ export { Form, FormActions, FormCheckbox, FormCheckboxField, FormCheckboxLabel,
export { export {
AppItem, AppItem,
AppList, AppList,
AppSelectorChevron,
ClickableAppName, ClickableAppName,
HamburgerButton, HamburgerButton,
HamburgerLine, HamburgerLine,

View File

@ -147,6 +147,20 @@ export const MainTitle = define('MainTitle', {
margin: 0, 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', { export const ClickableAppName = define('ClickableAppName', {
cursor: 'pointer', cursor: 'pointer',
borderRadius: theme('radius-md'), borderRadius: theme('radius-md'),