4.7 KiB
4.7 KiB
⚒️ forge
╔═╝╔═║╔═║╔═╝╔═╝
╔═╝║ ║╔╔╝║ ║╔═╝
╝ ══╝╝ ╝══╝══╝
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.
- All 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 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
import { define } from "forge"
export const Button = define("button", {
base: "button",
padding: 20,
background: "blue",
})
// Usage
<Button>Click me</Button>
variants
import { define } from "forge"
export const Button = define("button", {
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()
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:
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:
// 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:
// 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:
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", {
// ...
})
see it
Check out the examples/ dir and view them at http://localhost:3300 by
cloning this repo and running the local web server:
bun install
bun dev
open http://localhost:3300