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 { Styles } from '@because/forge'
|
||||||
import { Modal } from './modal'
|
|
||||||
import { apps, isNarrow, selectedApp } from '../state'
|
import { apps, isNarrow, selectedApp } from '../state'
|
||||||
import { EmptyState, Layout } from '../styles'
|
import { Layout } from '../styles'
|
||||||
import { AppDetail } from './AppDetail'
|
import { AppDetail } from './AppDetail'
|
||||||
|
import { DashboardLanding } from './DashboardLanding'
|
||||||
|
import { Modal } from './modal'
|
||||||
import { Sidebar } from './Sidebar'
|
import { Sidebar } from './Sidebar'
|
||||||
|
|
||||||
export function Dashboard({ render }: { render: () => void }) {
|
export function Dashboard({ render }: { render: () => void }) {
|
||||||
|
|
@ -15,7 +16,7 @@ export function Dashboard({ render }: { render: () => void }) {
|
||||||
{selected ? (
|
{selected ? (
|
||||||
<AppDetail app={selected} render={render} />
|
<AppDetail app={selected} render={render} />
|
||||||
) : (
|
) : (
|
||||||
<EmptyState>Select an app to view details</EmptyState>
|
<DashboardLanding />
|
||||||
)}
|
)}
|
||||||
<Modal />
|
<Modal />
|
||||||
</Layout>
|
</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 { openNewAppModal } from '../modals'
|
||||||
import {
|
import {
|
||||||
|
setSelectedApp,
|
||||||
setSidebarCollapsed,
|
setSidebarCollapsed,
|
||||||
sidebarCollapsed,
|
sidebarCollapsed,
|
||||||
} from '../state'
|
} from '../state'
|
||||||
|
|
@ -7,6 +8,7 @@ import {
|
||||||
HamburgerButton,
|
HamburgerButton,
|
||||||
HamburgerLine,
|
HamburgerLine,
|
||||||
Logo,
|
Logo,
|
||||||
|
LogoLink,
|
||||||
NewAppButton,
|
NewAppButton,
|
||||||
Sidebar as SidebarContainer,
|
Sidebar as SidebarContainer,
|
||||||
SidebarFooter,
|
SidebarFooter,
|
||||||
|
|
@ -14,6 +16,11 @@ import {
|
||||||
import { AppSelector } from './AppSelector'
|
import { AppSelector } from './AppSelector'
|
||||||
|
|
||||||
export function Sidebar({ render }: { render: () => void }) {
|
export function Sidebar({ render }: { render: () => void }) {
|
||||||
|
const goToDashboard = () => {
|
||||||
|
setSelectedApp(null)
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
setSidebarCollapsed(!sidebarCollapsed)
|
setSidebarCollapsed(!sidebarCollapsed)
|
||||||
render()
|
render()
|
||||||
|
|
@ -22,7 +29,11 @@ export function Sidebar({ render }: { render: () => void }) {
|
||||||
return (
|
return (
|
||||||
<SidebarContainer style={sidebarCollapsed ? { width: 'auto' } : undefined}>
|
<SidebarContainer style={sidebarCollapsed ? { width: 'auto' } : undefined}>
|
||||||
<Logo>
|
<Logo>
|
||||||
{!sidebarCollapsed && <span>🐾 Toes</span>}
|
{!sidebarCollapsed && (
|
||||||
|
<LogoLink onClick={goToDashboard} title="Go to dashboard">
|
||||||
|
🐾 Toes
|
||||||
|
</LogoLink>
|
||||||
|
)}
|
||||||
<HamburgerButton onClick={toggleSidebar} title={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}>
|
<HamburgerButton onClick={toggleSidebar} title={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}>
|
||||||
<HamburgerLine />
|
<HamburgerLine />
|
||||||
<HamburgerLine />
|
<HamburgerLine />
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,9 @@ narrowQuery.addEventListener('change', e => {
|
||||||
const events = new EventSource('/api/apps/stream')
|
const events = new EventSource('/api/apps/stream')
|
||||||
events.onmessage = e => {
|
events.onmessage = e => {
|
||||||
setApps(JSON.parse(e.data))
|
setApps(JSON.parse(e.data))
|
||||||
const valid = selectedApp && apps.some(a => a.name === selectedApp)
|
// If selected app no longer exists, clear selection to show dashboard
|
||||||
if (!valid && apps.length) setSelectedApp(apps[0]!.name)
|
if (selectedApp && !apps.some(a => a.name === selectedApp)) {
|
||||||
|
setSelectedApp(null)
|
||||||
|
}
|
||||||
render()
|
render()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,16 @@ export {
|
||||||
AppList,
|
AppList,
|
||||||
AppSelectorChevron,
|
AppSelectorChevron,
|
||||||
ClickableAppName,
|
ClickableAppName,
|
||||||
|
DashboardContainer,
|
||||||
|
DashboardHeader,
|
||||||
|
DashboardSubtitle,
|
||||||
|
DashboardTitle,
|
||||||
HamburgerButton,
|
HamburgerButton,
|
||||||
HamburgerLine,
|
HamburgerLine,
|
||||||
HeaderActions,
|
HeaderActions,
|
||||||
Layout,
|
Layout,
|
||||||
Logo,
|
Logo,
|
||||||
|
LogoLink,
|
||||||
Main,
|
Main,
|
||||||
MainContent,
|
MainContent,
|
||||||
MainHeader,
|
MainHeader,
|
||||||
|
|
@ -19,6 +24,10 @@ export {
|
||||||
SectionTab,
|
SectionTab,
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarFooter,
|
SidebarFooter,
|
||||||
|
StatCard,
|
||||||
|
StatLabel,
|
||||||
|
StatsGrid,
|
||||||
|
StatValue,
|
||||||
} from './layout'
|
} from './layout'
|
||||||
export { LogLine, LogsContainer, LogTime } from './logs.tsx'
|
export { LogLine, LogsContainer, LogTime } from './logs.tsx'
|
||||||
export {
|
export {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,18 @@ export const Logo = define('Logo', {
|
||||||
borderBottom: `1px solid ${theme('colors-border')}`,
|
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', {
|
export const HamburgerButton = define('HamburgerButton', {
|
||||||
base: 'button',
|
base: 'button',
|
||||||
background: 'none',
|
background: 'none',
|
||||||
|
|
@ -183,3 +195,58 @@ export const MainContent = define('MainContent', {
|
||||||
padding: '10px 24px',
|
padding: '10px 24px',
|
||||||
overflow: 'auto',
|
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