app tiles

This commit is contained in:
Chris Wanstrath 2026-02-25 20:33:02 -08:00
parent 363a82a845
commit 1a71656508
4 changed files with 93 additions and 71 deletions

View File

@ -63,7 +63,7 @@ export function DashboardLanding({ render }: { render: () => void }) {
</TabBar> </TabBar>
<TabContent active={dashboardTab === 'urls' || undefined}> <TabContent active={dashboardTab === 'urls' || undefined}>
<Urls /> <Urls render={render} />
</TabContent> </TabContent>
<TabContent active={dashboardTab === 'logs' || undefined}> <TabContent active={dashboardTab === 'logs' || undefined}>

View File

@ -1,16 +1,16 @@
import { buildAppUrl } from '../../shared/urls' import { buildAppUrl } from '../../shared/urls'
import { apps } from '../state' import { apps, setSelectedApp } from '../state'
import { import {
EmptyState, EmptyState,
StatusDot, Tile,
UrlLeft, TileGrid,
UrlLink, TileIcon,
UrlList, TileName,
UrlPort, TilePort,
UrlRow, TileStatus,
} from '../styles' } from '../styles'
export function Urls() { export function Urls({ render }: { render: () => void }) {
const nonTools = apps.filter(a => !a.tool) const nonTools = apps.filter(a => !a.tool)
if (nonTools.length === 0) { if (nonTools.length === 0) {
@ -18,26 +18,32 @@ export function Urls() {
} }
return ( return (
<UrlList> <TileGrid>
{nonTools.map(app => { {nonTools.map(app => {
const url = buildAppUrl(app.name, location.origin) const url = buildAppUrl(app.name, location.origin)
const running = app.state === 'running' const running = app.state === 'running'
const openAppPage = (e: MouseEvent) => {
e.preventDefault()
e.stopPropagation()
setSelectedApp(app.name)
render()
}
return ( return (
<UrlRow key={app.name}> <Tile
<UrlLeft> key={app.name}
<StatusDot state={app.state} /> href={running ? url : undefined}
<span>{app.icon}</span> target={running ? '_blank' : undefined}
{running ? ( style={running ? undefined : { cursor: 'default' }}
<UrlLink href={url} target="_blank">{url}</UrlLink> >
) : ( <TileStatus state={app.state} onClick={openAppPage} />
<span style={{ color: 'var(--colors-textFaint)' }}>{app.name}</span> <TileIcon>{app.icon}</TileIcon>
)} <TileName>{app.name}</TileName>
</UrlLeft> <TilePort>{app.port ? `:${app.port}` : '\u2014'}</TilePort>
{app.port ? <UrlPort>:{app.port}</UrlPort> : null} </Tile>
</UrlRow>
) )
})} })}
</UrlList> </TileGrid>
) )
} }

View File

@ -203,58 +203,73 @@ export const LogStatus = define('LogStatus', {
}, },
}) })
// URL List // App Tiles Grid
export const UrlLeft = define('UrlLeft', { export const TileGrid = define('TileGrid', {
display: 'flex',
alignItems: 'center',
gap: 8,
minWidth: 0,
})
export const UrlLink = define('UrlLink', {
base: 'a',
color: theme('colors-link'),
textDecoration: 'none',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
selectors: {
'&:hover': { textDecoration: 'underline' },
},
})
export const UrlList = define('UrlList', {
width: '100%', width: '100%',
minWidth: 400, maxWidth: 900,
maxWidth: 800, display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))',
gap: 20,
})
export const Tile = define('Tile', {
base: 'a',
position: 'relative',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center',
gap: 8,
padding: '28px 20px 24px',
background: theme('colors-bgElement'), background: theme('colors-bgElement'),
border: `1px solid ${theme('colors-border')}`, border: `1px solid ${theme('colors-border')}`,
borderRadius: theme('radius-md'), borderRadius: theme('radius-md'),
overflow: 'hidden', textDecoration: 'none',
}) cursor: 'pointer',
export const UrlPort = define('UrlPort', {
fontFamily: theme('fonts-mono'),
fontSize: 12,
color: theme('colors-textFaint'),
flexShrink: 0,
})
export const UrlRow = define('UrlRow', {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '10px 16px',
fontFamily: theme('fonts-mono'),
fontSize: 13,
selectors: { selectors: {
'&:hover': { '&:hover': {
background: theme('colors-bgHover'), background: theme('colors-bgHover'),
}, borderColor: theme('colors-textFaint'),
'&:not(:last-child)': { },
borderBottom: `1px solid ${theme('colors-border')}`, },
})
export const TileIcon = define('TileIcon', {
fontSize: 48,
lineHeight: 1,
userSelect: 'none',
})
export const TileName = define('TileName', {
fontSize: 15,
fontWeight: 600,
color: theme('colors-text'),
textAlign: 'center',
})
export const TilePort = define('TilePort', {
fontFamily: theme('fonts-mono'),
fontSize: 13,
color: theme('colors-textFaint'),
})
export const TileStatus = define('TileStatus', {
position: 'absolute',
top: 8,
right: 8,
width: 2,
height: 2,
borderRadius: '50%',
cursor: 'pointer',
padding: 4,
backgroundClip: 'content-box',
variants: {
state: {
error: { background: theme('colors-statusInvalid') },
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') },
}, },
}, },
}) })

View File

@ -15,11 +15,12 @@ export {
LogStatus, LogStatus,
LogText, LogText,
LogTimestamp, LogTimestamp,
UrlLeft, Tile,
UrlLink, TileGrid,
UrlList, TileIcon,
UrlPort, TileName,
UrlRow, TilePort,
TileStatus,
VitalCard, VitalCard,
VitalLabel, VitalLabel,
VitalsSection, VitalsSection,