pretty cool

This commit is contained in:
Chris Wanstrath 2025-12-28 18:51:38 -08:00
parent 3c3ad71296
commit ad7922ee7a
9 changed files with 86 additions and 83 deletions

View File

@ -2,7 +2,7 @@
## Why Forge?
CSS is powerful, but hostile to humans at scale.
CSS is powerful, but hostile.
### Problems with CSS

View File

@ -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'
}

View File

@ -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>

View File

@ -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>

View File

@ -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)

View File

@ -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}

View File

@ -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>
)

View File

@ -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)))

View File

@ -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)
}
}
}