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