forge/examples/spa/app.tsx
Chris Wanstrath 855112ac82 themes
2025-12-29 14:18:30 -08:00

268 lines
6.3 KiB
TypeScript

import { define } from '../../src'
import { theme } from '../ssr/themes'
import { ButtonExamplesContent } from '../button'
import { ProfileExamplesContent } from '../profile'
import { NavigationExamplesContent } from '../navigation'
import { FormExamplesContent } from '../form'
// ThemePicker component
const ThemePicker = define('SpaThemePicker', {
marginLeft: 'auto',
parts: {
Select: {
base: 'select',
padding: `${theme('spacing-xs')} ${theme('spacing-md')}`,
background: theme('colors-bgElevated'),
border: `1px solid ${theme('colors-border')}`,
borderRadius: theme('radius-sm'),
color: theme('colors-fg'),
fontSize: 14,
cursor: 'pointer',
transition: 'all 0.2s ease',
states: {
':hover': {
borderColor: theme('colors-borderActive'),
},
':focus': {
outline: 'none',
borderColor: theme('colors-borderActive'),
}
}
}
},
render({ parts: { Root, Select } }) {
const handleChange = (e: Event) => {
const target = e.target as HTMLSelectElement
const themeName = target.value
document.body.setAttribute('data-theme', themeName)
localStorage.setItem('theme', themeName)
}
return (
<Root>
<Select id="theme-select" onchange={handleChange}>
<option value="dark">Dark</option>
<option value="light">Light</option>
</Select>
</Root>
)
}
})
export const Main = define('SpaMain', {
base: 'div',
minHeight: '100%',
height: '100%',
padding: theme('spacing-xl'),
fontFamily: theme('fonts-mono'),
background: theme('colors-bg'),
color: theme('colors-fg'),
boxSizing: 'border-box',
})
export const Container = define('SpaContainer', {
base: 'div',
maxWidth: 1200,
margin: '0 auto'
})
// Simple client-side router
const Link = define('Link', {
base: 'a',
color: theme('colors-fgMuted'),
textDecoration: 'none',
fontSize: 14,
states: {
hover: {
color: theme('colors-fg'),
}
},
selectors: {
'&[aria-current]': {
color: theme('colors-fg'),
textDecoration: 'underline',
}
},
render({ props, parts: { Root } }) {
const handleClick = (e: Event) => {
e.preventDefault()
window.history.pushState({}, '', props.href)
window.dispatchEvent(new Event('routechange'))
}
return (
<Root {...props} onclick={handleClick}>
{props.children}
</Root>
)
}
})
const Nav = define('Nav', {
base: 'nav',
display: 'flex',
gap: theme('spacing-lg'),
marginBottom: theme('spacing-xl'),
padding: theme('spacing-lg'),
background: theme('colors-bgElevated'),
border: `1px solid ${theme('colors-border')}`,
borderRadius: theme('radius-sm'),
})
const P = define('P', {
color: theme('colors-fgMuted'),
fontSize: 16,
marginBottom: theme('spacing-xxl'),
})
const ExamplesGrid = define('ExamplesGrid', {
display: 'grid',
gap: theme('spacing-lg'),
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))'
})
const ExampleCard = define('ExampleCard', {
base: 'a',
background: theme('colors-bgElevated'),
padding: theme('spacing-lg'),
border: `1px solid ${theme('colors-border')}`,
borderRadius: theme('radius-sm'),
textDecoration: 'none',
display: 'block',
states: {
hover: {
borderColor: theme('colors-borderActive'),
}
},
parts: {
H2: {
color: theme('colors-fg'),
margin: `0 0 ${theme('spacing-sm')} 0`,
fontSize: 18,
fontWeight: 400,
},
P: {
color: theme('colors-fgMuted'),
margin: 0,
fontSize: 14,
}
},
render({ props: { title, desc, ...props }, parts: { Root, H2, P } }) {
const handleClick = (e: Event) => {
e.preventDefault()
window.history.pushState({}, '', props.href)
window.dispatchEvent(new Event('routechange'))
}
return (
<Root {...props} onclick={handleClick}>
<H2>{title}</H2>
<P>{desc}</P>
</Root>
)
}
})
const HomePage = () => (
<>
<P>Client-side rendered examples. Click around, check the source.</P>
<ExamplesGrid>
<ExampleCard href="/spa/profile"
title="Profile Card"
desc="Parts, variants, custom render. Size/theme switching."
/>
<ExampleCard href="/spa/buttons"
title="Buttons"
desc="Intent, size, disabled states. Basic variant patterns."
/>
<ExampleCard href="/spa/navigation"
title="Navigation"
desc="Tabs, pills, breadcrumbs, vertical nav. No router required."
/>
<ExampleCard href="/spa/form"
title="Forms"
desc="Inputs, validation states, checkboxes, textareas."
/>
</ExamplesGrid>
</>
)
const ProfilePage = () => <ProfileExamplesContent />
const ButtonsPage = () => <ButtonExamplesContent />
const NavigationPage = () => <NavigationExamplesContent />
const FormPage = () => <FormExamplesContent />
export function route(path: string) {
switch (path) {
case '/spa':
case '/spa/':
return <HomePage />
case '/spa/profile':
return <ProfilePage />
case '/spa/buttons':
return <ButtonsPage />
case '/spa/navigation':
return <NavigationPage />
case '/spa/form':
return <FormPage />
default:
return <P>404 Not Found</P>
}
}
const HomeLink = define('HomeLink', {
base: 'a',
color: theme('colors-fgMuted'),
textDecoration: 'none',
fontSize: 14,
states: {
hover: {
color: theme('colors-fg'),
}
}
})
export function App() {
const path = window.location.pathname
return (
<Main>
<Container>
<Nav>
<HomeLink href="/">Home</HomeLink>
<Link href="/spa" aria-current={path === '/spa' || path === '/spa/' ? 'page' : undefined}>SPA Examples</Link>
<Link href="/spa/profile" aria-current={path === '/spa/profile' ? 'page' : undefined}>Profile</Link>
<Link href="/spa/buttons" aria-current={path === '/spa/buttons' ? 'page' : undefined}>Buttons</Link>
<Link href="/spa/navigation" aria-current={path === '/spa/navigation' ? 'page' : undefined}>Navigation</Link>
<Link href="/spa/form" aria-current={path === '/spa/form' ? 'page' : undefined}>Forms</Link>
<ThemePicker />
</Nav>
<div id="content">
{route(path)}
</div>
</Container>
</Main>
)
}