Compare commits

...

3 Commits

Author SHA1 Message Date
e2ff53e6d8 boigas 2026-01-27 23:23:26 -08:00
dd25203f66 go webs go 2026-01-27 23:00:44 -08:00
79b64d80b5 j'emoji 2026-01-27 22:56:01 -08:00
3 changed files with 163 additions and 79 deletions

View File

@ -27,7 +27,7 @@
"hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="], "hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
"hype": ["hype@git+https://git.nose.space/defunkt/hype#a8d3a8203e145df7a222ea409588c2ea3a1ee4e6", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "a8d3a8203e145df7a222ea409588c2ea3a1ee4e6"], "hype": ["hype@git+https://git.nose.space/defunkt/hype#1a8d4228347a47b696b4c6918179cda94fc1b0cd", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "1a8d4228347a47b696b4c6918179cda94fc1b0cd"],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],

View File

@ -5,6 +5,7 @@ import { theme } from './themes'
// UI state (survives re-renders) // UI state (survives re-renders)
let selectedApp: string | null = localStorage.getItem('selectedApp') let selectedApp: string | null = localStorage.getItem('selectedApp')
let sidebarCollapsed: boolean = localStorage.getItem('sidebarCollapsed') === 'true'
// Server state (from SSE) // Server state (from SSE)
let apps: App[] = [] let apps: App[] = []
@ -30,12 +31,36 @@ const Logo = define('Logo', {
height: 64, height: 64,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between',
padding: '0 16px', padding: '0 16px',
fontSize: 20, fontSize: 20,
fontWeight: 'bold', fontWeight: 'bold',
borderBottom: `1px solid ${theme('colors-border')}`, borderBottom: `1px solid ${theme('colors-border')}`,
}) })
const HamburgerButton = define('HamburgerButton', {
base: 'button',
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 4,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
gap: 4,
selectors: {
'&:hover span': { background: theme('colors-text') },
},
})
const HamburgerLine = define('HamburgerLine', {
width: 18,
height: 2,
background: theme('colors-textMuted'),
borderRadius: 1,
transition: 'background 0.15s',
})
const SectionLabel = define('SectionLabel', { const SectionLabel = define('SectionLabel', {
padding: '16px 16px 8px', padding: '16px 16px 8px',
fontSize: 12, fontSize: 12,
@ -141,7 +166,7 @@ const HeaderActions = define('HeaderActions', {
const MainContent = define('MainContent', { const MainContent = define('MainContent', {
flex: 1, flex: 1,
padding: 24, padding: '10px 24px',
overflow: 'auto', overflow: 'auto',
}) })
@ -248,8 +273,43 @@ const LogLine = define('LogLine', {
const LogTime = define('LogTime', { const LogTime = define('LogTime', {
color: theme('colors-textFaintest'), color: theme('colors-textFaintest'),
marginRight: 12, marginRight: 12,
display: 'inline',
}) })
let selectedTab: 'overview' | 'todo' = 'overview'
const TabContent = define('TabContent', {
display: 'none',
variants: {
active: {
display: 'block'
}
}
})
const NavButton = define('NavButton', {
render({ props }) {
return (
<Button onClick={props.onClick} style={{ marginRight: 10, marginBottom: 20 }}>
{props.children}
</Button>
)
}
})
function setSelectedTab(tab: 'overview' | 'todo') {
selectedTab = tab
render()
}
const Nav = () => {
return <>
<NavButton onClick={() => setSelectedTab('overview')}>Overview</NavButton>
<NavButton onClick={() => setSelectedTab('todo')}>TODO</NavButton>
</>
}
const stateLabels: Record<AppState, string> = { const stateLabels: Record<AppState, string> = {
invalid: 'Invalid', invalid: 'Invalid',
stopped: 'Stopped', stopped: 'Stopped',
@ -269,13 +329,17 @@ const selectApp = (name: string) => {
render() render()
} }
const toggleSidebar = () => {
sidebarCollapsed = !sidebarCollapsed
localStorage.setItem('sidebarCollapsed', String(sidebarCollapsed))
render()
}
const AppDetail = ({ app }: { app: App }) => ( const AppDetail = ({ app }: { app: App }) => (
<> <>
<MainHeader> <MainHeader>
<MainTitle> <MainTitle>
{app.state === 'running' && app.icon ? <>{app.icon}</> : ( {app.icon ?? <StatusDot state={app.state} />}
<StatusDot state={app.state} />
)}
&nbsp; &nbsp;
{app.name} {app.name}
</MainTitle> </MainTitle>
@ -285,6 +349,9 @@ const AppDetail = ({ app }: { app: App }) => (
</HeaderActions> </HeaderActions>
</MainHeader> </MainHeader>
<MainContent> <MainContent>
<Nav />
<TabContent active={selectedTab === 'overview'}>
<Section> <Section>
<SectionTitle>Status</SectionTitle> <SectionTitle>Status</SectionTitle>
<InfoRow> <InfoRow>
@ -358,6 +425,11 @@ const AppDetail = ({ app }: { app: App }) => (
<Button disabled>{stateLabels[app.state]}...</Button> <Button disabled>{stateLabels[app.state]}...</Button>
)} )}
</ActionBar> </ActionBar>
</TabContent>
<TabContent active={selectedTab === 'todo'}>
<h1>hardy har har</h1>
</TabContent>
</MainContent> </MainContent>
</> </>
) )
@ -368,28 +440,39 @@ const Dashboard = () => {
return ( return (
<Layout> <Layout>
<Styles /> <Styles />
<Sidebar> <Sidebar style={sidebarCollapsed ? { width: 'auto' } : undefined}>
<Logo>🐾 Toes</Logo> <Logo>
<SectionLabel>Apps</SectionLabel> {!sidebarCollapsed && <span>🐾 Toes</span>}
<HamburgerButton onClick={toggleSidebar} title={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}>
<HamburgerLine />
<HamburgerLine />
<HamburgerLine />
</HamburgerButton>
</Logo>
{!sidebarCollapsed && <SectionLabel>Apps</SectionLabel>}
<AppList> <AppList>
{apps.map(app => ( {apps.map(app => (
<AppItem <AppItem
key={app.name} key={app.name}
onClick={() => selectApp(app.name)} onClick={() => selectApp(app.name)}
selected={app.name === selectedApp ? true : undefined} selected={app.name === selectedApp ? true : undefined}
style={sidebarCollapsed ? { justifyContent: 'center', padding: '10px 12px' } : undefined}
title={sidebarCollapsed ? app.name : undefined}
> >
{app.state === 'running' && app.icon ? ( {app.state === 'running' && app.icon ? (
<span style={{ fontSize: 14 }}>{app.icon}</span> <span style={{ fontSize: sidebarCollapsed ? 18 : 14 }}>{app.icon}</span>
) : ( ) : (
<StatusDot state={app.state} /> <StatusDot state={app.state} />
)} )}
{app.name} {!sidebarCollapsed && app.name}
</AppItem> </AppItem>
))} ))}
</AppList> </AppList>
{!sidebarCollapsed && (
<SidebarFooter> <SidebarFooter>
<NewAppButton>+ New App</NewAppButton> <NewAppButton>+ New App</NewAppButton>
</SidebarFooter> </SidebarFooter>
)}
</Sidebar> </Sidebar>
<Main> <Main>
{selected ? ( {selected ? (

View File

@ -196,6 +196,7 @@ export const stopApp = (dir: string) => {
const watchAppsDir = () => { const watchAppsDir = () => {
watch(APPS_DIR, { recursive: true }, (_event, filename) => { watch(APPS_DIR, { recursive: true }, (_event, filename) => {
if (!filename) return if (!filename) return
if (!filename.includes('/')) return
// Extract the app directory name from the path (e.g., "myapp/package.json" -> "myapp") // Extract the app directory name from the path (e.g., "myapp/package.json" -> "myapp")
const dir = filename.split('/')[0]! const dir = filename.split('/')[0]!