diff --git a/src/client/components/AppDetail.tsx b/src/client/components/AppDetail.tsx
index a9be58a..84b7442 100644
--- a/src/client/components/AppDetail.tsx
+++ b/src/client/components/AppDetail.tsx
@@ -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 }) {
{app.icon}
openRenameAppModal(app)}>{app.name}
+ {isNarrow && (
+ openAppSelectorModal(render)}>
+ ▼
+
+ )}
diff --git a/src/client/components/Dashboard.tsx b/src/client/components/Dashboard.tsx
index 356ff74..4c436be 100644
--- a/src/client/components/Dashboard.tsx
+++ b/src/client/components/Dashboard.tsx
@@ -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 (
-
+ {!isNarrow && }
{selected ? (
) : (
diff --git a/src/client/components/Sidebar.tsx b/src/client/components/Sidebar.tsx
index d9dd3f4..c3987e7 100644
--- a/src/client/components/Sidebar.tsx
+++ b/src/client/components/Sidebar.tsx
@@ -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 (
@@ -52,37 +29,7 @@ export function Sidebar({ render }: { render: () => void }) {
- {!sidebarCollapsed && toolApps.length > 0 && (
-
- switchSection('apps')}>
- Apps
-
- switchSection('tools')}>
- Tools
-
-
- )}
-
- {activeApps.map(app => (
- selectApp(app.name)}
- selected={app.name === selectedApp ? true : undefined}
- style={sidebarCollapsed ? { justifyContent: 'center', padding: '10px 12px' } : undefined}
- title={sidebarCollapsed ? app.name : undefined}
- >
- {sidebarCollapsed ? (
- {app.icon}
- ) : (
- <>
- {app.icon}
- {app.name}
-
- >
- )}
-
- ))}
-
+
{!sidebarCollapsed && (
+ New App
diff --git a/src/client/components/index.ts b/src/client/components/index.ts
index 318742c..c6f1f1c 100644
--- a/src/client/components/index.ts
+++ b/src/client/components/index.ts
@@ -1,4 +1,5 @@
export { AppDetail } from './AppDetail'
+export { AppSelector } from './AppSelector'
export { Dashboard } from './Dashboard'
export { Nav } from './Nav'
export { Sidebar } from './Sidebar'
diff --git a/src/client/components/modal.tsx b/src/client/components/modal.tsx
index 4629cd8..d513285 100644
--- a/src/client/components/modal.tsx
+++ b/src/client/components/modal.tsx
@@ -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,
})
diff --git a/src/client/index.tsx b/src/client/index.tsx
index cdc8fc0..33236b5 100644
--- a/src/client/index.tsx
+++ b/src/client/index.tsx
@@ -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 => {
diff --git a/src/client/modals/AppSelector.tsx b/src/client/modals/AppSelector.tsx
new file mode 100644
index 0000000..53917d9
--- /dev/null
+++ b/src/client/modals/AppSelector.tsx
@@ -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', () => (
+
+ ))
+}
diff --git a/src/client/modals/index.ts b/src/client/modals/index.ts
index 16d10cd..48017df 100644
--- a/src/client/modals/index.ts
+++ b/src/client/modals/index.ts
@@ -1,3 +1,4 @@
+export { openAppSelectorModal } from './AppSelector'
export { openDeleteAppModal } from './DeleteApp'
export { openNewAppModal } from './NewApp'
export { openRenameAppModal } from './RenameApp'
diff --git a/src/client/state.ts b/src/client/state.ts
index a62dd31..bc59146 100644
--- a/src/client/state.ts
+++ b/src/client/state.ts
@@ -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))
diff --git a/src/client/styles/index.ts b/src/client/styles/index.ts
index eb8b995..42a6847 100644
--- a/src/client/styles/index.ts
+++ b/src/client/styles/index.ts
@@ -3,6 +3,7 @@ export { Form, FormActions, FormCheckbox, FormCheckboxField, FormCheckboxLabel,
export {
AppItem,
AppList,
+ AppSelectorChevron,
ClickableAppName,
HamburgerButton,
HamburgerLine,
diff --git a/src/client/styles/layout.ts b/src/client/styles/layout.ts
index 347188d..593e65b 100644
--- a/src/client/styles/layout.ts
+++ b/src/client/styles/layout.ts
@@ -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'),