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