diff --git a/README.md b/README.md index 79cc19d..244579a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Why Forge? -CSS is powerful, but hostile to humans at scale. +CSS is powerful, but hostile. ### Problems with CSS diff --git a/examples/navigation.tsx b/examples/navigation.tsx index 2b9aac8..8bd7313 100644 --- a/examples/navigation.tsx +++ b/examples/navigation.tsx @@ -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' } diff --git a/examples/spa/app.tsx b/examples/spa/app.tsx index 427500b..f4a659a 100644 --- a/examples/spa/app.tsx +++ b/examples/spa/app.tsx @@ -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 (
- {route(window.location.pathname)} + {route(path)}
diff --git a/examples/spa/index.html b/examples/spa/index.html index f77f967..4d19034 100644 --- a/examples/spa/index.html +++ b/examples/spa/index.html @@ -4,6 +4,15 @@ Forge SPA Examples +
diff --git a/examples/spa/index.tsx b/examples/spa/index.tsx index 5b64ab2..283c6dd 100644 --- a/examples/spa/index.tsx +++ b/examples/spa/index.tsx @@ -8,12 +8,12 @@ if (root) { render(, 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(, root) + } } -window.addEventListener('routechange', updateContent) -window.addEventListener('popstate', updateContent) +window.addEventListener('routechange', updateApp) +window.addEventListener('popstate', updateApp) diff --git a/examples/ssr/helpers.tsx b/examples/ssr/helpers.tsx index ed74710..808736a 100644 --- a/examples/ssr/helpers.tsx +++ b/examples/ssr/helpers.tsx @@ -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 ( @@ -81,11 +92,11 @@ export const Layout = define({
{props.title}
{props.children} diff --git a/examples/ssr/pages.tsx b/examples/ssr/pages.tsx index 9b0ffca..2483ee0 100644 --- a/examples/ssr/pages.tsx +++ b/examples/ssr/pages.tsx @@ -57,8 +57,8 @@ const ExampleCard = define('SSR_ExampleCard', { } }) -export const IndexPage = () => ( - +export const IndexPage = ({ path }: any) => ( +

Explore component examples built with Forge

@@ -80,20 +80,20 @@ export const IndexPage = () => (
) -export const ButtonExamplesPage = () => ( - +export const ButtonExamplesPage = ({ path }: any) => ( + ) -export const ProfileExamplesPage = () => ( - +export const ProfileExamplesPage = ({ path }: any) => ( + ) -export const NavigationExamplesPage = () => ( - +export const NavigationExamplesPage = ({ path }: any) => ( + ) diff --git a/server.tsx b/server.tsx index 7c0afaf..1d9f6a2 100644 --- a/server.tsx +++ b/server.tsx @@ -7,13 +7,13 @@ const app = new Hono() app.get('/', c => c.html()) -app.get('/ssr', c => c.html()) +app.get('/ssr', c => c.html()) -app.get('/ssr/profile', c => c.html()) +app.get('/ssr/profile', c => c.html()) -app.get('/ssr/buttons', c => c.html()) +app.get('/ssr/buttons', c => c.html()) -app.get('/ssr/navigation', c => c.html()) +app.get('/ssr/navigation', c => c.html()) app.get('/styles', c => c.text(stylesToCSS(styles))) diff --git a/src/index.tsx b/src/index.tsx index e7e40a4..884c5f3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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)) { 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) } } }