diff --git a/src/client/components/Dashboard.tsx b/src/client/components/Dashboard.tsx
index 4c436be..427179e 100644
--- a/src/client/components/Dashboard.tsx
+++ b/src/client/components/Dashboard.tsx
@@ -1,8 +1,9 @@
import { Styles } from '@because/forge'
-import { Modal } from './modal'
import { apps, isNarrow, selectedApp } from '../state'
-import { EmptyState, Layout } from '../styles'
+import { Layout } from '../styles'
import { AppDetail } from './AppDetail'
+import { DashboardLanding } from './DashboardLanding'
+import { Modal } from './modal'
import { Sidebar } from './Sidebar'
export function Dashboard({ render }: { render: () => void }) {
@@ -15,7 +16,7 @@ export function Dashboard({ render }: { render: () => void }) {
{selected ? (
) : (
- Select an app to view details
+
)}
diff --git a/src/client/components/DashboardLanding.tsx b/src/client/components/DashboardLanding.tsx
new file mode 100644
index 0000000..183d3d2
--- /dev/null
+++ b/src/client/components/DashboardLanding.tsx
@@ -0,0 +1,40 @@
+import { apps } from '../state'
+import {
+ DashboardContainer,
+ DashboardHeader,
+ DashboardSubtitle,
+ DashboardTitle,
+ StatCard,
+ StatLabel,
+ StatValue,
+ StatsGrid,
+} from '../styles'
+
+export function DashboardLanding() {
+ const regularApps = apps.filter(app => !app.tool)
+ const toolApps = apps.filter(app => app.tool)
+ const runningApps = apps.filter(app => app.state === 'running')
+
+ return (
+
+
+ 🐾 Toes
+ Your personal web appliance
+
+
+
+ {regularApps.length}
+ Apps
+
+
+ {toolApps.length}
+ Tools
+
+
+ {runningApps.length}
+ Running
+
+
+
+ )
+}
diff --git a/src/client/components/Sidebar.tsx b/src/client/components/Sidebar.tsx
index c3987e7..ba586cc 100644
--- a/src/client/components/Sidebar.tsx
+++ b/src/client/components/Sidebar.tsx
@@ -1,5 +1,6 @@
import { openNewAppModal } from '../modals'
import {
+ setSelectedApp,
setSidebarCollapsed,
sidebarCollapsed,
} from '../state'
@@ -7,6 +8,7 @@ import {
HamburgerButton,
HamburgerLine,
Logo,
+ LogoLink,
NewAppButton,
Sidebar as SidebarContainer,
SidebarFooter,
@@ -14,6 +16,11 @@ import {
import { AppSelector } from './AppSelector'
export function Sidebar({ render }: { render: () => void }) {
+ const goToDashboard = () => {
+ setSelectedApp(null)
+ render()
+ }
+
const toggleSidebar = () => {
setSidebarCollapsed(!sidebarCollapsed)
render()
@@ -22,7 +29,11 @@ export function Sidebar({ render }: { render: () => void }) {
return (
- {!sidebarCollapsed && 🐾 Toes}
+ {!sidebarCollapsed && (
+
+ 🐾 Toes
+
+ )}
diff --git a/src/client/index.tsx b/src/client/index.tsx
index 33236b5..0495d55 100644
--- a/src/client/index.tsx
+++ b/src/client/index.tsx
@@ -45,7 +45,9 @@ narrowQuery.addEventListener('change', e => {
const events = new EventSource('/api/apps/stream')
events.onmessage = e => {
setApps(JSON.parse(e.data))
- const valid = selectedApp && apps.some(a => a.name === selectedApp)
- if (!valid && apps.length) setSelectedApp(apps[0]!.name)
+ // If selected app no longer exists, clear selection to show dashboard
+ if (selectedApp && !apps.some(a => a.name === selectedApp)) {
+ setSelectedApp(null)
+ }
render()
}
diff --git a/src/client/styles/index.ts b/src/client/styles/index.ts
index 42a6847..6be7e61 100644
--- a/src/client/styles/index.ts
+++ b/src/client/styles/index.ts
@@ -5,11 +5,16 @@ export {
AppList,
AppSelectorChevron,
ClickableAppName,
+ DashboardContainer,
+ DashboardHeader,
+ DashboardSubtitle,
+ DashboardTitle,
HamburgerButton,
HamburgerLine,
HeaderActions,
Layout,
Logo,
+ LogoLink,
Main,
MainContent,
MainHeader,
@@ -19,6 +24,10 @@ export {
SectionTab,
Sidebar,
SidebarFooter,
+ StatCard,
+ StatLabel,
+ StatsGrid,
+ StatValue,
} from './layout'
export { LogLine, LogsContainer, LogTime } from './logs.tsx'
export {
diff --git a/src/client/styles/layout.ts b/src/client/styles/layout.ts
index 593e65b..c8d93f2 100644
--- a/src/client/styles/layout.ts
+++ b/src/client/styles/layout.ts
@@ -28,6 +28,18 @@ export const Logo = define('Logo', {
borderBottom: `1px solid ${theme('colors-border')}`,
})
+export const LogoLink = define('LogoLink', {
+ cursor: 'pointer',
+ borderRadius: theme('radius-md'),
+ padding: '4px 8px',
+ margin: '-4px -8px',
+ selectors: {
+ '&:hover': {
+ background: theme('colors-bgHover'),
+ },
+ },
+})
+
export const HamburgerButton = define('HamburgerButton', {
base: 'button',
background: 'none',
@@ -183,3 +195,58 @@ export const MainContent = define('MainContent', {
padding: '10px 24px',
overflow: 'auto',
})
+
+export const DashboardContainer = define('DashboardContainer', {
+ flex: 1,
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 40,
+ gap: 40,
+})
+
+export const DashboardHeader = define('DashboardHeader', {
+ textAlign: 'center',
+})
+
+export const DashboardTitle = define('DashboardTitle', {
+ fontSize: 48,
+ fontWeight: 'bold',
+ margin: 0,
+ marginBottom: 8,
+})
+
+export const DashboardSubtitle = define('DashboardSubtitle', {
+ fontSize: 16,
+ color: theme('colors-textMuted'),
+ margin: 0,
+})
+
+export const StatsGrid = define('StatsGrid', {
+ display: 'flex',
+ gap: 24,
+})
+
+export const StatCard = define('StatCard', {
+ background: theme('colors-bgElement'),
+ border: `1px solid ${theme('colors-border')}`,
+ borderRadius: theme('radius-md'),
+ padding: '24px 40px',
+ textAlign: 'center',
+ minWidth: 120,
+})
+
+export const StatValue = define('StatValue', {
+ fontSize: 36,
+ fontWeight: 'bold',
+ color: theme('colors-text'),
+ marginBottom: 4,
+})
+
+export const StatLabel = define('StatLabel', {
+ fontSize: 14,
+ color: theme('colors-textMuted'),
+ textTransform: 'uppercase',
+ letterSpacing: '0.05em',
+})