responsive for mobile
This commit is contained in:
parent
396f214eae
commit
b43c1b4660
|
|
@ -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 }) {
|
|||
<OpenEmojiPicker app={app} render={render}>{app.icon}</OpenEmojiPicker>
|
||||
|
||||
<ClickableAppName onClick={() => openRenameAppModal(app)}>{app.name}</ClickableAppName>
|
||||
{isNarrow && (
|
||||
<AppSelectorChevron onClick={() => openAppSelectorModal(render)}>
|
||||
▼
|
||||
</AppSelectorChevron>
|
||||
)}
|
||||
</MainTitle>
|
||||
<HeaderActions>
|
||||
<Button variant="danger" onClick={() => openDeleteAppModal(app)}>Delete</Button>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Layout>
|
||||
<Styles />
|
||||
<Sidebar render={render} />
|
||||
{!isNarrow && <Sidebar render={render} />}
|
||||
{selected ? (
|
||||
<AppDetail app={selected} render={render} />
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<SidebarContainer style={sidebarCollapsed ? { width: 'auto' } : undefined}>
|
||||
<Logo>
|
||||
|
|
@ -52,37 +29,7 @@ export function Sidebar({ render }: { render: () => void }) {
|
|||
<HamburgerLine />
|
||||
</HamburgerButton>
|
||||
</Logo>
|
||||
{!sidebarCollapsed && toolApps.length > 0 && (
|
||||
<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>
|
||||
<AppSelector render={render} collapsed={sidebarCollapsed} />
|
||||
{!sidebarCollapsed && (
|
||||
<SidebarFooter>
|
||||
<NewAppButton onClick={openNewAppModal}>+ New App</NewAppButton>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export { AppDetail } from './AppDetail'
|
||||
export { AppSelector } from './AppSelector'
|
||||
export { Dashboard } from './Dashboard'
|
||||
export { Nav } from './Nav'
|
||||
export { Sidebar } from './Sidebar'
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
17
src/client/modals/AppSelector.tsx
Normal file
17
src/client/modals/AppSelector.tsx
Normal 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' }}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export { openAppSelectorModal } from './AppSelector'
|
||||
export { openDeleteAppModal } from './DeleteApp'
|
||||
export { openNewAppModal } from './NewApp'
|
||||
export { openRenameAppModal } from './RenameApp'
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ export { Form, FormActions, FormCheckbox, FormCheckboxField, FormCheckboxLabel,
|
|||
export {
|
||||
AppItem,
|
||||
AppList,
|
||||
AppSelectorChevron,
|
||||
ClickableAppName,
|
||||
HamburgerButton,
|
||||
HamburgerLine,
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user