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:
parent
14d758ef42
commit
a91f400100
|
|
@ -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>
|
||||
|
|
|
|||
40
src/client/components/DashboardLanding.tsx
Normal file
40
src/client/components/DashboardLanding.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user