268 lines
6.3 KiB
TypeScript
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 } }) {
|
|
return (
|
|
<Root>
|
|
<Select id="theme-select" onChange={themeChanged}>
|
|
<option value="dark">Dark</option>
|
|
<option value="light">Light</option>
|
|
</Select>
|
|
</Root>
|
|
)
|
|
}
|
|
})
|
|
|
|
function themeChanged(e: Event) {
|
|
const target = e.target as HTMLSelectElement
|
|
const themeName = target.value
|
|
document.body.setAttribute('data-theme', themeName)
|
|
localStorage.setItem('theme', themeName)
|
|
}
|
|
|
|
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>
|
|
)
|
|
}
|