Merge branch 'mobile'
This commit is contained in:
commit
d29e306e61
|
|
@ -2,13 +2,14 @@ import { define } from '@because/forge'
|
|||
import type { App } from '../../shared/types'
|
||||
import { buildAppUrl } from '../../shared/urls'
|
||||
import { restartApp, shareApp, startApp, stopApp, unshareApp } from '../api'
|
||||
import { openAppSelectorModal, openDeleteAppModal, openRenameAppModal } from '../modals'
|
||||
import { apps, getSelectedTab, isNarrow } from '../state'
|
||||
import { openDeleteAppModal, openRenameAppModal } from '../modals'
|
||||
import { apps, getSelectedTab, isNarrow, setMobileSidebar } from '../state'
|
||||
import {
|
||||
ActionBar,
|
||||
AppSelectorChevron,
|
||||
Button,
|
||||
ClickableAppName,
|
||||
HamburgerButton,
|
||||
HamburgerLine,
|
||||
HeaderActions,
|
||||
InfoLabel,
|
||||
InfoRow,
|
||||
|
|
@ -52,14 +53,15 @@ export function AppDetail({ app, render }: { app: App, render: () => void }) {
|
|||
<Main>
|
||||
<MainHeader>
|
||||
<MainTitle>
|
||||
<OpenEmojiPicker app={app} render={render}>{app.icon}</OpenEmojiPicker>
|
||||
|
||||
<ClickableAppName onClick={() => openRenameAppModal(app)}>{app.name}</ClickableAppName>
|
||||
{isNarrow && (
|
||||
<AppSelectorChevron onClick={() => openAppSelectorModal(render)}>
|
||||
▼
|
||||
</AppSelectorChevron>
|
||||
<HamburgerButton onClick={() => { setMobileSidebar(true); render() }} title="Show apps">
|
||||
<HamburgerLine />
|
||||
<HamburgerLine />
|
||||
<HamburgerLine />
|
||||
</HamburgerButton>
|
||||
)}
|
||||
<OpenEmojiPicker app={app} render={render}>{app.icon}</OpenEmojiPicker>
|
||||
<ClickableAppName onClick={() => openRenameAppModal(app)}>{app.name}</ClickableAppName>
|
||||
</MainTitle>
|
||||
<HeaderActions>
|
||||
{!app.tool && (
|
||||
|
|
|
|||
|
|
@ -17,11 +17,12 @@ interface AppSelectorProps {
|
|||
render: () => void
|
||||
onSelect?: () => void
|
||||
collapsed?: boolean
|
||||
large?: boolean
|
||||
switcherStyle?: CSSProperties
|
||||
listStyle?: CSSProperties
|
||||
}
|
||||
|
||||
export function AppSelector({ render, onSelect, collapsed, switcherStyle, listStyle }: AppSelectorProps) {
|
||||
export function AppSelector({ render, onSelect, collapsed, large, switcherStyle, listStyle }: AppSelectorProps) {
|
||||
const switchSection = (section: 'apps' | 'tools') => {
|
||||
setSidebarSection(section)
|
||||
render()
|
||||
|
|
@ -35,10 +36,10 @@ export function AppSelector({ render, onSelect, collapsed, switcherStyle, listSt
|
|||
<>
|
||||
{!collapsed && toolApps.length > 0 && (
|
||||
<SectionSwitcher style={switcherStyle}>
|
||||
<SectionTab active={sidebarSection === 'apps' ? true : undefined} onClick={() => switchSection('apps')}>
|
||||
<SectionTab active={sidebarSection === 'apps' ? true : undefined} large={large || undefined} onClick={() => switchSection('apps')}>
|
||||
Apps
|
||||
</SectionTab>
|
||||
<SectionTab active={sidebarSection === 'tools' ? true : undefined} onClick={() => switchSection('tools')}>
|
||||
<SectionTab active={sidebarSection === 'tools' ? true : undefined} large={large || undefined} onClick={() => switchSection('tools')}>
|
||||
Tools
|
||||
</SectionTab>
|
||||
</SectionSwitcher>
|
||||
|
|
@ -59,6 +60,7 @@ export function AppSelector({ render, onSelect, collapsed, switcherStyle, listSt
|
|||
key={app.name}
|
||||
href={`/app/${app.name}`}
|
||||
onClick={onSelect}
|
||||
large={large || undefined}
|
||||
selected={app.name === selectedApp ? true : undefined}
|
||||
style={collapsed ? { justifyContent: 'center', padding: '10px 12px' } : undefined}
|
||||
title={collapsed ? app.name : undefined}
|
||||
|
|
@ -67,7 +69,7 @@ export function AppSelector({ render, onSelect, collapsed, switcherStyle, listSt
|
|||
<span style={{ fontSize: 18 }}>{app.icon}</span>
|
||||
) : (
|
||||
<>
|
||||
<span style={{ fontSize: 14 }}>{app.icon}</span>
|
||||
<span style={{ fontSize: large ? 20 : 14 }}>{app.icon}</span>
|
||||
{app.name}
|
||||
<StatusDot state={app.state} data-app={app.name} data-state={app.state} style={{ marginLeft: 'auto' }} />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,48 @@
|
|||
import { Styles } from '@because/forge'
|
||||
import { apps, currentView, isNarrow, selectedApp } from '../state'
|
||||
import { Layout } from '../styles'
|
||||
import { openNewAppModal } from '../modals'
|
||||
import { apps, currentView, isNarrow, mobileSidebar, selectedApp, setMobileSidebar } from '../state'
|
||||
import {
|
||||
HamburgerButton,
|
||||
HamburgerLine,
|
||||
Layout,
|
||||
Main,
|
||||
MainContent as MainContentContainer,
|
||||
MainHeader,
|
||||
MainTitle,
|
||||
NewAppButton,
|
||||
} from '../styles'
|
||||
import { AppDetail } from './AppDetail'
|
||||
import { AppSelector } from './AppSelector'
|
||||
import { DashboardLanding } from './DashboardLanding'
|
||||
import { Modal } from './modal'
|
||||
import { SettingsPage } from './SettingsPage'
|
||||
import { Sidebar } from './Sidebar'
|
||||
|
||||
function MobileSidebar({ render }: { render: () => void }) {
|
||||
return (
|
||||
<Main>
|
||||
<MainHeader>
|
||||
<MainTitle>
|
||||
<HamburgerButton onClick={() => { setMobileSidebar(false); render() }} title="Hide apps">
|
||||
<HamburgerLine />
|
||||
<HamburgerLine />
|
||||
<HamburgerLine />
|
||||
</HamburgerButton>
|
||||
<a href="/" style={{ textDecoration: 'none', color: 'inherit' }}>🐾 Toes</a>
|
||||
</MainTitle>
|
||||
</MainHeader>
|
||||
<MainContentContainer>
|
||||
<AppSelector render={render} large />
|
||||
<div style={{ padding: '12px 16px' }}>
|
||||
<NewAppButton onClick={openNewAppModal}>+ New App</NewAppButton>
|
||||
</div>
|
||||
</MainContentContainer>
|
||||
</Main>
|
||||
)
|
||||
}
|
||||
|
||||
function MainContent({ render }: { render: () => void }) {
|
||||
if (isNarrow && mobileSidebar) return <MobileSidebar render={render} />
|
||||
const selected = apps.find(a => a.name === selectedApp)
|
||||
if (selected) return <AppDetail app={selected} render={render} />
|
||||
if (currentView === 'settings') return <SettingsPage render={render} />
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { useEffect } from 'hono/jsx'
|
||||
import { openAppSelectorModal } from '../modals'
|
||||
import { navigate } from '../router'
|
||||
import { dashboardTab, isNarrow } from '../state'
|
||||
import { dashboardTab, isNarrow, setMobileSidebar } from '../state'
|
||||
import {
|
||||
AppSelectorChevron,
|
||||
HamburgerButton,
|
||||
HamburgerLine,
|
||||
DashboardContainer,
|
||||
DashboardHeader,
|
||||
DashboardTitle,
|
||||
|
|
@ -43,14 +43,20 @@ export function DashboardLanding({ render }: { render: () => void }) {
|
|||
>
|
||||
⚙️
|
||||
</SettingsGear>
|
||||
{isNarrow && (
|
||||
<HamburgerButton
|
||||
onClick={() => { setMobileSidebar(true); render() }}
|
||||
title="Show apps"
|
||||
style={{ position: 'absolute', top: 16, left: 16 }}
|
||||
>
|
||||
<HamburgerLine />
|
||||
<HamburgerLine />
|
||||
<HamburgerLine />
|
||||
</HamburgerButton>
|
||||
)}
|
||||
<DashboardHeader>
|
||||
<DashboardTitle narrow={narrow}>
|
||||
🐾 Toes
|
||||
{isNarrow && (
|
||||
<AppSelectorChevron onClick={() => openAppSelectorModal(render)}>
|
||||
▼
|
||||
</AppSelectorChevron>
|
||||
)}
|
||||
</DashboardTitle>
|
||||
</DashboardHeader>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { buildAppUrl } from '../../shared/urls'
|
||||
import { navigate } from '../router'
|
||||
import { apps } from '../state'
|
||||
import { apps, isNarrow } from '../state'
|
||||
import {
|
||||
EmptyState,
|
||||
Tile,
|
||||
|
|
@ -19,7 +19,7 @@ export function Urls({ render }: { render: () => void }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<TileGrid>
|
||||
<TileGrid narrow={isNarrow || undefined}>
|
||||
{nonTools.map(app => {
|
||||
const url = buildAppUrl(app.name, location.origin)
|
||||
const running = app.state === 'running'
|
||||
|
|
@ -36,6 +36,7 @@ export function Urls({ render }: { render: () => void }) {
|
|||
key={app.name}
|
||||
href={running ? url : appPage}
|
||||
target={running ? '_blank' : undefined}
|
||||
narrow={isNarrow || undefined}
|
||||
>
|
||||
<TileStatus state={app.state} onClick={openAppPage} />
|
||||
<TileIcon>{app.icon}</TileIcon>
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
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,4 +1,3 @@
|
|||
export { openAppSelectorModal } from './AppSelector'
|
||||
export { openDeleteAppModal } from './DeleteApp'
|
||||
export { openNewAppModal } from './NewApp'
|
||||
export { openRenameAppModal } from './RenameApp'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { setCurrentView, setDashboardTab, setSelectedApp, setSelectedTab } from './state'
|
||||
import { setCurrentView, setDashboardTab, setMobileSidebar, setSelectedApp, setSelectedTab } from './state'
|
||||
|
||||
let _render: () => void
|
||||
|
||||
|
|
@ -28,6 +28,7 @@ export function initRouter(render: () => void) {
|
|||
}
|
||||
|
||||
function route() {
|
||||
setMobileSidebar(false)
|
||||
const path = location.pathname
|
||||
|
||||
if (path.startsWith('/app/')) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export let isNarrow: boolean = window.matchMedia('(max-width: 768px)').matches
|
|||
export let selectedApp: string | null = null
|
||||
export let sidebarCollapsed: boolean = localStorage.getItem('sidebarCollapsed') === 'true'
|
||||
export let dashboardTab: DashboardTab = 'urls'
|
||||
export let mobileSidebar: boolean = false
|
||||
export let sidebarSection: 'apps' | 'tools' = (localStorage.getItem('sidebarSection') as 'apps' | 'tools') || 'apps'
|
||||
|
||||
// Server state (from SSE)
|
||||
|
|
@ -33,6 +34,10 @@ export function setIsNarrow(narrow: boolean) {
|
|||
isNarrow = narrow
|
||||
}
|
||||
|
||||
export function setMobileSidebar(open: boolean) {
|
||||
mobileSidebar = open
|
||||
}
|
||||
|
||||
export function setSidebarCollapsed(collapsed: boolean) {
|
||||
sidebarCollapsed = collapsed
|
||||
localStorage.setItem('sidebarCollapsed', String(collapsed))
|
||||
|
|
|
|||
|
|
@ -209,6 +209,9 @@ export const TileGrid = define('TileGrid', {
|
|||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))',
|
||||
gap: 20,
|
||||
variants: {
|
||||
narrow: { gridTemplateColumns: '1fr' },
|
||||
},
|
||||
})
|
||||
|
||||
export const Tile = define('Tile', {
|
||||
|
|
@ -230,6 +233,14 @@ export const Tile = define('Tile', {
|
|||
borderColor: theme('colors-textFaint'),
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
narrow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: '16px 20px',
|
||||
gap: 16,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const TileIcon = define('TileIcon', {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ export { Form, FormActions, FormCheckbox, FormCheckboxField, FormCheckboxLabel,
|
|||
export {
|
||||
AppItem,
|
||||
AppList,
|
||||
AppSelectorChevron,
|
||||
ClickableAppName,
|
||||
DashboardContainer,
|
||||
DashboardHeader,
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ export const SectionTab = define('SectionTab', {
|
|||
background: theme('colors-bgSelected'),
|
||||
color: theme('colors-text'),
|
||||
},
|
||||
large: { fontSize: 14, padding: '8px 12px' },
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -129,6 +130,7 @@ export const AppItem = define('AppItem', {
|
|||
'&:hover': { background: theme('colors-bgHover'), color: theme('colors-text') },
|
||||
},
|
||||
variants: {
|
||||
large: { fontSize: 18, padding: '12px 16px', gap: 12 },
|
||||
selected: { background: theme('colors-bgSelected'), color: theme('colors-text'), fontWeight: 500 },
|
||||
},
|
||||
})
|
||||
|
|
@ -170,20 +172,6 @@ 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'),
|
||||
|
|
@ -237,6 +225,7 @@ export const DashboardContainer = define('DashboardContainer', {
|
|||
|
||||
export const DashboardHeader = define('DashboardHeader', {
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
})
|
||||
|
||||
export const DashboardTitle = define('DashboardTitle', {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user