forked from defunkt/forge
readme on landing page
This commit is contained in:
parent
85844c9b44
commit
992bddc769
10
bun.lock
10
bun.lock
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"configVersion": 1,
|
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "forge",
|
"name": "forge",
|
||||||
|
|
@ -9,6 +8,9 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
|
"@types/prismjs": "^1.26.5",
|
||||||
|
"prismjs": "^1.30.0",
|
||||||
|
"snarkdown": "^2.0.0",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
|
|
@ -20,10 +22,16 @@
|
||||||
|
|
||||||
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
"@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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
|
||||||
547
examples/landing.tsx
Normal file
547
examples/landing.tsx
Normal file
|
|
@ -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(/<pre class="code[^"]*"><code class="language-(\w+)">([\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 `<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", {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
\`\`\`
|
||||||
|
`
|
||||||
|
|
@ -108,7 +108,7 @@ const Link = define('Link', {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const Nav = define('Nav', {
|
const Nav = define({
|
||||||
base: 'nav',
|
base: 'nav',
|
||||||
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
import { createScope, Styles } from '../../src'
|
|
||||||
import { theme } from './themes'
|
|
||||||
|
|
||||||
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', {
|
|
||||||
marginTop: theme('spacing-xxl'),
|
|
||||||
paddingTop: theme('spacing-xl'),
|
|
||||||
borderTop: `1px solid ${theme('colors-border')}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
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'),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export const LandingPage = () => {
|
|
||||||
const themeScript = `
|
|
||||||
function switchTheme(themeName) {
|
|
||||||
document.body.setAttribute('data-theme', themeName)
|
|
||||||
localStorage.setItem('theme', themeName)
|
|
||||||
}
|
|
||||||
|
|
||||||
window.switchTheme = switchTheme
|
|
||||||
|
|
||||||
// Load saved theme or default to dark
|
|
||||||
const savedTheme = localStorage.getItem('theme') || 'dark'
|
|
||||||
document.body.setAttribute('data-theme', savedTheme)
|
|
||||||
`
|
|
||||||
|
|
||||||
return (
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>forge</title>
|
|
||||||
<Styles />
|
|
||||||
</head>
|
|
||||||
<Page data-theme="dark">
|
|
||||||
<Container>
|
|
||||||
<Pre>{`╔═╝╔═║╔═║╔═╝╔═╝
|
|
||||||
╔═╝║ ║╔╔╝║ ║╔═╝
|
|
||||||
╝ ══╝╝ ╝══╝══╝`}</Pre>
|
|
||||||
|
|
||||||
<P>
|
|
||||||
Typed, local, variant-driven CSS. No globals, no selector hell, no inline styles.
|
|
||||||
Built for TSX. Compiles to real CSS.
|
|
||||||
</P>
|
|
||||||
|
|
||||||
<P>
|
|
||||||
CSS is hostile to humans at scale. Forge fixes that by making styles local,
|
|
||||||
typed, and composable. Parts replace selectors. Variants replace inline styles.
|
|
||||||
Everything deterministic.
|
|
||||||
</P>
|
|
||||||
|
|
||||||
<LinkSection>
|
|
||||||
<Link href="/ssr">SSR demos →</Link>
|
|
||||||
<Link href="/spa">SPA demos →</Link>
|
|
||||||
</LinkSection>
|
|
||||||
</Container>
|
|
||||||
<script dangerouslySetInnerHTML={{ __html: themeScript }}></script>
|
|
||||||
</Page>
|
|
||||||
</html>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -8,7 +8,10 @@
|
||||||
"build:spa": "bun build examples/spa/index.tsx --outfile dist/spa.js --target browser"
|
"build:spa": "bun build examples/spa/index.tsx --outfile dist/spa.js --target browser"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest"
|
"@types/bun": "latest",
|
||||||
|
"@types/prismjs": "^1.26.5",
|
||||||
|
"prismjs": "^1.30.0",
|
||||||
|
"snarkdown": "^2.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
import { IndexPage, ProfileExamplesPage, ButtonExamplesPage, NavigationExamplesPage, FormExamplesPage } from './examples/ssr/pages'
|
import { IndexPage, ProfileExamplesPage, ButtonExamplesPage, NavigationExamplesPage, FormExamplesPage } from './examples/ssr/pages'
|
||||||
import { LandingPage } from './examples/ssr/landing'
|
import { LandingPage } from './examples/landing'
|
||||||
import { stylesToCSS } from './src'
|
import { stylesToCSS } from './src'
|
||||||
|
|
||||||
const app = new Hono()
|
const app = new Hono()
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,8 @@ describe('define - basic functionality', () => {
|
||||||
const html2 = renderToString(Component2({}))
|
const html2 = renderToString(Component2({}))
|
||||||
|
|
||||||
// Should have different auto-generated names
|
// Should have different auto-generated names
|
||||||
expect(html1).toMatch(/class="Def\d+"/)
|
expect(html1).toMatch(/class="Div\d*"/)
|
||||||
expect(html2).toMatch(/class="Def\d+"/)
|
expect(html2).toMatch(/class="Div\d*"/)
|
||||||
expect(html1).not.toBe(html2)
|
expect(html1).not.toBe(html2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -891,7 +891,7 @@ describe('edge cases', () => {
|
||||||
const html = renderToString(Component({}))
|
const html = renderToString(Component({}))
|
||||||
|
|
||||||
expect(html).toBeDefined()
|
expect(html).toBeDefined()
|
||||||
expect(html).toMatch(/class="Def\d+"/)
|
expect(html).toMatch(/class="Div\d*"/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('handles definition with only parts', () => {
|
test('handles definition with only parts', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user