forge/examples/landing.tsx
2026-01-05 17:04:08 -08:00

547 lines
13 KiB
TypeScript

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(/<pre class="code[^"]*"><code class="language-(\w+)">([\s\S]*?)<\/code><\/pre>/g, (_, lang, code) => {
// Decode HTML entities
const decoded = code
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.replace(/&quot;/g, '"')
.replace(/&#39;/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 `<pre class="code ${lang}"><code class="language-${lang}">${highlighted}</code></pre>`
})
}
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 (
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>forge</title>
<Styles />
<style dangerouslySetInnerHTML={{ __html: prismTheme }} />
</head>
<Page data-theme="dark">
<ThemeToggle id="theme-toggle" onclick="toggleTheme()">🌙 dark</ThemeToggle>
<Container>
<Pre>{`╔═╝╔═║╔═║╔═╝╔═╝
╔═╝║ ║╔╔╝║ ║╔═╝
╝ ══╝╝ ╝══╝══╝`}</Pre>
<LinkSection>
<Link href="/ssr">SSR demos </Link>
<Link href="/spa">SPA demos </Link>
</LinkSection>
<div dangerouslySetInnerHTML={{ __html: highlightCodeBlocks(markdown(MARKDOWN_CONTENT)) }}></div>
</Container>
<script dangerouslySetInnerHTML={{ __html: themeScript }}></script>
</Page>
</html>
)
}
const MARKDOWN_CONTENT = `
## overview
Forge is a typed, local, variant-driven way to organize CSS and create
self-contained TSX components out of discrete parts.
## css problems
- Styles are global and open - anything can override anything anywhere.
- No IDE-friendly link between the class name in markup and its definition.
- Organizational techniques are patterns a human must know and follow, not APIs.
- Errors happen silently.
## forge solutions
- All styles are local to your TSX components.
- Styles are defined using TS typing.
- Component styles are made up of independently styled "Parts".
- "Variants" replace inline styles with typed, declarative parameters.
- Style composition is deterministic.
- Themes are easy.
- Errors and feedback are provided.
## examples
### styles
\`\`\`tsx
import { define } from "forge"
export const Container = define("Container", {
width: 600,
margin: '0 auto',
})
export const Button = define({
base: "button",
padding: 20,
background: "blue",
})
// Usage
<Container>
<Button>Click me</Button>
</Container>
\`\`\`
### variants
\`\`\`tsx
import { define } from "forge"
export const Button = define({
base: "button",
padding: 20,
background: "blue",
variants: {
status: {
danger: { background: "red" },
warning: { background: "yellow" },
}
},
})
// Usage
<Button>Click me</Button>
<Button status="danger">Click me carefully</Button>
<Button status="warning">Click me?</Button>
\`\`\`
### parts + \`render()\`
\`\`\`typescript
export const Profile = define("div", {
padding: 50,
background: "red",
parts: {
Header: { display: "flex" },
Avatar: { base: "img", width: 50 },
Bio: { color: "gray" },
},
variants: {
size: {
small: {
parts: { Avatar: { width: 20 } },
},
},
},
render({ props, parts: { Root, Header, Avatar, Bio } }) {
return (
<Root>
<Header>
<Avatar src={props.pic} />
<Bio>{props.bio}</Bio>
</Header>
</Root>
)
},
})
// Usage:
import { Profile } from "./whatever"
console.log(<Profile pic={user.pic} bio={user.bio} />)
console.log(<Profile size="small" pic={user.pic} bio={user.bio} />)
\`\`\`
### selectors
Use \`selectors\` to write custom CSS selectors. Reference the current
element with \`&\` and other parts with \`@PartName\`:
\`\`\`tsx
const Checkbox = define("Checkbox", {
parts: {
Input: {
base: "input[type=checkbox]",
display: "none",
},
Label: {
base: "label",
padding: 10,
cursor: "pointer",
color: "gray",
selectors: {
// style Label when Input is checked
"@Input:checked + &": {
color: "green",
fontWeight: "bold",
},
// style Label when Input is disabled
"@Input:disabled + &": {
opacity: 0.5,
cursor: "not-allowed",
},
},
},
},
render({ props, parts: { Root, Input, Label } }) {
return (
<Root>
<Label>
<Input checked={props.checked} />
{props.label}
</Label>
</Root>
)
},
})
// Usage
<Checkbox label="Agree to terms" checked />
\`\`\`
## themes
built-in support for CSS variables with full type safety:
\`\`\`tsx
// themes.tsx - Define your themes
import { createThemes } from "forge"
export const theme = createThemes({
dark: {
bgColor: "#0a0a0a",
fgColor: "#00ff00",
sm: 12,
lg: 24,
},
light: {
bgColor: "#f5f5f0",
fgColor: "#0a0a0a",
sm: 12,
lg: 24,
},
})
// Use theme() in your components
import { define } from "forge"
import { theme } from "./themes"
const Button = define("Button", {
padding: theme("spacing-sm"),
background: theme("colors-bg"),
color: theme("colors-fg"),
})
\`\`\`
Theme switching is done via the \`data-theme\` attribute:
\`\`\`tsx
// Toggle between themes
document.body.setAttribute("data-theme", "dark")
document.body.setAttribute("data-theme", "light")
\`\`\`
The \`theme()\` function is fully typed based on your theme keys, giving
you autocomplete and type checking throughout your codebase.
## scopes
Sometimes you want your parts named things like ButtonRow, ButtonCell,
ButtonTable, etc, but all those Button's are repetitive:
\`\`\`typescript
const { define } = createScope("Button")
// css class becomes "Button"
const Button = define("Root", {
// becomes "Button"
// ...
})
// css class becomes "ButtonRow"
const ButtonRow = define("Row", {
// ...
})
// css class becomes "ButtonContainer"
const ButtonContainer = define("Container", {
// ...
})
\`\`\`
`