Add dashboard landing page with clickable logo navigation

The Toes logo now links to a system-wide dashboard view that shows
app and tool counts. This is the default view when first opening
the web app.

https://claude.ai/code/session_013L9HKHxMEoub76B1zuKive
This commit is contained in:
Claude 2026-02-05 14:41:18 +00:00
parent 14d758ef42
commit a91f400100
No known key found for this signature in database
6 changed files with 136 additions and 6 deletions

View File

@ -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 ? (
<AppDetail app={selected} render={render} />
) : (
<EmptyState>Select an app to view details</EmptyState>
<DashboardLanding />
)}
<Modal />
</Layout>

View File

@ -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 (
<DashboardContainer>
<DashboardHeader>
<DashboardTitle>🐾 Toes</DashboardTitle>
<DashboardSubtitle>Your personal web appliance</DashboardSubtitle>
</DashboardHeader>
<StatsGrid>
<StatCard>
<StatValue>{regularApps.length}</StatValue>
<StatLabel>Apps</StatLabel>
</StatCard>
<StatCard>
<StatValue>{toolApps.length}</StatValue>
<StatLabel>Tools</StatLabel>
</StatCard>
<StatCard>
<StatValue>{runningApps.length}</StatValue>
<StatLabel>Running</StatLabel>
</StatCard>
</StatsGrid>
</DashboardContainer>
)
}

View File

@ -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 (
<SidebarContainer style={sidebarCollapsed ? { width: 'auto' } : undefined}>
<Logo>
{!sidebarCollapsed && <span>🐾 Toes</span>}
{!sidebarCollapsed && (
<LogoLink onClick={goToDashboard} title="Go to dashboard">
🐾 Toes
</LogoLink>
)}
<HamburgerButton onClick={toggleSidebar} title={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}>
<HamburgerLine />
<HamburgerLine />

View File

@ -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()
}

View File

@ -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 {

View File

@ -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',
})