FORGE
This commit is contained in:
commit
f92e7af18d
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# dependencies (bun install)
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# output
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# code coverage
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.eslintcache
|
||||||
|
.cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
66
README.md
Normal file
66
README.md
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
# forge
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { define } from "forge"
|
||||||
|
|
||||||
|
export const Button = define("button", {
|
||||||
|
layout: {
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
look: {
|
||||||
|
background: "blue",
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
kind: {
|
||||||
|
danger: { look: { background: "red" } },
|
||||||
|
warning: { look: { background: "yellow" } },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
<Button>Click me</Button>
|
||||||
|
<Button kind="danger">Click me carefully</Button>
|
||||||
|
<Button kind="warning">Click me?</Button>
|
||||||
|
|
||||||
|
export const Profile = define("div", {
|
||||||
|
layout: {
|
||||||
|
padding: 50,
|
||||||
|
},
|
||||||
|
look: {
|
||||||
|
background: "red",
|
||||||
|
},
|
||||||
|
|
||||||
|
parts: {
|
||||||
|
header: { layout: { display: "flex" } },
|
||||||
|
avatar: { layout: { width: 50 } },
|
||||||
|
bio: { look: { color: "gray" } },
|
||||||
|
},
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
small: {
|
||||||
|
parts: { avatar: { layout: { 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'
|
||||||
|
|
||||||
|
<Profile pic={user.pic} bio={user.bio} />
|
||||||
|
```
|
||||||
31
bun.lock
Normal file
31
bun.lock
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "forge",
|
||||||
|
"dependencies": {
|
||||||
|
"hono": "^4.11.3",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
150
examples/button.tsx
Normal file
150
examples/button.tsx
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
import { define } from '../src'
|
||||||
|
import { Layout, ExampleSection } from './helpers'
|
||||||
|
|
||||||
|
const Button = define('Button', {
|
||||||
|
base: 'button',
|
||||||
|
|
||||||
|
layout: {
|
||||||
|
padding: "12px 24px",
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
|
||||||
|
look: {
|
||||||
|
background: "#3b82f6",
|
||||||
|
color: "white",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: 8,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 600,
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
userSelect: "none",
|
||||||
|
boxShadow: "0 4px 6px rgba(59, 130, 246, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1)",
|
||||||
|
transform: "translateY(0)",
|
||||||
|
},
|
||||||
|
|
||||||
|
states: {
|
||||||
|
":not(:disabled):hover": {
|
||||||
|
look: {
|
||||||
|
transform: 'translateY(-2px) !important',
|
||||||
|
filter: 'brightness(1.05)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
":not(:disabled):active": {
|
||||||
|
look: {
|
||||||
|
transform: 'translateY(1px) !important',
|
||||||
|
boxShadow: '0 2px 3px rgba(0, 0, 0, 0.2) !important'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
intent: {
|
||||||
|
primary: {
|
||||||
|
look: {
|
||||||
|
background: "#3b82f6",
|
||||||
|
color: "white",
|
||||||
|
boxShadow: "0 4px 6px rgba(59, 130, 246, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
look: {
|
||||||
|
background: "#f3f4f6",
|
||||||
|
color: "#374151",
|
||||||
|
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
look: {
|
||||||
|
background: "#ef4444",
|
||||||
|
color: "white",
|
||||||
|
boxShadow: "0 4px 6px rgba(239, 68, 68, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ghost: {
|
||||||
|
look: {
|
||||||
|
background: "transparent",
|
||||||
|
color: "#aaa",
|
||||||
|
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.2), 0 2px 4px rgba(0, 0, 0, 0.1)",
|
||||||
|
border: "1px solid #eee",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
small: {
|
||||||
|
layout: {
|
||||||
|
padding: "8px 16px",
|
||||||
|
},
|
||||||
|
look: {
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
layout: {
|
||||||
|
padding: "16px 32px",
|
||||||
|
},
|
||||||
|
look: {
|
||||||
|
fontSize: 18,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
look: {
|
||||||
|
opacity: 0.5,
|
||||||
|
cursor: "not-allowed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const ButtonRow = define('ButtonRow', {
|
||||||
|
layout: {
|
||||||
|
display: 'flex',
|
||||||
|
gap: 16,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
alignItems: 'center',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ButtonExamplesPage = () => (
|
||||||
|
<Layout title="Forge Button Component Examples">
|
||||||
|
<ExampleSection title="Intents">
|
||||||
|
<ButtonRow>
|
||||||
|
<Button intent="primary">Primary</Button>
|
||||||
|
<Button intent="secondary">Secondary</Button>
|
||||||
|
<Button intent="danger">Danger</Button>
|
||||||
|
<Button intent="ghost">Ghost</Button>
|
||||||
|
</ButtonRow>
|
||||||
|
</ExampleSection >
|
||||||
|
|
||||||
|
<ExampleSection title="Sizes">
|
||||||
|
<ButtonRow>
|
||||||
|
<Button size="small">Small Button</Button>
|
||||||
|
<Button>Medium Button</Button>
|
||||||
|
<Button size="large">Large Button</Button>
|
||||||
|
</ButtonRow>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
<ExampleSection title="Sizes">
|
||||||
|
<ButtonRow>
|
||||||
|
<Button intent="primary" size="small">Small Primary</Button>
|
||||||
|
<Button intent="secondary" size="small">Small Secondary</Button>
|
||||||
|
<Button intent="danger" size="large">Large Danger</Button>
|
||||||
|
<Button intent="ghost" size="large">Large Ghost</Button>
|
||||||
|
</ButtonRow>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
<ExampleSection title="Disabled">
|
||||||
|
<ButtonRow>
|
||||||
|
<Button disabled>Default Disabled</Button>
|
||||||
|
<Button intent="primary" disabled>Primary Disabled</Button>
|
||||||
|
<Button intent="secondary" disabled>Secondary Disabled</Button>
|
||||||
|
<Button intent="danger" disabled>Danger Disabled</Button>
|
||||||
|
<Button intent="ghost" disabled>Ghost Disabled</Button>
|
||||||
|
</ButtonRow>
|
||||||
|
</ExampleSection>
|
||||||
|
</Layout >
|
||||||
|
)
|
||||||
78
examples/helpers.tsx
Normal file
78
examples/helpers.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { define, Styles } from '../src'
|
||||||
|
|
||||||
|
export const Body = define('Body', {
|
||||||
|
base: 'body',
|
||||||
|
layout: {
|
||||||
|
margin: 0,
|
||||||
|
padding: '40px 20px',
|
||||||
|
|
||||||
|
},
|
||||||
|
look: {
|
||||||
|
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
||||||
|
background: '#f3f4f6',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const Container = define('Container', {
|
||||||
|
layout: {
|
||||||
|
maxWidth: 1200,
|
||||||
|
margin: '0 auto'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Header = define('Header', {
|
||||||
|
base: 'h1',
|
||||||
|
layout: {
|
||||||
|
marginBottom: 40,
|
||||||
|
},
|
||||||
|
look: {
|
||||||
|
color: '#111827'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ExampleSection = define('ExampleSection', {
|
||||||
|
layout: {
|
||||||
|
marginBottom: 40
|
||||||
|
},
|
||||||
|
parts: {
|
||||||
|
Header: {
|
||||||
|
base: 'h2',
|
||||||
|
layout: {
|
||||||
|
marginBottom: 16
|
||||||
|
},
|
||||||
|
look: {
|
||||||
|
color: '#374151',
|
||||||
|
fontSize: 18
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render({ props, parts: { Root, Header } }) {
|
||||||
|
return (
|
||||||
|
<Root>
|
||||||
|
<Header>{props.title}</Header>
|
||||||
|
{props.children}
|
||||||
|
</Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Layout = define({
|
||||||
|
render({ props }) {
|
||||||
|
return (
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{props.title}</title>
|
||||||
|
<Styles />
|
||||||
|
</head>
|
||||||
|
<Body>
|
||||||
|
<Container>
|
||||||
|
<Header>{props.title}</Header>
|
||||||
|
{props.children}
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
77
examples/index.tsx
Normal file
77
examples/index.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
export const IndexPage = () => (
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Forge Examples</title>
|
||||||
|
<style dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 40px 20px;
|
||||||
|
background: #f9fafb;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #111827;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
.examples-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
}
|
||||||
|
.example-card {
|
||||||
|
background: white;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.example-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
.example-card h2 {
|
||||||
|
color: #111827;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
.example-card p {
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
`}} />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Forge Examples</h1>
|
||||||
|
<p>Explore component examples built with Forge</p>
|
||||||
|
|
||||||
|
<div class="examples-grid">
|
||||||
|
<a href="/profile" class="example-card">
|
||||||
|
<h2>Profile Card</h2>
|
||||||
|
<p>User profile component with variants for size, theme, and verified status</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/buttons" class="example-card">
|
||||||
|
<h2>Buttons</h2>
|
||||||
|
<p>Button component with intent, size, and disabled variants</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
277
examples/profile.tsx
Normal file
277
examples/profile.tsx
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
import { define } from '../src'
|
||||||
|
import { Layout, ExampleSection } from './helpers'
|
||||||
|
|
||||||
|
const UserProfile = define('UserProfile', {
|
||||||
|
base: 'div',
|
||||||
|
|
||||||
|
layout: {
|
||||||
|
padding: 24,
|
||||||
|
maxWidth: 600,
|
||||||
|
margin: "0 auto",
|
||||||
|
},
|
||||||
|
|
||||||
|
look: {
|
||||||
|
background: "white",
|
||||||
|
borderRadius: 12,
|
||||||
|
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
|
||||||
|
},
|
||||||
|
|
||||||
|
parts: {
|
||||||
|
Header: {
|
||||||
|
layout: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 16,
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Avatar: {
|
||||||
|
base: 'img',
|
||||||
|
layout: {
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
},
|
||||||
|
look: {
|
||||||
|
borderRadius: "50%",
|
||||||
|
objectFit: "cover",
|
||||||
|
border: "3px solid #e5e7eb",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Info: {
|
||||||
|
layout: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Name: {
|
||||||
|
layout: {
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
look: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "#111827",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Handle: {
|
||||||
|
look: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#6b7280",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Bio: {
|
||||||
|
layout: {
|
||||||
|
marginBottom: 16,
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
look: {
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 1.6,
|
||||||
|
color: "#374151",
|
||||||
|
wordWrap: "break-word",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Stats: {
|
||||||
|
layout: {
|
||||||
|
display: "flex",
|
||||||
|
gap: 24,
|
||||||
|
paddingTop: 16,
|
||||||
|
},
|
||||||
|
look: {
|
||||||
|
borderTop: "1px solid #e5e7eb",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Stat: {
|
||||||
|
layout: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
StatValue: {
|
||||||
|
look: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "#111827",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
StatLabel: {
|
||||||
|
look: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#6b7280",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
compact: {
|
||||||
|
layout: { padding: 16, maxWidth: 300 },
|
||||||
|
parts: {
|
||||||
|
Avatar: { layout: { width: 48, height: 48 } },
|
||||||
|
Name: { look: { fontSize: 16 } },
|
||||||
|
Bio: { look: { fontSize: 13 } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
skinny: {
|
||||||
|
layout: { padding: 20, maxWidth: 125 },
|
||||||
|
parts: {
|
||||||
|
Header: {
|
||||||
|
layout: {
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 12,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Avatar: { layout: { width: 80, height: 80 } },
|
||||||
|
Info: { layout: { flex: 0, width: "100%" } },
|
||||||
|
Name: { look: { textAlign: "center", fontSize: 18 } },
|
||||||
|
Handle: { look: { textAlign: "center" } },
|
||||||
|
Bio: { look: { textAlign: "center", fontSize: 13 } },
|
||||||
|
Stats: {
|
||||||
|
layout: {
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 16,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Stat: { layout: { alignItems: "center" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
layout: { padding: 32, maxWidth: 800 },
|
||||||
|
parts: {
|
||||||
|
Avatar: { layout: { width: 96, height: 96 } },
|
||||||
|
Name: { look: { fontSize: 24 } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verified: {
|
||||||
|
parts: {
|
||||||
|
Avatar: {
|
||||||
|
look: {
|
||||||
|
border: "3px solid #3b82f6",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
dark: {
|
||||||
|
look: {
|
||||||
|
background: "#1f2937",
|
||||||
|
},
|
||||||
|
parts: {
|
||||||
|
Name: { look: { color: "#f9fafb" } },
|
||||||
|
Handle: { look: { color: "#9ca3af" } },
|
||||||
|
Bio: { look: { color: "#d1d5db" } },
|
||||||
|
Stats: { look: { borderTop: "1px solid #374151" } },
|
||||||
|
StatValue: { look: { color: "#f9fafb" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
render({ props, parts: { Root, Header, Avatar, Info, Name, Handle, Bio, Stats, Stat, StatValue, StatLabel } }) {
|
||||||
|
return (
|
||||||
|
<Root>
|
||||||
|
<Header>
|
||||||
|
<Avatar src={props.avatarUrl} alt={props.name} />
|
||||||
|
<Info>
|
||||||
|
<Name>
|
||||||
|
{props.name}
|
||||||
|
{props.verified && " ✓"}
|
||||||
|
</Name>
|
||||||
|
<Handle>@{props.username}</Handle>
|
||||||
|
</Info>
|
||||||
|
</Header>
|
||||||
|
|
||||||
|
<Bio>{props.bio}</Bio>
|
||||||
|
|
||||||
|
<Stats>
|
||||||
|
<Stat>
|
||||||
|
<StatValue>{props.followers?.toLocaleString() || 0}</StatValue>
|
||||||
|
<StatLabel>Followers</StatLabel>
|
||||||
|
</Stat>
|
||||||
|
<Stat>
|
||||||
|
<StatValue>{props.following?.toLocaleString() || 0}</StatValue>
|
||||||
|
<StatLabel>Following</StatLabel>
|
||||||
|
</Stat>
|
||||||
|
<Stat>
|
||||||
|
<StatValue>{props.posts?.toLocaleString() || 0}</StatValue>
|
||||||
|
<StatLabel>Posts</StatLabel>
|
||||||
|
</Stat>
|
||||||
|
</Stats>
|
||||||
|
</Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Export the full example page
|
||||||
|
export const ProfileExamplesPage = () => (
|
||||||
|
<Layout title="Forge Profile Examples">
|
||||||
|
<ExampleSection title="Default Profile">
|
||||||
|
<UserProfile
|
||||||
|
name="Sarah Chen"
|
||||||
|
username="sarahc"
|
||||||
|
avatarUrl="https://i.pravatar.cc/150?img=5"
|
||||||
|
bio="Designer & developer. Building beautiful interfaces. Based in SF."
|
||||||
|
followers={1234}
|
||||||
|
following={567}
|
||||||
|
posts={89}
|
||||||
|
/>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
<ExampleSection title="Compact Variant">
|
||||||
|
<UserProfile
|
||||||
|
size="compact"
|
||||||
|
name="Alex Rivera"
|
||||||
|
username="arivera"
|
||||||
|
avatarUrl="https://i.pravatar.cc/150?img=12"
|
||||||
|
bio="Creative director. Coffee enthusiast."
|
||||||
|
followers={456}
|
||||||
|
following={234}
|
||||||
|
posts={45}
|
||||||
|
/>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
<ExampleSection title="Skinny Variant">
|
||||||
|
<UserProfile
|
||||||
|
size="skinny"
|
||||||
|
name="Taylor Kim"
|
||||||
|
username="tkim"
|
||||||
|
avatarUrl="https://i.pravatar.cc/150?img=3"
|
||||||
|
bio="Minimalist designer."
|
||||||
|
followers={2890}
|
||||||
|
following={125}
|
||||||
|
posts={312}
|
||||||
|
/>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
<ExampleSection title="Verified User (Dark Theme)">
|
||||||
|
<UserProfile
|
||||||
|
verified={true}
|
||||||
|
theme="dark"
|
||||||
|
name="Jordan Smith"
|
||||||
|
username="jordansmith"
|
||||||
|
avatarUrl="https://i.pravatar.cc/150?img=8"
|
||||||
|
bio="Tech entrepreneur. Angel investor. Sharing insights on startups and innovation."
|
||||||
|
followers={52300}
|
||||||
|
following={892}
|
||||||
|
posts={1240}
|
||||||
|
/>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
<ExampleSection title="Large Verified Profile">
|
||||||
|
<UserProfile
|
||||||
|
size="large"
|
||||||
|
verified={true}
|
||||||
|
name="Morgan Taylor"
|
||||||
|
username="mtaylor"
|
||||||
|
avatarUrl="https://i.pravatar.cc/150?img=20"
|
||||||
|
bio="Product designer with 10 years of experience. Passionate about accessibility and inclusive design. Speaker at design conferences."
|
||||||
|
followers={8900}
|
||||||
|
following={234}
|
||||||
|
posts={567}
|
||||||
|
/>
|
||||||
|
</ExampleSection>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
17
package.json
Normal file
17
package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"name": "forge",
|
||||||
|
"module": "src/index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run --hot server.tsx"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"hono": "^4.11.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
23
server.tsx
Normal file
23
server.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Hono } from 'hono'
|
||||||
|
import { IndexPage } from './examples/index'
|
||||||
|
import { ProfileExamplesPage } from './examples/profile'
|
||||||
|
import { ButtonExamplesPage } from './examples/button'
|
||||||
|
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
app.get('/', (c) => {
|
||||||
|
return c.html(<IndexPage />)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/profile', (c) => {
|
||||||
|
return c.html(<ProfileExamplesPage />)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/buttons', (c) => {
|
||||||
|
return c.html(<ButtonExamplesPage />)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default {
|
||||||
|
port: 3300,
|
||||||
|
fetch: app.fetch,
|
||||||
|
}
|
||||||
126
src/index.tsx
Normal file
126
src/index.tsx
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
import type { JSX } from 'hono/jsx'
|
||||||
|
import { type TagDef, UnitlessProps } from './types'
|
||||||
|
|
||||||
|
const styles: Record<string, Record<string, string>> = {}
|
||||||
|
export const Styles = () => <style dangerouslySetInnerHTML={{ __html: stylesToCSS(styles) }} />
|
||||||
|
|
||||||
|
// turns style object into string CSS definition
|
||||||
|
function stylesToCSS(styles: Record<string, Record<string, string>>): string {
|
||||||
|
let out: string[] = []
|
||||||
|
|
||||||
|
for (const [selector, style] of Object.entries(styles)) {
|
||||||
|
out.push(`.${selector} {`)
|
||||||
|
for (const [name, value] of Object.entries(style)) {
|
||||||
|
out.push(` ${name}: ${value};`)
|
||||||
|
}
|
||||||
|
out.push(`}\n`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates a CSS class name
|
||||||
|
function makeClassName(baseName: string, partName?: string, variantName?: string, variantKey?: string): string {
|
||||||
|
const cls = partName ? `${baseName}_${partName}` : baseName
|
||||||
|
|
||||||
|
if (variantName && variantKey) {
|
||||||
|
return cls + `.${variantName}-${variantKey}`
|
||||||
|
} else {
|
||||||
|
return cls
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'fontSize' => 'font-size'
|
||||||
|
function camelToDash(name: string): string {
|
||||||
|
let out = ''
|
||||||
|
|
||||||
|
for (const letter of name.split(''))
|
||||||
|
out += letter.toUpperCase() === letter ? `-${letter.toLowerCase()}` : letter
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// turns a TagDef into a JSX style object
|
||||||
|
function makeStyle(def: TagDef) {
|
||||||
|
const style: Record<string, string> = {}
|
||||||
|
|
||||||
|
for (const [name, value] of Object.entries(Object.assign({}, def.layout ?? {}, def.look ?? {})))
|
||||||
|
style[camelToDash(name)] = `${typeof value === 'number' && !UnitlessProps.has(name) ? `${value}px` : value}`
|
||||||
|
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
|
||||||
|
// turns a TagDef into a JSX component
|
||||||
|
function makeComponent(baseName: string, rootDef: TagDef, rootProps: Record<string, any>, partName?: string) {
|
||||||
|
const def = partName ? rootDef.parts?.[partName]! : rootDef
|
||||||
|
const base = def.base ?? 'div'
|
||||||
|
const Tag = (base) as keyof JSX.IntrinsicElements
|
||||||
|
|
||||||
|
const classNames = [makeClassName(baseName, partName)]
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(rootProps)) {
|
||||||
|
const variantConfig = rootDef.variants?.[key]
|
||||||
|
if (!variantConfig) continue
|
||||||
|
|
||||||
|
const variantName = key
|
||||||
|
const variantKey = value
|
||||||
|
|
||||||
|
let variantDef: TagDef | undefined
|
||||||
|
if ('parts' in variantConfig || 'layout' in variantConfig || 'look' in variantConfig) {
|
||||||
|
if (value === true) variantDef = variantConfig as TagDef
|
||||||
|
} else {
|
||||||
|
variantDef = (variantConfig as Record<string, TagDef>)[value as string]
|
||||||
|
}
|
||||||
|
if (!variantDef) continue
|
||||||
|
|
||||||
|
classNames.push(`${variantName}-${variantKey}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ({ children, ...props }: { children: any, [key: string]: any }) =>
|
||||||
|
<Tag class={classNames.join(' ')} {...props}>{children}</Tag>
|
||||||
|
}
|
||||||
|
|
||||||
|
// adds CSS styles for tag definition
|
||||||
|
function registerStyles(name: string, def: TagDef) {
|
||||||
|
const rootClassName = makeClassName(name)
|
||||||
|
styles[rootClassName] ??= makeStyle(def)
|
||||||
|
|
||||||
|
for (const [partName, partDef] of Object.entries(def.parts ?? {})) {
|
||||||
|
const partClassName = makeClassName(name, partName)
|
||||||
|
styles[partClassName] ??= makeStyle(partDef)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [variantName, variantConfig] of Object.entries(def.variants ?? {})) {
|
||||||
|
for (const [variantKey, variantDef] of Object.entries(variantConfig as Record<string, TagDef>)) {
|
||||||
|
const className = makeClassName(name, undefined, variantName, variantKey)
|
||||||
|
styles[className] ??= makeStyle({ layout: variantDef.layout, look: variantDef.look })
|
||||||
|
|
||||||
|
for (const [partName, partDef] of Object.entries(variantDef.parts ?? {})) {
|
||||||
|
const partClassName = makeClassName(name, partName, variantName, variantKey)
|
||||||
|
styles[partClassName] ??= makeStyle(partDef)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let anonComponents = 1
|
||||||
|
|
||||||
|
// the main event
|
||||||
|
export function define(nameOrDef: string | TagDef, defIfNamed?: TagDef) {
|
||||||
|
const name = defIfNamed ? (nameOrDef as string) : `Def${anonComponents++}`
|
||||||
|
const def = defIfNamed ?? nameOrDef as TagDef
|
||||||
|
|
||||||
|
registerStyles(name, def)
|
||||||
|
|
||||||
|
return (props: Record<string, any>) => {
|
||||||
|
const parts: Record<string, Function> = {}
|
||||||
|
|
||||||
|
for (const [part] of Object.entries(def.parts ?? {}))
|
||||||
|
parts[part] = makeComponent(name, def, props, part)
|
||||||
|
|
||||||
|
parts.Root = makeComponent(name, def, props)
|
||||||
|
return def.render ? def.render({ props, parts }) : <parts.Root {...props}>{props.children}</parts.Root>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
define.Styles = Styles
|
||||||
121
src/types.ts
Normal file
121
src/types.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
export type TagDef = {
|
||||||
|
className?: string
|
||||||
|
base?: string
|
||||||
|
layout?: Layout
|
||||||
|
look?: Look
|
||||||
|
states?: Record<string, TagDef>,
|
||||||
|
parts?: Record<string, TagDef>
|
||||||
|
variants?: Record<string, TagDef | Record<string, TagDef>>
|
||||||
|
render?: (obj: any) => any
|
||||||
|
}
|
||||||
|
|
||||||
|
type Layout = {
|
||||||
|
alignContent?: string
|
||||||
|
alignItems?: string
|
||||||
|
alignSelf?: string
|
||||||
|
bottom?: number | string
|
||||||
|
display?: string
|
||||||
|
flex?: number | string
|
||||||
|
flexBasis?: number | string
|
||||||
|
flexDirection?: string
|
||||||
|
flexGrow?: number
|
||||||
|
flexShrink?: number
|
||||||
|
flexWrap?: string
|
||||||
|
gap?: number | string
|
||||||
|
gridAutoFlow?: string
|
||||||
|
gridColumn?: string
|
||||||
|
gridGap?: number | string
|
||||||
|
gridRow?: string
|
||||||
|
gridTemplateColumns?: string
|
||||||
|
gridTemplateRows?: string
|
||||||
|
height?: number | string
|
||||||
|
justifyContent?: string
|
||||||
|
justifyItems?: string
|
||||||
|
justifySelf?: string
|
||||||
|
left?: number | string
|
||||||
|
margin?: number | string
|
||||||
|
marginBottom?: number | string
|
||||||
|
marginLeft?: number | string
|
||||||
|
marginRight?: number | string
|
||||||
|
marginTop?: number | string
|
||||||
|
maxHeight?: number | string
|
||||||
|
maxWidth?: number | string
|
||||||
|
minHeight?: number | string
|
||||||
|
minWidth?: number | string
|
||||||
|
order?: number
|
||||||
|
overflow?: string
|
||||||
|
overflowX?: string
|
||||||
|
overflowY?: string
|
||||||
|
padding?: number | string
|
||||||
|
paddingBottom?: number | string
|
||||||
|
paddingLeft?: number | string
|
||||||
|
paddingRight?: number | string
|
||||||
|
paddingTop?: number | string
|
||||||
|
position?: string
|
||||||
|
right?: number | string
|
||||||
|
top?: number | string
|
||||||
|
width?: number | string
|
||||||
|
zIndex?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type Look = {
|
||||||
|
animation?: string
|
||||||
|
backdropFilter?: string
|
||||||
|
background?: string
|
||||||
|
backgroundColor?: string
|
||||||
|
border?: string
|
||||||
|
borderBottom?: string
|
||||||
|
borderColor?: string
|
||||||
|
borderLeft?: string
|
||||||
|
borderRadius?: number | string
|
||||||
|
borderRight?: string
|
||||||
|
borderStyle?: string
|
||||||
|
borderTop?: string
|
||||||
|
borderWidth?: number | string
|
||||||
|
boxShadow?: string
|
||||||
|
color?: string
|
||||||
|
cursor?: string
|
||||||
|
filter?: string
|
||||||
|
fontFamily?: string
|
||||||
|
fontSize?: number | string
|
||||||
|
fontStyle?: string
|
||||||
|
fontWeight?: number | string
|
||||||
|
letterSpacing?: number | string
|
||||||
|
lineHeight?: number | string
|
||||||
|
mixBlendMode?: string
|
||||||
|
objectFit?: string
|
||||||
|
opacity?: number
|
||||||
|
outline?: string
|
||||||
|
outlineColor?: string
|
||||||
|
outlineOffset?: number | string
|
||||||
|
outlineStyle?: string
|
||||||
|
outlineWidth?: number | string
|
||||||
|
pointerEvents?: string
|
||||||
|
textAlign?: string
|
||||||
|
textDecoration?: string
|
||||||
|
textOverflow?: string
|
||||||
|
textShadow?: string
|
||||||
|
textTransform?: string
|
||||||
|
transform?: string
|
||||||
|
transition?: string
|
||||||
|
userSelect?: string
|
||||||
|
visibility?: string
|
||||||
|
whiteSpace?: string
|
||||||
|
wordWrap?: string
|
||||||
|
overflowWrap?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UnitlessProps = new Set([
|
||||||
|
'animationIterationCount',
|
||||||
|
'columnCount',
|
||||||
|
'flex',
|
||||||
|
'flexGrow',
|
||||||
|
'flexShrink',
|
||||||
|
'fontWeight',
|
||||||
|
'lineHeight',
|
||||||
|
'opacity',
|
||||||
|
'order',
|
||||||
|
'orphans',
|
||||||
|
'widows',
|
||||||
|
'zIndex'
|
||||||
|
])
|
||||||
30
tsconfig.json
Normal file
30
tsconfig.json
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "hono/jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user