responsive for mobile
This commit is contained in:
parent
396f214eae
commit
b43c1b4660
|
|
@ -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>
|
||||||
|
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
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 { openDeleteAppModal } from './DeleteApp'
|
||||||
export { openNewAppModal } from './NewApp'
|
export { openNewAppModal } from './NewApp'
|
||||||
export { openRenameAppModal } from './RenameApp'
|
export { openRenameAppModal } from './RenameApp'
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ export { Form, FormActions, FormCheckbox, FormCheckboxField, FormCheckboxLabel,
|
||||||
export {
|
export {
|
||||||
AppItem,
|
AppItem,
|
||||||
AppList,
|
AppList,
|
||||||
|
AppSelectorChevron,
|
||||||
ClickableAppName,
|
ClickableAppName,
|
||||||
HamburgerButton,
|
HamburgerButton,
|
||||||
HamburgerLine,
|
HamburgerLine,
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user