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
🌙 dark
{`╔═╝╔═║╔═║╔═╝╔═╝
╔═╝║ ║╔╔╝║ ║╔═╝
╝ ══╝╝ ╝══╝══╝`}
SSR demos →
SPA demos →
)
}
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
\`\`\`
### 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
\`\`\`
### 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 (
{props.bio}
)
},
})
// Usage:
import { Profile } from "./whatever"
console.log( )
console.log( )
\`\`\`
### 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 (
)
},
})
// Usage
\`\`\`
## 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", {
// ...
})
\`\`\`
`