diff --git a/src/client/components/AppDetail.tsx b/src/client/components/AppDetail.tsx index 7265797..811ae64 100644 --- a/src/client/components/AppDetail.tsx +++ b/src/client/components/AppDetail.tsx @@ -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 }) {
- {app.icon} -   - openRenameAppModal(app)}>{app.name} {isNarrow && ( - openAppSelectorModal(render)}> - ▼ - + { setMobileSidebar(true); render() }} title="Show apps"> + + + + )} + {app.icon} + openRenameAppModal(app)}>{app.name} {!app.tool && ( diff --git a/src/client/components/AppSelector.tsx b/src/client/components/AppSelector.tsx index 9bf025e..f2c17cf 100644 --- a/src/client/components/AppSelector.tsx +++ b/src/client/components/AppSelector.tsx @@ -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 && ( - switchSection('apps')}> + switchSection('apps')}> Apps - switchSection('tools')}> + switchSection('tools')}> Tools @@ -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 {app.icon} ) : ( <> - {app.icon} + {app.icon} {app.name} diff --git a/src/client/components/Dashboard.tsx b/src/client/components/Dashboard.tsx index c88e014..72349f7 100644 --- a/src/client/components/Dashboard.tsx +++ b/src/client/components/Dashboard.tsx @@ -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 ( +
+ + + { setMobileSidebar(false); render() }} title="Hide apps"> + + + + + 🐾 Toes + + + + +
+ + New App +
+
+
+ ) +} + function MainContent({ render }: { render: () => void }) { + if (isNarrow && mobileSidebar) return const selected = apps.find(a => a.name === selectedApp) if (selected) return if (currentView === 'settings') return diff --git a/src/client/components/DashboardLanding.tsx b/src/client/components/DashboardLanding.tsx index 758f2d3..ca5fbf3 100644 --- a/src/client/components/DashboardLanding.tsx +++ b/src/client/components/DashboardLanding.tsx @@ -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 }) { > ⚙️ + {isNarrow && ( + { setMobileSidebar(true); render() }} + title="Show apps" + style={{ position: 'absolute', top: 16, left: 16 }} + > + + + + + )} 🐾 Toes - {isNarrow && ( - openAppSelectorModal(render)}> - ▼ - - )} diff --git a/src/client/components/Urls.tsx b/src/client/components/Urls.tsx index 4a3f276..f67e67f 100644 --- a/src/client/components/Urls.tsx +++ b/src/client/components/Urls.tsx @@ -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 ( - + {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} > {app.icon} diff --git a/src/client/modals/AppSelector.tsx b/src/client/modals/AppSelector.tsx deleted file mode 100644 index 53917d9..0000000 --- a/src/client/modals/AppSelector.tsx +++ /dev/null @@ -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', () => ( - - )) -} diff --git a/src/client/modals/index.ts b/src/client/modals/index.ts index 48017df..16d10cd 100644 --- a/src/client/modals/index.ts +++ b/src/client/modals/index.ts @@ -1,4 +1,3 @@ -export { openAppSelectorModal } from './AppSelector' export { openDeleteAppModal } from './DeleteApp' export { openNewAppModal } from './NewApp' export { openRenameAppModal } from './RenameApp' diff --git a/src/client/router.ts b/src/client/router.ts index 5bedb2f..417fd4a 100644 --- a/src/client/router.ts +++ b/src/client/router.ts @@ -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/')) { diff --git a/src/client/state.ts b/src/client/state.ts index a1501f6..3cfae74 100644 --- a/src/client/state.ts +++ b/src/client/state.ts @@ -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)) diff --git a/src/client/styles/dashboard.ts b/src/client/styles/dashboard.ts index 146aeb0..5839c1b 100644 --- a/src/client/styles/dashboard.ts +++ b/src/client/styles/dashboard.ts @@ -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', { diff --git a/src/client/styles/index.ts b/src/client/styles/index.ts index f9223fa..2d14a7d 100644 --- a/src/client/styles/index.ts +++ b/src/client/styles/index.ts @@ -29,7 +29,6 @@ export { Form, FormActions, FormCheckbox, FormCheckboxField, FormCheckboxLabel, export { AppItem, AppList, - AppSelectorChevron, ClickableAppName, DashboardContainer, DashboardHeader, diff --git a/src/client/styles/layout.ts b/src/client/styles/layout.ts index 9ab7b01..7b4de20 100644 --- a/src/client/styles/layout.ts +++ b/src/client/styles/layout.ts @@ -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', {