From 4cf3e0e832ec9ebfbec569d4e097d44a48dfe947 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sat, 27 Dec 2025 21:14:58 -0800 Subject: [PATCH] big stuff --- examples/button.tsx | 8 +- examples/navigation.tsx | 441 +++++++++++++++++++------- examples/profile.tsx | 9 +- examples/spa/app.tsx | 180 +++++++++++ examples/spa/index.html | 12 + examples/spa/index.tsx | 19 ++ examples/{ => ssr}/helpers.tsx | 34 +- examples/ssr/landing.tsx | 117 +++++++ examples/{index.tsx => ssr/pages.tsx} | 39 ++- package.json | 5 +- server.tsx | 37 +-- src/index.tsx | 22 ++ tsconfig.json | 2 +- 13 files changed, 773 insertions(+), 152 deletions(-) create mode 100644 examples/spa/app.tsx create mode 100644 examples/spa/index.html create mode 100644 examples/spa/index.tsx rename examples/{ => ssr}/helpers.tsx (61%) create mode 100644 examples/ssr/landing.tsx rename examples/{index.tsx => ssr/pages.tsx} (58%) diff --git a/examples/button.tsx b/examples/button.tsx index f51e4ef..a72479e 100644 --- a/examples/button.tsx +++ b/examples/button.tsx @@ -1,5 +1,5 @@ import { define } from '../src' -import { Layout, ExampleSection } from './helpers' +import { ExampleSection } from './ssr/helpers' const Button = define('Button', { base: 'button', @@ -80,8 +80,8 @@ const ButtonRow = define('ButtonRow', { alignItems: 'center', }) -export const ButtonExamplesPage = () => ( - +export const ButtonExamplesContent = () => ( + <> @@ -117,5 +117,5 @@ export const ButtonExamplesPage = () => ( - + ) diff --git a/examples/navigation.tsx b/examples/navigation.tsx index 429cbaf..1232ff8 100644 --- a/examples/navigation.tsx +++ b/examples/navigation.tsx @@ -1,20 +1,25 @@ import { define } from '../src' -import { Layout, ExampleSection } from './helpers' - -const Tabs = define('Tabs', { - display: 'flex', - gap: 0, - borderBottom: '2px solid #e5e7eb', +import { ExampleSection } from './ssr/helpers' +const TabSwitcher = define('TabSwitcher', { parts: { - Tab: { - base: 'button', + Input: { + base: 'input', + display: 'none', // Hide radio inputs + }, + TabBar: { + display: 'flex', + gap: 0, + borderBottom: '2px solid #e5e7eb', + marginBottom: 24, + }, + TabLabel: { + base: 'label', padding: '12px 24px', position: 'relative', marginBottom: -2, background: 'transparent', - border: 'none', borderBottom: '2px solid transparent', color: '#6b7280', fontSize: 14, @@ -27,50 +32,76 @@ const Tabs = define('Tabs', { color: '#111827', } } + }, + Content: { + display: 'none', + padding: 20, + background: '#f9fafb', + borderRadius: 8, } }, - variants: { - active: { - parts: { - Tab: { - color: '#3b82f6', - borderBottom: '2px solid #3b82f6', - } - } - } - }, - - render({ props, parts: { Root, Tab } }) { + render({ props, parts: { Root, Input, TabBar, TabLabel, Content } }) { return ( - + + diff --git a/examples/spa/index.tsx b/examples/spa/index.tsx new file mode 100644 index 0000000..5b64ab2 --- /dev/null +++ b/examples/spa/index.tsx @@ -0,0 +1,19 @@ +import { render } from 'hono/jsx/dom' +import { App, route } from './app' + +const root = document.getElementById('root') + +// Initial render +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) +} + +window.addEventListener('routechange', updateContent) +window.addEventListener('popstate', updateContent) diff --git a/examples/helpers.tsx b/examples/ssr/helpers.tsx similarity index 61% rename from examples/helpers.tsx rename to examples/ssr/helpers.tsx index b294d82..ed74710 100644 --- a/examples/helpers.tsx +++ b/examples/ssr/helpers.tsx @@ -1,4 +1,4 @@ -import { define, Styles } from '../src' +import { define, Styles } from '../../src' export const Body = define('Body', { base: 'body', @@ -43,6 +43,31 @@ export const ExampleSection = define('ExampleSection', { } }) +const Nav = define('SSR_Nav', { + base: 'nav', + + display: 'flex', + gap: 20, + marginBottom: 40, + padding: 20, + background: 'white', + borderRadius: 8, + boxShadow: '0 1px 3px rgba(0,0,0,0.1)' +}) + +const NavLink = define('SSR_NavLink', { + base: 'a', + + color: '#3b82f6', + textDecoration: 'none', + + states: { + hover: { + textDecoration: 'underline' + } + } +}) + export const Layout = define({ render({ props }) { return ( @@ -55,6 +80,13 @@ export const Layout = define({ +
{props.title}
{props.children}
diff --git a/examples/ssr/landing.tsx b/examples/ssr/landing.tsx new file mode 100644 index 0000000..1f8152e --- /dev/null +++ b/examples/ssr/landing.tsx @@ -0,0 +1,117 @@ +import { define, Styles } from '../../src' + +const Page = define('LandingPage', { + base: 'body', + + margin: 0, + padding: 0, + minHeight: '100vh', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif", + background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', +}) + +const Container = define('LandingContainer', { + textAlign: 'center', + color: 'white', +}) + +const Title = define('LandingTitle', { + base: 'h1', + + fontSize: 48, + fontWeight: 700, + marginBottom: 50, + color: 'white', +}) + +const Subtitle = define('LandingSubtitle', { + base: 'p', + + fontSize: 20, + marginBottom: 48, + color: 'rgba(255, 255, 255, 0.9)', +}) + +const ButtonGroup = define('LandingButtonGroup', { + display: 'flex', + gap: 50, + justifyContent: 'center', + flexWrap: 'wrap', +}) + +const ChoiceCard = define('LandingChoiceCard', { + base: 'a', + + display: 'block', + padding: 40, + background: 'white', + borderRadius: 16, + textDecoration: 'none', + color: '#111827', + boxShadow: '0 10px 30px rgba(0, 0, 0, 0.2)', + transition: 'all 0.3s ease', + minWidth: 250, + + states: { + ':hover': { + transform: 'translateY(-8px)', + boxShadow: '0 20px 40px rgba(0, 0, 0, 0.3)', + } + }, + + parts: { + Icon: { + fontSize: 48, + marginBottom: 16, + }, + Title: { + base: 'h2', + fontSize: 24, + fontWeight: 600, + marginBottom: 8, + color: '#111827', + } + }, + + render({ props, parts: { Root, Icon, Title, Description } }) { + return ( + + {props.icon} + {props.title} + + ) + } +}) + +export const LandingPage = () => ( + + + + + Forge - Choose Your Rendering Mode + + + + + Welcome to Forge + + + + + + + + + +) diff --git a/examples/index.tsx b/examples/ssr/pages.tsx similarity index 58% rename from examples/index.tsx rename to examples/ssr/pages.tsx index 73f3f11..9b0ffca 100644 --- a/examples/index.tsx +++ b/examples/ssr/pages.tsx @@ -1,19 +1,22 @@ -import { define } from '../src' +import { define } from '../../src' import { Layout } from './helpers' +import { ButtonExamplesContent } from '../button' +import { ProfileExamplesContent } from '../profile' +import { NavigationExamplesContent } from '../navigation' -const P = define('P', { +const P = define('SSR_P', { color: '#6b7280', fontSize: 18, marginBottom: 48, }) -const ExamplesGrid = define('ExamplesGrid', { +const ExamplesGrid = define('SSR_ExamplesGrid', { display: 'grid', gap: 20, gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))' }) -const ExampleCard = define('ExampleCard', { +const ExampleCard = define('SSR_ExampleCard', { base: 'a', background: 'white', @@ -44,9 +47,9 @@ const ExampleCard = define('ExampleCard', { } }, - render({ props: { title, desc }, parts: { Root, H2, P } }) { + render({ props: { title, desc, ...rest }, parts: { Root, H2, P } }) { return ( - +

{title}

{desc}

@@ -59,20 +62,38 @@ export const IndexPage = () => (

Explore component examples built with Forge

- - - ) + +export const ButtonExamplesPage = () => ( + + + +) + +export const ProfileExamplesPage = () => ( + + + +) + +export const NavigationExamplesPage = () => ( + + + +) diff --git a/package.json b/package.json index 78e595e..0104871 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,9 @@ "module": "src/index.ts", "type": "module", "scripts": { - "dev": "bun run --hot server.tsx", - "test": "bun test" + "dev": "bun build:spa && bun run --hot server.tsx", + "test": "bun test", + "build:spa": "bun build examples/spa/index.tsx --outfile dist/spa.js --target browser" }, "devDependencies": { "@types/bun": "latest" diff --git a/server.tsx b/server.tsx index 54f5e17..7c0afaf 100644 --- a/server.tsx +++ b/server.tsx @@ -1,30 +1,31 @@ import { Hono } from 'hono' -import { IndexPage } from './examples/index' -import { ProfileExamplesPage } from './examples/profile' -import { ButtonExamplesPage } from './examples/button' -import { NavigationExamplesPage } from './examples/navigation' +import { IndexPage, ProfileExamplesPage, ButtonExamplesPage, NavigationExamplesPage } from './examples/ssr/pages' +import { LandingPage } from './examples/ssr/landing' import { styles, stylesToCSS } from './src' const app = new Hono() -app.get('/', c => { - return c.html() -}) +app.get('/', c => c.html()) -app.get('/profile', c => { - return c.html() -}) +app.get('/ssr', c => c.html()) -app.get('/buttons', c => { - return c.html() -}) +app.get('/ssr/profile', c => c.html()) -app.get('/navigation', c => { - return c.html() -}) +app.get('/ssr/buttons', c => c.html()) -app.get('/styles', c => { - return c.text(stylesToCSS(styles)) +app.get('/ssr/navigation', c => c.html()) + +app.get('/styles', c => c.text(stylesToCSS(styles))) + +app.get('/spa/*', async c => c.html(await Bun.file('./examples/spa/index.html').text())) + +app.get('/spa.js', async c => { + const file = Bun.file('./dist/spa.js') + return new Response(file, { + headers: { + 'Content-Type': 'application/javascript', + }, + }) }) export default { diff --git a/src/index.tsx b/src/index.tsx index 9b39377..8ba3c53 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,8 +2,27 @@ import type { JSX } from 'hono/jsx' import { type TagDef, UnitlessProps, NonStyleKeys } from './types' export const styles: Record> = {} + +// Use w/ SSR: export const Styles = () =>