From 992bddc7692f152dc3efeb2505eff82c9be82f2b Mon Sep 17 00:00:00 2001 From: Chris Wanstrath <2+defunkt@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:04:08 -0800 Subject: [PATCH] readme on landing page --- bun.lock | 10 +- examples/landing.tsx | 547 +++++++++++++++++++++++++++++++++++++++ examples/spa/app.tsx | 2 +- examples/ssr/landing.tsx | 116 --------- package.json | 5 +- server.tsx | 2 +- src/tests/index.test.tsx | 6 +- 7 files changed, 565 insertions(+), 123 deletions(-) create mode 100644 examples/landing.tsx delete mode 100644 examples/ssr/landing.tsx diff --git a/bun.lock b/bun.lock index b9ae47c..eb943e3 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "forge", @@ -9,6 +8,9 @@ }, "devDependencies": { "@types/bun": "latest", + "@types/prismjs": "^1.26.5", + "prismjs": "^1.30.0", + "snarkdown": "^2.0.0", }, "peerDependencies": { "typescript": "^5", @@ -20,10 +22,16 @@ "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + "@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="], + "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], "hono": ["hono@4.11.3", "", {}, "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w=="], + "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], + + "snarkdown": ["snarkdown@2.0.0", "", {}, "sha512-MgL/7k/AZdXCTJiNgrO7chgDqaB9FGM/1Tvlcenenb7div6obaDATzs16JhFyHHBGodHT3B7RzRc5qk8pFhg3A=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], diff --git a/examples/landing.tsx b/examples/landing.tsx new file mode 100644 index 0000000..7f54513 --- /dev/null +++ b/examples/landing.tsx @@ -0,0 +1,547 @@ +import { createScope, Styles } from '../src' +import { theme } from './ssr/themes' +import markdown from 'snarkdown' +import Prism from 'prismjs' +import 'prismjs/components/prism-javascript' +import 'prismjs/components/prism-jsx' +import 'prismjs/components/prism-typescript' +import 'prismjs/components/prism-tsx' + +const { define } = createScope('Landing') + +const Page = define('Page', { + base: 'body', + + margin: 0, + padding: theme('spacing-xl'), + minHeight: '100vh', + fontFamily: theme('fonts-mono'), + background: theme('colors-bg'), + color: theme('colors-fg'), +}) + +const Container = define('Container', { + maxWidth: 800, + margin: '0 auto', +}) + +const Pre = define('Pre', { + base: 'pre', + + fontSize: 14, + lineHeight: 1.4, + marginBottom: theme('spacing-xl'), + color: theme('colors-fg'), + whiteSpace: 'pre', + borderBottom: '1px solid var(--theme-colors-border)', +}) + +const P = define('P', { + base: 'p', + + fontSize: 16, + lineHeight: 1.6, + marginBottom: theme('spacing-xl'), + color: theme('colors-fgMuted'), +}) + +const LinkSection = define('LinkSection', { + marginBottom: theme('spacing-xl'), +}) + +const Link = define('Link', { + base: 'a', + + display: 'inline-block', + marginRight: theme('spacing-xl'), + padding: `${theme('spacing-sm')} ${theme('spacing-lg')}`, + background: theme('colors-bgElevated'), + border: `1px solid ${theme('colors-border')}`, + color: theme('colors-fg'), + textDecoration: 'none', + fontSize: 14, + + states: { + ':hover': { + background: theme('colors-bgHover'), + borderColor: theme('colors-borderActive'), + } + } +}) + +const ThemeToggle = define('ThemeToggle', { + position: 'fixed', + top: theme('spacing-lg'), + right: theme('spacing-lg'), + padding: `${theme('spacing-sm')} ${theme('spacing-lg')}`, + background: theme('colors-bgElevated'), + border: `1px solid ${theme('colors-border')}`, + color: theme('colors-fg'), + fontSize: 14, + cursor: 'pointer', + fontFamily: theme('fonts-mono'), + + states: { + ':hover': { + background: theme('colors-bgHover'), + borderColor: theme('colors-borderActive'), + } + } +}) + +// Helper to highlight code blocks in markdown HTML +function highlightCodeBlocks(html: string): string { + return html.replace(/
([\s\S]*?)<\/code><\/pre>/g, (_, lang, code) => {
+    // Decode HTML entities
+    const decoded = code
+      .replace(/</g, '<')
+      .replace(/>/g, '>')
+      .replace(/&/g, '&')
+      .replace(/"/g, '"')
+      .replace(/'/g, "'")
+
+    // Map language to Prism grammar (tsx/typescript)
+    const grammar = lang === 'tsx' ? Prism.languages.tsx : Prism.languages.typescript
+
+    const highlighted = Prism.highlight(decoded, grammar!, lang)
+    return `
${highlighted}
` + }) +} + +export const LandingPage = () => { + const themeScript = ` + function switchTheme(themeName) { + document.body.setAttribute('data-theme', themeName) + localStorage.setItem('theme', themeName) + updateThemeToggle() + } + + function updateThemeToggle() { + const currentTheme = document.body.getAttribute('data-theme') + const toggle = document.getElementById('theme-toggle') + if (toggle) { + toggle.textContent = currentTheme === 'dark' ? '☀ light' : '🌙 dark' + } + } + + function toggleTheme() { + const currentTheme = document.body.getAttribute('data-theme') + const newTheme = currentTheme === 'dark' ? 'light' : 'dark' + switchTheme(newTheme) + } + + window.switchTheme = switchTheme + window.toggleTheme = toggleTheme + + // Load saved theme or default to dark + const savedTheme = localStorage.getItem('theme') || 'dark' + document.body.setAttribute('data-theme', savedTheme) + updateThemeToggle() + ` + + const prismTheme = ` + code[class*="language-"], + pre[class*="language-"] { + color: var(--theme-colors-fg); + background: none; + font-family: var(--theme-fonts-mono); + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + tab-size: 4; + hyphens: none; + } + + pre[class*="language-"] { + padding: 1em; + margin: 0.5em 0; + overflow: auto; + background: var(--theme-colors-bgElevated); + border: 1px solid var(--theme-colors-border); + border-radius: 4px; + } + + /* Dark theme colors */ + [data-theme="dark"] .token.comment, + [data-theme="dark"] .token.prolog, + [data-theme="dark"] .token.doctype, + [data-theme="dark"] .token.cdata { + color: #999999; + } + + [data-theme="dark"] .token.punctuation { + color: #808080; + } + + [data-theme="dark"] .token.tag { + color: #ff6b6b; + } + + [data-theme="dark"] .token.property, + [data-theme="dark"] .token.boolean, + [data-theme="dark"] .token.number, + [data-theme="dark"] .token.constant, + [data-theme="dark"] .token.symbol, + [data-theme="dark"] .token.deleted { + color: #ffd93d; + } + + [data-theme="dark"] .token.selector, + [data-theme="dark"] .token.attr-name, + [data-theme="dark"] .token.string, + [data-theme="dark"] .token.char, + [data-theme="dark"] .token.builtin, + [data-theme="dark"] .token.inserted { + color: #ff6b6b; + } + + [data-theme="dark"] .token.operator, + [data-theme="dark"] .token.entity, + [data-theme="dark"] .token.url, + [data-theme="dark"] .language-css .token.string, + [data-theme="dark"] .style .token.string { + color: #d4d4d4; + } + + [data-theme="dark"] .token.atrule, + [data-theme="dark"] .token.attr-value, + [data-theme="dark"] .token.keyword { + color: #bb86fc; + } + + [data-theme="dark"] .token.function, + [data-theme="dark"] .token.class-name { + color: #4dd0e1; + } + + [data-theme="dark"] .token.regex, + [data-theme="dark"] .token.important, + [data-theme="dark"] .token.variable { + color: #bb86fc; + } + + /* Light theme colors */ + [data-theme="light"] .token.comment, + [data-theme="light"] .token.prolog, + [data-theme="light"] .token.doctype, + [data-theme="light"] .token.cdata { + color: #888888; + } + + [data-theme="light"] .token.punctuation { + color: #666666; + } + + [data-theme="light"] .token.tag { + color: #dc143c; + } + + [data-theme="light"] .token.property, + [data-theme="light"] .token.boolean, + [data-theme="light"] .token.number, + [data-theme="light"] .token.constant, + [data-theme="light"] .token.symbol, + [data-theme="light"] .token.deleted { + color: #c9a700; + } + + [data-theme="light"] .token.selector, + [data-theme="light"] .token.attr-name, + [data-theme="light"] .token.string, + [data-theme="light"] .token.char, + [data-theme="light"] .token.builtin, + [data-theme="light"] .token.inserted { + color: #dc143c; + } + + [data-theme="light"] .token.operator, + [data-theme="light"] .token.entity, + [data-theme="light"] .token.url, + [data-theme="light"] .language-css .token.string, + [data-theme="light"] .style .token.string { + color: #000000; + } + + [data-theme="light"] .token.atrule, + [data-theme="light"] .token.attr-value, + [data-theme="light"] .token.keyword { + color: #9333ea; + } + + [data-theme="light"] .token.function, + [data-theme="light"] .token.class-name { + color: #0891b2; + } + + [data-theme="light"] .token.regex, + [data-theme="light"] .token.important, + [data-theme="light"] .token.variable { + color: #9333ea; + } + ` + + return ( + + + + + forge + +