import { render as renderApp } from 'hono/jsx/dom' import { define, Styles } from 'forge' import type { App, AppState } from '../shared/types' import { theme } from './themes' // UI state (survives re-renders) let selectedApp: string | null = localStorage.getItem('selectedApp') let sidebarCollapsed: boolean = localStorage.getItem('sidebarCollapsed') === 'true' // Server state (from SSE) let apps: App[] = [] // Layout const Layout = define('Layout', { display: 'flex', height: '100vh', fontFamily: theme('fonts-sans'), background: theme('colors-bg'), color: theme('colors-text'), }) const Sidebar = define('Sidebar', { width: 220, borderRight: `1px solid ${theme('colors-border')}`, display: 'flex', flexDirection: 'column', flexShrink: 0, }) const Logo = define('Logo', { height: 64, display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 16px', fontSize: 20, fontWeight: 'bold', 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', { padding: '16px 16px 8px', fontSize: 12, fontWeight: 600, color: theme('colors-textFaint'), textTransform: 'uppercase', letterSpacing: '0.05em', }) const AppList = define('AppList', { flex: 1, overflow: 'auto', }) const AppItem = define('AppItem', { display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8, padding: '8px 16px', color: theme('colors-textMuted'), textDecoration: 'none', fontSize: 14, cursor: 'pointer', selectors: { '&:hover': { background: theme('colors-bgHover'), color: theme('colors-text') }, }, variants: { selected: { background: theme('colors-bgSelected'), color: theme('colors-text'), fontWeight: 500 }, }, }) const StatusDot = define('StatusDot', { width: 8, height: 8, borderRadius: '50%', flexShrink: 0, variants: { state: { invalid: { background: theme('colors-statusInvalid') }, stopped: { background: theme('colors-statusStopped') }, starting: { background: theme('colors-statusStarting') }, running: { background: theme('colors-statusRunning') }, stopping: { background: theme('colors-statusStarting') }, }, inline: { display: 'inline' } }, }) const SidebarFooter = define('SidebarFooter', { padding: 16, borderTop: `1px solid ${theme('colors-border')}`, }) const NewAppButton = define('NewAppButton', { display: 'block', padding: '8px 12px', background: theme('colors-bgElement'), border: `1px solid ${theme('colors-border')}`, borderRadius: theme('radius-md'), color: theme('colors-textMuted'), textDecoration: 'none', fontSize: 14, textAlign: 'center', cursor: 'pointer', selectors: { '&:hover': { background: theme('colors-bgHover'), color: theme('colors-text') }, }, }) // Main pane const Main = define('Main', { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', }) const MainHeader = define('MainHeader', { height: 64, display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 24px', borderBottom: `1px solid ${theme('colors-border')}`, }) const MainTitle = define('MainTitle', { display: 'flex', alignItems: 'center', gap: 8, fontSize: 20, fontWeight: 600, margin: 0, }) const HeaderActions = define('HeaderActions', { display: 'flex', gap: 8, }) const MainContent = define('MainContent', { flex: 1, padding: '10px 24px', overflow: 'auto', }) const Section = define('Section', { marginBottom: 32, }) const SectionTitle = define('SectionTitle', { fontSize: 12, fontWeight: 600, color: theme('colors-textFaint'), textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 12, paddingBottom: 8, borderBottom: `1px solid ${theme('colors-border')}`, }) const InfoRow = define('InfoRow', { display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12, fontSize: 14, }) const InfoLabel = define('InfoLabel', { color: theme('colors-textFaint'), width: 80, flexShrink: 0, }) const InfoValue = define('InfoValue', { color: theme('colors-text'), display: 'flex', alignItems: 'center', gap: 8, }) const Link = define('Link', { base: 'a', color: theme('colors-link'), textDecoration: 'none', selectors: { '&:hover': { textDecoration: 'underline' }, }, }) const Button = define('Button', { base: 'button', padding: '6px 12px', background: theme('colors-bgElement'), border: `1px solid ${theme('colors-border')}`, borderRadius: theme('radius-md'), color: theme('colors-text'), fontSize: 13, cursor: 'pointer', selectors: { '&:hover': { background: theme('colors-bgHover') }, }, variants: { variant: { danger: { borderColor: theme('colors-dangerBorder'), color: theme('colors-dangerText') }, primary: { background: theme('colors-primary'), borderColor: theme('colors-primary'), color: theme('colors-primaryText') }, }, }, }) const ActionBar = define('ActionBar', { display: 'flex', gap: 8, marginTop: 24, paddingTop: 24, borderTop: `1px solid ${theme('colors-border')}`, }) const EmptyState = define('EmptyState', { display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: theme('colors-textFaint'), fontSize: 14, }) const LogsContainer = define('LogsContainer', { background: theme('colors-bgSubtle'), borderRadius: theme('radius-md'), padding: 12, fontFamily: theme('fonts-mono'), fontSize: 12, color: theme('colors-textMuted'), maxHeight: 200, overflow: 'auto', }) const LogLine = define('LogLine', { marginBottom: 4, selectors: { '&:last-child': { marginBottom: 0 }, }, }) const LogTime = define('LogTime', { color: theme('colors-textFaintest'), 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 ( ) } }) function setSelectedTab(tab: 'overview' | 'todo') { selectedTab = tab render() } const Nav = () => { return <> setSelectedTab('overview')}>Overview setSelectedTab('todo')}>TODO } const stateLabels: Record = { invalid: 'Invalid', stopped: 'Stopped', starting: 'Starting', running: 'Running', stopping: 'Stopping', } // Actions - call API then let SSE update the state const startApp = (name: string) => fetch(`/api/apps/${name}/start`, { method: 'POST' }) const stopApp = (name: string) => fetch(`/api/apps/${name}/stop`, { method: 'POST' }) const restartApp = (name: string) => fetch(`/api/apps/${name}/restart`, { method: 'POST' }) const selectApp = (name: string) => { selectedApp = name localStorage.setItem('selectedApp', name) render() } const toggleSidebar = () => { sidebarCollapsed = !sidebarCollapsed localStorage.setItem('sidebarCollapsed', String(sidebarCollapsed)) render() } const AppDetail = ({ app }: { app: App }) => ( <> {app.icon ?? }   {app.name}