pretty cool
This commit is contained in:
parent
3c3ad71296
commit
ad7922ee7a
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Why Forge?
|
||||
|
||||
CSS is powerful, but hostile to humans at scale.
|
||||
CSS is powerful, but hostile.
|
||||
|
||||
### Problems with CSS
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const TabSwitcher = define('TabSwitcher', {
|
|||
},
|
||||
|
||||
selectors: {
|
||||
'.TabSwitcher_Input:checked + &': {
|
||||
'@Input:checked + &': {
|
||||
color: '#3b82f6',
|
||||
borderBottom: '2px solid #3b82f6'
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ const TabSwitcher = define('TabSwitcher', {
|
|||
borderRadius: 8,
|
||||
|
||||
selectors: {
|
||||
'.TabSwitcher_Input:checked ~ &': {
|
||||
'@Input:checked ~ &': {
|
||||
display: 'block'
|
||||
}
|
||||
}
|
||||
|
|
@ -115,11 +115,11 @@ const Pills = define('Pills', {
|
|||
},
|
||||
|
||||
selectors: {
|
||||
'.Pills_Input:checked + &': {
|
||||
'@Input:checked + &': {
|
||||
background: '#3b82f6',
|
||||
color: 'white'
|
||||
},
|
||||
'.Pills_Input:checked + &:hover': {
|
||||
'@Input:checked + &:hover': {
|
||||
background: '#2563eb'
|
||||
}
|
||||
}
|
||||
|
|
@ -185,11 +185,11 @@ const VerticalNav = define('VerticalNav', {
|
|||
},
|
||||
|
||||
selectors: {
|
||||
'.VerticalNav_Input:checked + &': {
|
||||
'@Input:checked + &': {
|
||||
background: '#eff6ff',
|
||||
color: '#3b82f6',
|
||||
},
|
||||
'.VerticalNav_Input:checked + &:hover': {
|
||||
'@Input:checked + &:hover': {
|
||||
background: '#dbeafe',
|
||||
color: '#2563eb'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { NavigationExamplesContent } from '../navigation'
|
|||
export const Main = define('SpaMain', {
|
||||
base: 'div',
|
||||
|
||||
minHeight: '100%',
|
||||
padding: '40px 20px',
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
||||
background: '#f3f4f6',
|
||||
|
|
@ -24,6 +25,7 @@ const Link = define('Link', {
|
|||
|
||||
color: '#3b82f6',
|
||||
textDecoration: 'none',
|
||||
fontWeight: 500,
|
||||
|
||||
states: {
|
||||
hover: {
|
||||
|
|
@ -31,6 +33,14 @@ const Link = define('Link', {
|
|||
}
|
||||
},
|
||||
|
||||
selectors: {
|
||||
'&[aria-current]': {
|
||||
color: '#1e40af',
|
||||
fontWeight: 600,
|
||||
textDecoration: 'underline'
|
||||
}
|
||||
},
|
||||
|
||||
render({ props, parts: { Root } }) {
|
||||
const handleClick = (e: Event) => {
|
||||
e.preventDefault()
|
||||
|
|
@ -161,18 +171,20 @@ export function route(path: string) {
|
|||
}
|
||||
|
||||
export function App() {
|
||||
const path = window.location.pathname
|
||||
|
||||
return (
|
||||
<Main>
|
||||
<Container>
|
||||
<Nav>
|
||||
<a href="/" style="color: #3b82f6; text-decoration: none;">Home</a>
|
||||
<Link href="/spa">SPA Examples</Link>
|
||||
<Link href="/spa/profile">Profile</Link>
|
||||
<Link href="/spa/buttons">Buttons</Link>
|
||||
<Link href="/spa/navigation">Navigation</Link>
|
||||
<a href="/" style="color: #3b82f6; text-decoration: none; font-weight: 500;">Home</a>
|
||||
<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>
|
||||
</Nav>
|
||||
<div id="content">
|
||||
{route(window.location.pathname)}
|
||||
{route(path)}
|
||||
</div>
|
||||
</Container>
|
||||
</Main>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,15 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Forge SPA Examples</title>
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ if (root) {
|
|||
render(<App />, root)
|
||||
}
|
||||
|
||||
// On route change, only update the content div
|
||||
function updateContent() {
|
||||
const contentDiv = document.getElementById('content')
|
||||
if (contentDiv)
|
||||
render(route(window.location.pathname), contentDiv)
|
||||
// On route change, re-render the whole app to update nav state
|
||||
function updateApp() {
|
||||
if (root) {
|
||||
render(<App />, root)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('routechange', updateContent)
|
||||
window.addEventListener('popstate', updateContent)
|
||||
window.addEventListener('routechange', updateApp)
|
||||
window.addEventListener('popstate', updateApp)
|
||||
|
|
|
|||
|
|
@ -60,16 +60,27 @@ const NavLink = define('SSR_NavLink', {
|
|||
|
||||
color: '#3b82f6',
|
||||
textDecoration: 'none',
|
||||
fontWeight: 500,
|
||||
|
||||
states: {
|
||||
hover: {
|
||||
textDecoration: 'underline'
|
||||
}
|
||||
},
|
||||
|
||||
selectors: {
|
||||
'&[aria-current]': {
|
||||
color: '#1e40af',
|
||||
fontWeight: 600,
|
||||
textDecoration: 'underline'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const Layout = define({
|
||||
render({ props }) {
|
||||
const path = props.path || ''
|
||||
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
|
|
@ -81,11 +92,11 @@ export const Layout = define({
|
|||
<Body>
|
||||
<Container>
|
||||
<Nav>
|
||||
<NavLink href="/">Home</NavLink>
|
||||
<NavLink href="/ssr">SSR Examples</NavLink>
|
||||
<NavLink href="/ssr/profile">Profile</NavLink>
|
||||
<NavLink href="/ssr/buttons">Buttons</NavLink>
|
||||
<NavLink href="/ssr/navigation">Navigation</NavLink>
|
||||
<NavLink href="/" aria-current={path === '/' ? 'page' : undefined}>Home</NavLink>
|
||||
<NavLink href="/ssr" aria-current={path.startsWith('/ssr') && path !== '/ssr/profile' && path !== '/ssr/buttons' && path !== '/ssr/navigation' ? 'page' : undefined}>SSR Examples</NavLink>
|
||||
<NavLink href="/ssr/profile" aria-current={path === '/ssr/profile' ? 'page' : undefined}>Profile</NavLink>
|
||||
<NavLink href="/ssr/buttons" aria-current={path === '/ssr/buttons' ? 'page' : undefined}>Buttons</NavLink>
|
||||
<NavLink href="/ssr/navigation" aria-current={path === '/ssr/navigation' ? 'page' : undefined}>Navigation</NavLink>
|
||||
</Nav>
|
||||
<Header>{props.title}</Header>
|
||||
{props.children}
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ const ExampleCard = define('SSR_ExampleCard', {
|
|||
}
|
||||
})
|
||||
|
||||
export const IndexPage = () => (
|
||||
<Layout title="Forge Examples">
|
||||
export const IndexPage = ({ path }: any) => (
|
||||
<Layout title="Forge Examples" path={path}>
|
||||
<P>Explore component examples built with Forge</P>
|
||||
|
||||
<ExamplesGrid>
|
||||
|
|
@ -80,20 +80,20 @@ export const IndexPage = () => (
|
|||
</Layout>
|
||||
)
|
||||
|
||||
export const ButtonExamplesPage = () => (
|
||||
<Layout title="Forge Button Component Examples">
|
||||
export const ButtonExamplesPage = ({ path }: any) => (
|
||||
<Layout title="Forge Button Component Examples" path={path}>
|
||||
<ButtonExamplesContent />
|
||||
</Layout>
|
||||
)
|
||||
|
||||
export const ProfileExamplesPage = () => (
|
||||
<Layout title="Forge Profile Examples">
|
||||
export const ProfileExamplesPage = ({ path }: any) => (
|
||||
<Layout title="Forge Profile Examples" path={path}>
|
||||
<ProfileExamplesContent />
|
||||
</Layout>
|
||||
)
|
||||
|
||||
export const NavigationExamplesPage = () => (
|
||||
<Layout title="Forge Navigation Examples">
|
||||
export const NavigationExamplesPage = ({ path }: any) => (
|
||||
<Layout title="Forge Navigation Examples" path={path}>
|
||||
<NavigationExamplesContent />
|
||||
</Layout>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ const app = new Hono()
|
|||
|
||||
app.get('/', c => c.html(<LandingPage />))
|
||||
|
||||
app.get('/ssr', c => c.html(<IndexPage />))
|
||||
app.get('/ssr', c => c.html(<IndexPage path="/ssr" />))
|
||||
|
||||
app.get('/ssr/profile', c => c.html(<ProfileExamplesPage />))
|
||||
app.get('/ssr/profile', c => c.html(<ProfileExamplesPage path="/ssr/profile" />))
|
||||
|
||||
app.get('/ssr/buttons', c => c.html(<ButtonExamplesPage />))
|
||||
app.get('/ssr/buttons', c => c.html(<ButtonExamplesPage path="/ssr/buttons" />))
|
||||
|
||||
app.get('/ssr/navigation', c => c.html(<NavigationExamplesPage />))
|
||||
app.get('/ssr/navigation', c => c.html(<NavigationExamplesPage path="/ssr/navigation" />))
|
||||
|
||||
app.get('/styles', c => c.text(stylesToCSS(styles)))
|
||||
|
||||
|
|
|
|||
|
|
@ -134,30 +134,29 @@ function stateName(state: string): string {
|
|||
return state.startsWith(':') ? state : `:${state}`
|
||||
}
|
||||
|
||||
// adds CSS styles for tag definition
|
||||
function registerStyles(name: string, def: TagDef) {
|
||||
const rootClassName = makeClassName(name)
|
||||
styles[rootClassName] ??= makeStyle(def)
|
||||
// Register base styles, selectors, and states for a class
|
||||
function registerClassStyles(name: string, className: string, def: TagDef) {
|
||||
styles[className] ??= makeStyle(def)
|
||||
|
||||
for (let [selector, selectorDef] of Object.entries(def.selectors ?? {})) {
|
||||
selector = selector.replace('&', `.${rootClassName}`)
|
||||
selector = selector.replace(/@(\w+)/g, (_, partName) => `.${makeClassName(name, partName)}`)
|
||||
selector = selector.replace('&', `.${className}`)
|
||||
if (styles[selector]) throw `${selector} already defined!`
|
||||
styles[selector] = makeStyle(selectorDef)
|
||||
}
|
||||
|
||||
for (const [state, style] of Object.entries(def.states ?? {}))
|
||||
styles[`${rootClassName}${stateName(state)}`] = makeStyle(style)
|
||||
styles[`${className}${stateName(state)}`] = makeStyle(style)
|
||||
}
|
||||
|
||||
// adds CSS styles for tag definition
|
||||
function registerStyles(name: string, def: TagDef) {
|
||||
const rootClassName = makeClassName(name)
|
||||
registerClassStyles(name, rootClassName, def)
|
||||
|
||||
for (const [partName, partDef] of Object.entries(def.parts ?? {})) {
|
||||
const partClassName = makeClassName(name, partName)
|
||||
styles[partClassName] ??= makeStyle(partDef)
|
||||
for (let [selector, selectorDef] of Object.entries(partDef.selectors ?? {})) {
|
||||
selector = selector.replace('&', `.${partClassName}`)
|
||||
if (styles[selector]) throw `${selector} already defined!`
|
||||
styles[selector] = makeStyle(selectorDef)
|
||||
}
|
||||
for (const [state, style] of Object.entries(partDef.states ?? {}))
|
||||
styles[`${partClassName}${stateName(state)}`] = makeStyle(style)
|
||||
registerClassStyles(name, partClassName, partDef)
|
||||
}
|
||||
|
||||
for (const [variantName, variantConfig] of Object.entries(def.variants ?? {})) {
|
||||
|
|
@ -171,50 +170,22 @@ function registerStyles(name: string, def: TagDef) {
|
|||
const variantDef = variantConfig as TagDef
|
||||
const baseClassName = makeClassName(name)
|
||||
const className = `${baseClassName}.${variantName}`
|
||||
styles[className] ??= makeStyle(variantDef)
|
||||
for (let [selector, selectorDef] of Object.entries(variantDef.selectors ?? {})) {
|
||||
selector = selector.replace('&', `.${className}`)
|
||||
if (styles[selector]) throw `${selector} already defined!`
|
||||
styles[selector] = makeStyle(selectorDef)
|
||||
}
|
||||
for (const [state, style] of Object.entries(variantDef.states ?? {}))
|
||||
styles[`${className}${stateName(state)}`] = makeStyle(style)
|
||||
registerClassStyles(name, className, variantDef)
|
||||
|
||||
for (const [partName, partDef] of Object.entries(variantDef.parts ?? {})) {
|
||||
const basePartClassName = makeClassName(name, partName)
|
||||
const partClassName = `${basePartClassName}.${variantName}`
|
||||
styles[partClassName] ??= makeStyle(partDef)
|
||||
for (let [selector, selectorDef] of Object.entries(partDef.selectors ?? {})) {
|
||||
selector = selector.replace('&', `.${partClassName}`)
|
||||
if (styles[selector]) throw `${selector} already defined!`
|
||||
styles[selector] = makeStyle(selectorDef)
|
||||
}
|
||||
for (const [state, style] of Object.entries(partDef.states ?? {}))
|
||||
styles[`${partClassName}${stateName(state)}`] = makeStyle(style)
|
||||
registerClassStyles(name, partClassName, partDef)
|
||||
}
|
||||
} else {
|
||||
// Keyed variant - iterate over the keys
|
||||
for (const [variantKey, variantDef] of Object.entries(variantConfig as Record<string, TagDef>)) {
|
||||
const className = makeClassName(name, undefined, variantName, variantKey)
|
||||
styles[className] ??= makeStyle(variantDef)
|
||||
for (let [selector, selectorDef] of Object.entries(variantDef.selectors ?? {})) {
|
||||
selector = selector.replace('&', `.${className}`)
|
||||
if (styles[selector]) throw `${selector} already defined!`
|
||||
styles[selector] = makeStyle(selectorDef)
|
||||
}
|
||||
for (const [state, style] of Object.entries(variantDef.states ?? {}))
|
||||
styles[`${className}${stateName(state)}`] = makeStyle(style)
|
||||
registerClassStyles(name, className, variantDef)
|
||||
|
||||
for (const [partName, partDef] of Object.entries(variantDef.parts ?? {})) {
|
||||
const partClassName = makeClassName(name, partName, variantName, variantKey)
|
||||
styles[partClassName] ??= makeStyle(partDef)
|
||||
for (let [selector, selectorDef] of Object.entries(partDef.selectors ?? {})) {
|
||||
selector = selector.replace('&', `.${partClassName}`)
|
||||
if (styles[selector]) throw `${selector} already defined!`
|
||||
styles[selector] = makeStyle(selectorDef)
|
||||
}
|
||||
for (const [state, style] of Object.entries(partDef.states ?? {}))
|
||||
styles[`${partClassName}${stateName(state)}`] = makeStyle(style)
|
||||
registerClassStyles(name, partClassName, partDef)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user