README+theme

This commit is contained in:
Chris Wanstrath 2025-12-29 13:17:19 -08:00
parent c561207128
commit f643f8b2eb
9 changed files with 415 additions and 375 deletions

View File

@ -1,37 +1,32 @@
# Forge # ⚒️ forge
## Why Forge? ```
╔═╝╔═║╔═║╔═╝╔═╝
╔═╝║ ║╔╔╝║ ║╔═╝
╝ ══╝╝ ╝══╝══╝
```
CSS is powerful, but hostile. ## overview
### Problems with CSS Forge is a typed, local, variant-driven way to organize CSS, built around TSX.
- Styles are **global and open** — anything can override anything. ## css problems
- Theres **no link** between a class in markup and its definition.
- Inline styles exist because theres **no structured way to vary styles per instance**.
- Overrides are silent — conflicts happen without feedback.
- Complex components require selector gymnastics and reach-in styling.
### What Forge Does Instead - 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.
- Styles are **local to components** and attached by generated handles, not strings. ## forge solutions
- **Parts** give components named sub-targets without selectors.
- **Variants** replace inline styles with typed, declarative parameters.
- Style composition is **deterministic** (known merge order, last-wins).
- Overlapping changes are **warned about in dev**, not silently ignored.
### What Forge Is - 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.
- Errors and feedback.
- A typed, local, variant-driven way to author CSS. ## examples
- A system that optimizes for **people typing at a keyboard**, not selectors in a cascade.
### What Forge Is Not
- Not a new component model.
- Not a new language.
- Not a CSS replacement — it compiles _to_ CSS, but removes the chaos.
Example:
```tsx ```tsx
import { define } from "forge" import { define } from "forge"
@ -43,7 +38,7 @@ export const Button = define("button", {
background: "blue", background: "blue",
variants: { variants: {
kind: { status: {
danger: { background: "red" }, danger: { background: "red" },
warning: { background: "yellow" }, warning: { background: "yellow" },
} }
@ -52,8 +47,8 @@ export const Button = define("button", {
// Usage // Usage
<Button>Click me</Button> <Button>Click me</Button>
<Button kind="danger">Click me carefully</Button> <Button status="danger">Click me carefully</Button>
<Button kind="warning">Click me?</Button> <Button status="warning">Click me?</Button>
export const Profile = define("div", { export const Profile = define("div", {
padding: 50, padding: 50,
@ -90,3 +85,14 @@ import { Profile } from './whatever'
<Profile pic={user.pic} bio={user.bio} /> <Profile pic={user.pic} bio={user.bio} />
``` ```
## 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
```

View File

@ -1,75 +1,88 @@
import { createScope } from '../src' import { createScope } from '../src'
import { ExampleSection } from './ssr/helpers' import { ExampleSection, theme } from './ssr/helpers'
const { define } = createScope('Button') const { define } = createScope('Button')
const Button = define('Root', { const Button = define('Root', {
base: 'button', base: 'button',
padding: "12px 24px", padding: `${theme.spacing.sm}px ${theme.spacing.lg}px`,
display: "inline-flex", display: "inline-flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
gap: 8, gap: theme.spacing.xs,
background: "#3b82f6", background: theme.colors.accent,
color: "white", color: theme.colors.bg,
border: "none", border: `1px solid ${theme.colors.accent}`,
borderRadius: 8, borderRadius: theme.radius.sm,
fontSize: 16, fontSize: 14,
fontWeight: 600, fontWeight: 400,
cursor: "pointer", cursor: "pointer",
transition: "all 0.2s ease", transition: "all 0.2s ease",
userSelect: "none", 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: { states: {
":not(:disabled):hover": { ":not(:disabled):hover": {
transform: 'translateY(-2px)', background: theme.colors.accentDim,
filter: 'brightness(1.05)' borderColor: theme.colors.accentDim,
}, },
":not(:disabled):active": { ":not(:disabled):active": {
transform: 'translateY(1px)', transform: 'translateY(1px)',
boxShadow: '0 2px 3px rgba(0, 0, 0, 0.2)'
}, },
}, },
variants: { variants: {
intent: { intent: {
primary: { primary: {
background: "#3b82f6", background: theme.colors.accent,
color: "white", color: theme.colors.bg,
boxShadow: "0 4px 6px rgba(59, 130, 246, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1)", border: `1px solid ${theme.colors.accent}`,
}, },
secondary: { secondary: {
background: "#f3f4f6", background: theme.colors.bgElevated,
color: "#374151", color: theme.colors.fg,
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06)", border: `1px solid ${theme.colors.border}`,
states: {
":not(:disabled):hover": {
borderColor: theme.colors.borderActive,
}
}
}, },
danger: { danger: {
background: "#ef4444", background: "#ff0000",
color: "white", color: theme.colors.bg,
boxShadow: "0 4px 6px rgba(239, 68, 68, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1)", border: "1px solid #ff0000",
states: {
":not(:disabled):hover": {
background: "#cc0000",
borderColor: "#cc0000",
}
}
}, },
ghost: { ghost: {
background: "transparent", background: "transparent",
color: "#aaa", color: theme.colors.fgMuted,
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.2), 0 2px 4px rgba(0, 0, 0, 0.1)", border: `1px solid ${theme.colors.border}`,
border: "1px solid #eee", states: {
":not(:disabled):hover": {
color: theme.colors.fg,
borderColor: theme.colors.borderActive,
}
}
}, },
}, },
size: { size: {
small: { small: {
padding: "8px 16px", padding: `${theme.spacing.xs}px ${theme.spacing.md}px`,
fontSize: 14, fontSize: 12,
}, },
large: { large: {
padding: "16px 32px", padding: `${theme.spacing.md}px ${theme.spacing.xl}px`,
fontSize: 18, fontSize: 16,
}, },
}, },
disabled: { disabled: {
opacity: 0.5, opacity: 0.3,
cursor: "not-allowed", cursor: "not-allowed",
}, },
}, },
@ -77,7 +90,7 @@ const Button = define('Root', {
const ButtonRow = define('Row', { const ButtonRow = define('Row', {
display: 'flex', display: 'flex',
gap: 16, gap: theme.spacing.md,
flexWrap: 'wrap', flexWrap: 'wrap',
alignItems: 'center', alignItems: 'center',
}) })

View File

@ -1,15 +1,15 @@
import { define } from '../src' import { define } from '../src'
import { ExampleSection } from './ssr/helpers' import { ExampleSection, theme } from './ssr/helpers'
const Input = define('Input', { const Input = define('Input', {
base: 'input', base: 'input',
padding: '12px 16px', padding: `${theme.spacing.sm}px ${theme.spacing.md}px`,
fontSize: 16, fontSize: 14,
border: '2px solid #e5e7eb', border: `1px solid ${theme.colors.border}`,
borderRadius: 8, borderRadius: theme.radius.sm,
background: 'white', background: theme.colors.bgElevated,
color: '#111827', color: theme.colors.fg,
transition: 'all 0.2s ease', transition: 'all 0.2s ease',
width: '100%', width: '100%',
boxSizing: 'border-box', boxSizing: 'border-box',
@ -17,12 +17,11 @@ const Input = define('Input', {
states: { states: {
':focus': { ':focus': {
outline: 'none', outline: 'none',
borderColor: '#3b82f6', borderColor: theme.colors.borderActive,
boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1)'
}, },
':disabled': { ':disabled': {
background: '#f3f4f6', background: theme.colors.bg,
color: '#9ca3af', color: theme.colors.fgDim,
cursor: 'not-allowed' cursor: 'not-allowed'
} }
}, },
@ -30,20 +29,18 @@ const Input = define('Input', {
variants: { variants: {
status: { status: {
error: { error: {
borderColor: '#ef4444', borderColor: '#ff0000',
states: { states: {
':focus': { ':focus': {
borderColor: '#ef4444', borderColor: '#ff0000',
boxShadow: '0 0 0 3px rgba(239, 68, 68, 0.1)'
} }
} }
}, },
success: { success: {
borderColor: '#10b981', borderColor: theme.colors.accent,
states: { states: {
':focus': { ':focus': {
borderColor: '#10b981', borderColor: theme.colors.accent,
boxShadow: '0 0 0 3px rgba(16, 185, 129, 0.1)'
} }
} }
} }
@ -54,12 +51,12 @@ const Input = define('Input', {
const Textarea = define('Textarea', { const Textarea = define('Textarea', {
base: 'textarea', base: 'textarea',
padding: '12px 16px', padding: `${theme.spacing.sm}px ${theme.spacing.md}px`,
fontSize: 16, fontSize: 14,
border: '2px solid #e5e7eb', border: `1px solid ${theme.colors.border}`,
borderRadius: 8, borderRadius: theme.radius.sm,
background: 'white', background: theme.colors.bgElevated,
color: '#111827', color: theme.colors.fg,
transition: 'all 0.2s ease', transition: 'all 0.2s ease',
width: '100%', width: '100%',
minHeight: 120, minHeight: 120,
@ -70,32 +67,31 @@ const Textarea = define('Textarea', {
states: { states: {
':focus': { ':focus': {
outline: 'none', outline: 'none',
borderColor: '#3b82f6', borderColor: theme.colors.borderActive,
boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1)'
} }
} }
}) })
const FormGroup = define('FormGroup', { const FormGroup = define('FormGroup', {
marginBottom: 24, marginBottom: theme.spacing.lg,
parts: { parts: {
Label: { Label: {
base: 'label', base: 'label',
display: 'block', display: 'block',
fontSize: 14, fontSize: 14,
fontWeight: 600, fontWeight: 400,
color: '#374151', color: theme.colors.fg,
marginBottom: 8 marginBottom: theme.spacing.xs
}, },
Helper: { Helper: {
fontSize: 13, fontSize: 12,
color: '#6b7280', color: theme.colors.fgMuted,
marginTop: 6 marginTop: 6
}, },
Error: { Error: {
fontSize: 13, fontSize: 12,
color: '#ef4444', color: '#ff0000',
marginTop: 6 marginTop: 6
} }
}, },
@ -116,29 +112,29 @@ const Checkbox = define('Checkbox', {
parts: { parts: {
Input: { Input: {
base: 'input[type=checkbox]', base: 'input[type=checkbox]',
width: 20, width: 18,
height: 20, height: 18,
cursor: 'pointer' cursor: 'pointer'
}, },
Label: { Label: {
base: 'label', base: 'label',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 12, gap: theme.spacing.sm,
cursor: 'pointer', cursor: 'pointer',
fontSize: 16, fontSize: 14,
color: '#374151', color: theme.colors.fgMuted,
states: { states: {
':hover': { ':hover': {
color: '#111827' color: theme.colors.fg
} }
}, },
selectors: { selectors: {
'@Input:disabled + &': { '@Input:disabled + &': {
cursor: 'not-allowed', cursor: 'not-allowed',
color: '#9ca3af' color: theme.colors.fgDim
} }
} }
} }
@ -164,19 +160,20 @@ const FormExamples = define('FormExamples', {
const Button = define('FormButton', { const Button = define('FormButton', {
base: 'button', base: 'button',
padding: '12px 24px', padding: `${theme.spacing.sm}px ${theme.spacing.lg}px`,
fontSize: 16, fontSize: 14,
fontWeight: 600, fontWeight: 400,
border: 'none', border: `1px solid ${theme.colors.accent}`,
borderRadius: 8, borderRadius: theme.radius.sm,
cursor: 'pointer', cursor: 'pointer',
transition: 'all 0.2s ease', transition: 'all 0.2s ease',
background: '#3b82f6', background: theme.colors.accent,
color: 'white', color: theme.colors.bg,
states: { states: {
':hover': { ':hover': {
background: '#2563eb' background: theme.colors.accentDim,
borderColor: theme.colors.accentDim,
}, },
':active': { ':active': {
transform: 'translateY(1px)' transform: 'translateY(1px)'
@ -186,11 +183,12 @@ const Button = define('FormButton', {
variants: { variants: {
variant: { variant: {
secondary: { secondary: {
background: '#6b7280', background: theme.colors.bgElevated,
color: 'white', color: theme.colors.fg,
border: `1px solid ${theme.colors.border}`,
states: { states: {
':hover': { ':hover': {
background: '#4b5563' borderColor: theme.colors.borderActive,
} }
} }
} }
@ -200,8 +198,8 @@ const Button = define('FormButton', {
const ButtonGroup = define('FormButtonGroup', { const ButtonGroup = define('FormButtonGroup', {
display: 'flex', display: 'flex',
gap: 12, gap: theme.spacing.sm,
marginTop: 24 marginTop: theme.spacing.lg
}) })
export const FormExamplesContent = () => ( export const FormExamplesContent = () => (

View File

@ -1,5 +1,5 @@
import { define } from '../src' import { define } from '../src'
import { ExampleSection } from './ssr/helpers' import { ExampleSection, theme } from './ssr/helpers'
const TabSwitcher = define('TabSwitcher', { const TabSwitcher = define('TabSwitcher', {
parts: { parts: {
@ -10,41 +10,41 @@ const TabSwitcher = define('TabSwitcher', {
TabBar: { TabBar: {
display: 'flex', display: 'flex',
gap: 0, gap: 0,
borderBottom: '2px solid #e5e7eb', borderBottom: `1px solid ${theme.colors.border}`,
marginBottom: 24, marginBottom: theme.spacing.lg,
}, },
TabLabel: { TabLabel: {
base: 'label', base: 'label',
padding: '12px 24px', padding: `${theme.spacing.sm}px ${theme.spacing.lg}px`,
position: 'relative', position: 'relative',
marginBottom: -2, marginBottom: -1,
background: 'transparent', background: 'transparent',
borderBottom: '2px solid transparent', borderBottom: '1px solid transparent',
color: '#6b7280', color: theme.colors.fgMuted,
fontSize: 14, fontSize: 14,
fontWeight: 500,
cursor: 'pointer', cursor: 'pointer',
transition: 'all 0.2s ease', transition: 'all 0.2s ease',
states: { states: {
':hover': { ':hover': {
color: '#111827', color: theme.colors.fg,
} }
}, },
selectors: { selectors: {
'@Input:checked + &': { '@Input:checked + &': {
color: '#3b82f6', color: theme.colors.accent,
borderBottom: '2px solid #3b82f6' borderBottom: `1px solid ${theme.colors.accent}`
} }
} }
}, },
Content: { Content: {
display: 'none', display: 'none',
padding: 20, padding: theme.spacing.lg,
background: '#f9fafb', background: theme.colors.bgElevated,
borderRadius: 8, border: `1px solid ${theme.colors.border}`,
borderRadius: theme.radius.sm,
selectors: { selectors: {
'@Input:checked ~ &': { '@Input:checked ~ &': {
@ -91,36 +91,37 @@ const Pills = define('Pills', {
}, },
PillBar: { PillBar: {
display: 'flex', display: 'flex',
gap: 8, gap: theme.spacing.xs,
flexWrap: 'wrap', flexWrap: 'wrap',
}, },
PillLabel: { PillLabel: {
base: 'label', base: 'label',
padding: '8px 16px', padding: `${theme.spacing.xs}px ${theme.spacing.md}px`,
background: '#f3f4f6', background: theme.colors.bgElevated,
border: 'none', border: `1px solid ${theme.colors.border}`,
borderRadius: 20, borderRadius: 20,
color: '#6b7280', color: theme.colors.fgMuted,
fontSize: 14, fontSize: 14,
fontWeight: 500,
cursor: 'pointer', cursor: 'pointer',
transition: 'all 0.2s ease', transition: 'all 0.2s ease',
states: { states: {
':hover': { ':hover': {
background: '#e5e7eb', borderColor: theme.colors.borderActive,
color: '#111827', color: theme.colors.fg,
} }
}, },
selectors: { selectors: {
'@Input:checked + &': { '@Input:checked + &': {
background: '#3b82f6', background: theme.colors.accent,
color: 'white' borderColor: theme.colors.accent,
color: theme.colors.bg
}, },
'@Input:checked + &:hover': { '@Input:checked + &:hover': {
background: '#2563eb' background: theme.colors.accentDim,
borderColor: theme.colors.accentDim,
} }
} }
} }
@ -164,34 +165,36 @@ const VerticalNav = define('VerticalNav', {
NavLabel: { NavLabel: {
base: 'label', base: 'label',
padding: '12px 16px', padding: `${theme.spacing.sm}px ${theme.spacing.md}px`,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 12, gap: theme.spacing.sm,
background: 'transparent', background: 'transparent',
borderRadius: 8, border: `1px solid transparent`,
color: '#6b7280', borderRadius: theme.radius.sm,
color: theme.colors.fgMuted,
fontSize: 14, fontSize: 14,
fontWeight: 500,
cursor: 'pointer', cursor: 'pointer',
transition: 'all 0.2s ease', transition: 'all 0.2s ease',
states: { states: {
':hover': { ':hover': {
background: '#f3f4f6', background: theme.colors.bgElevated,
color: '#111827', borderColor: theme.colors.border,
color: theme.colors.fg,
} }
}, },
selectors: { selectors: {
'@Input:checked + &': { '@Input:checked + &': {
background: '#eff6ff', background: theme.colors.bgElevated,
color: '#3b82f6', borderColor: theme.colors.accent,
color: theme.colors.accent,
}, },
'@Input:checked + &:hover': { '@Input:checked + &:hover': {
background: '#dbeafe', borderColor: theme.colors.accentDim,
color: '#2563eb' color: theme.colors.accentDim
} }
} }
}, },
@ -201,7 +204,7 @@ const VerticalNav = define('VerticalNav', {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
fontSize: 18, fontSize: 16,
} }
}, },
@ -232,33 +235,32 @@ const VerticalNav = define('VerticalNav', {
const Breadcrumbs = define('Breadcrumbs', { const Breadcrumbs = define('Breadcrumbs', {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 8, gap: theme.spacing.xs,
flexWrap: 'wrap', flexWrap: 'wrap',
parts: { parts: {
Item: { Item: {
base: 'a', base: 'a',
color: '#6b7280', color: theme.colors.fgMuted,
fontSize: 14, fontSize: 14,
textDecoration: 'none', textDecoration: 'none',
transition: 'color 0.2s ease', transition: 'color 0.2s ease',
states: { states: {
':hover': { ':hover': {
color: '#3b82f6', color: theme.colors.accent,
} }
} }
}, },
Separator: { Separator: {
color: '#d1d5db', color: theme.colors.fgDim,
fontSize: 14, fontSize: 14,
userSelect: 'none', userSelect: 'none',
}, },
Current: { Current: {
color: '#111827', color: theme.colors.fg,
fontSize: 14, fontSize: 14,
fontWeight: 500,
} }
}, },
@ -287,26 +289,25 @@ const Breadcrumbs = define('Breadcrumbs', {
const Tabs = define('Tabs', { const Tabs = define('Tabs', {
display: 'flex', display: 'flex',
gap: 0, gap: 0,
borderBottom: '2px solid #e5e7eb', borderBottom: `1px solid ${theme.colors.border}`,
parts: { parts: {
Tab: { Tab: {
base: 'button', base: 'button',
padding: '12px 24px', padding: `${theme.spacing.sm}px ${theme.spacing.lg}px`,
position: 'relative', position: 'relative',
marginBottom: -2, marginBottom: -1,
background: 'transparent', background: 'transparent',
border: 'none', border: 'none',
borderBottom: '2px solid transparent', borderBottom: '1px solid transparent',
color: '#6b7280', color: theme.colors.fgMuted,
fontSize: 14, fontSize: 14,
fontWeight: 500,
cursor: 'pointer', cursor: 'pointer',
transition: 'all 0.2s ease', transition: 'all 0.2s ease',
states: { states: {
':hover': { ':hover': {
color: '#111827', color: theme.colors.fg,
} }
} }
} }
@ -316,8 +317,8 @@ const Tabs = define('Tabs', {
active: { active: {
parts: { parts: {
Tab: { Tab: {
color: '#3b82f6', color: theme.colors.accent,
borderBottom: '2px solid #3b82f6', borderBottom: `1px solid ${theme.colors.accent}`,
} }
} }
} }
@ -338,26 +339,25 @@ const Tabs = define('Tabs', {
const SimplePills = define('SimplePills', { const SimplePills = define('SimplePills', {
display: 'flex', display: 'flex',
gap: 8, gap: theme.spacing.xs,
flexWrap: 'wrap', flexWrap: 'wrap',
parts: { parts: {
Pill: { Pill: {
base: 'button', base: 'button',
padding: '8px 16px', padding: `${theme.spacing.xs}px ${theme.spacing.md}px`,
background: '#f3f4f6', background: theme.colors.bgElevated,
border: 'none', border: `1px solid ${theme.colors.border}`,
borderRadius: 20, borderRadius: 20,
color: '#6b7280', color: theme.colors.fgMuted,
fontSize: 14, fontSize: 14,
fontWeight: 500,
cursor: 'pointer', cursor: 'pointer',
transition: 'all 0.2s ease', transition: 'all 0.2s ease',
states: { states: {
':hover': { ':hover': {
background: '#e5e7eb', borderColor: theme.colors.borderActive,
color: '#111827', color: theme.colors.fg,
} }
} }
} }
@ -367,12 +367,13 @@ const SimplePills = define('SimplePills', {
active: { active: {
parts: { parts: {
Pill: { Pill: {
background: '#3b82f6', background: theme.colors.accent,
color: 'white', borderColor: theme.colors.accent,
color: theme.colors.bg,
states: { states: {
':hover': { ':hover': {
background: '#2563eb', background: theme.colors.accentDim,
color: 'white', borderColor: theme.colors.accentDim,
} }
} }
} }
@ -402,24 +403,24 @@ const SimpleVerticalNav = define('SimpleVerticalNav', {
parts: { parts: {
NavItem: { NavItem: {
base: 'button', base: 'button',
padding: '12px 16px', padding: `${theme.spacing.sm}px ${theme.spacing.md}px`,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 12, gap: theme.spacing.sm,
background: 'transparent', background: 'transparent',
border: 'none', border: `1px solid transparent`,
borderRadius: 8, borderRadius: theme.radius.sm,
color: '#6b7280', color: theme.colors.fgMuted,
fontSize: 14, fontSize: 14,
fontWeight: 500,
textAlign: 'left', textAlign: 'left',
cursor: 'pointer', cursor: 'pointer',
transition: 'all 0.2s ease', transition: 'all 0.2s ease',
states: { states: {
':hover': { ':hover': {
background: '#f3f4f6', background: theme.colors.bgElevated,
color: '#111827', borderColor: theme.colors.border,
color: theme.colors.fg,
} }
} }
}, },
@ -429,7 +430,7 @@ const SimpleVerticalNav = define('SimpleVerticalNav', {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
fontSize: 18, fontSize: 16,
} }
}, },
@ -437,12 +438,13 @@ const SimpleVerticalNav = define('SimpleVerticalNav', {
active: { active: {
parts: { parts: {
NavItem: { NavItem: {
background: '#eff6ff', background: theme.colors.bgElevated,
color: '#3b82f6', borderColor: theme.colors.accent,
color: theme.colors.accent,
states: { states: {
':hover': { ':hover': {
background: '#dbeafe', borderColor: theme.colors.accentDim,
color: '#2563eb', color: theme.colors.accentDim,
} }
} }
} }

View File

@ -1,22 +1,22 @@
import { define } from '../src' import { define } from '../src'
import { ExampleSection } from './ssr/helpers' import { ExampleSection, theme } from './ssr/helpers'
const UserProfile = define('UserProfile', { const UserProfile = define('UserProfile', {
base: 'div', base: 'div',
padding: 24, padding: theme.spacing.lg,
maxWidth: 600, maxWidth: 600,
margin: "0 auto", margin: "0 auto",
background: "white", background: theme.colors.bgElevated,
borderRadius: 12, border: `1px solid ${theme.colors.border}`,
boxShadow: "0 2px 8px rgba(0,0,0,0.1)", borderRadius: theme.radius.md,
parts: { parts: {
Header: { Header: {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: 16, gap: theme.spacing.md,
marginBottom: 16, marginBottom: theme.spacing.md,
}, },
Avatar: { Avatar: {
base: 'img', base: 'img',
@ -24,34 +24,34 @@ const UserProfile = define('UserProfile', {
height: 64, height: 64,
borderRadius: "50%", borderRadius: "50%",
objectFit: "cover", objectFit: "cover",
border: "3px solid #e5e7eb", border: `2px solid ${theme.colors.border}`,
}, },
Info: { Info: {
flex: 1, flex: 1,
}, },
Name: { Name: {
marginBottom: 4, marginBottom: 4,
fontSize: 20, fontSize: 18,
fontWeight: 600, fontWeight: 400,
color: "#111827", color: theme.colors.fg,
}, },
Handle: { Handle: {
fontSize: 14, fontSize: 14,
color: "#6b7280", color: theme.colors.fgMuted,
}, },
Bio: { Bio: {
marginBottom: 16, marginBottom: theme.spacing.md,
width: "100%", width: "100%",
fontSize: 14, fontSize: 14,
lineHeight: 1.6, lineHeight: 1.6,
color: "#374151", color: theme.colors.fgMuted,
wordWrap: "break-word", wordWrap: "break-word",
}, },
Stats: { Stats: {
display: "flex", display: "flex",
gap: 24, gap: theme.spacing.lg,
paddingTop: 16, paddingTop: theme.spacing.md,
borderTop: "1px solid #e5e7eb", borderTop: `1px solid ${theme.colors.border}`,
}, },
Stat: { Stat: {
display: "flex", display: "flex",
@ -60,12 +60,12 @@ const UserProfile = define('UserProfile', {
}, },
StatValue: { StatValue: {
fontSize: 18, fontSize: 18,
fontWeight: 600, fontWeight: 400,
color: "#111827", color: theme.colors.fg,
}, },
StatLabel: { StatLabel: {
fontSize: 12, fontSize: 12,
color: "#6b7280", color: theme.colors.fgMuted,
textTransform: "uppercase", textTransform: "uppercase",
}, },
}, },
@ -114,19 +114,7 @@ const UserProfile = define('UserProfile', {
verified: { verified: {
parts: { parts: {
Avatar: { Avatar: {
border: "3px solid #3b82f6", border: `2px solid ${theme.colors.accent}`,
},
},
},
theme: {
dark: {
background: "#1f2937",
parts: {
Name: { color: "#f9fafb" },
Handle: { color: "#9ca3af" },
Bio: { color: "#d1d5db" },
Stats: { borderTop: "1px solid #374151" },
StatValue: { color: "#f9fafb" },
}, },
}, },
}, },
@ -207,10 +195,9 @@ export const ProfileExamplesContent = () => (
/> />
</ExampleSection> </ExampleSection>
<ExampleSection title="Verified User (Dark Theme)"> <ExampleSection title="Verified User">
<UserProfile <UserProfile
verified={true} verified={true}
theme="dark"
name="Jordan Smith" name="Jordan Smith"
username="jordansmith" username="jordansmith"
avatarUrl="https://i.pravatar.cc/150?img=8" avatarUrl="https://i.pravatar.cc/150?img=8"

View File

@ -1,4 +1,5 @@
import { define } from '../../src' import { define } from '../../src'
import { theme } from '../ssr/helpers'
import { ButtonExamplesContent } from '../button' import { ButtonExamplesContent } from '../button'
import { ProfileExamplesContent } from '../profile' import { ProfileExamplesContent } from '../profile'
import { NavigationExamplesContent } from '../navigation' import { NavigationExamplesContent } from '../navigation'
@ -8,9 +9,10 @@ export const Main = define('SpaMain', {
base: 'div', base: 'div',
minHeight: '100%', minHeight: '100%',
padding: '40px 20px', padding: theme.spacing.xl,
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif", fontFamily: theme.fonts.mono,
background: '#f3f4f6', background: theme.colors.bg,
color: theme.colors.fg,
}) })
export const Container = define('SpaContainer', { export const Container = define('SpaContainer', {
@ -24,21 +26,20 @@ export const Container = define('SpaContainer', {
const Link = define('Link', { const Link = define('Link', {
base: 'a', base: 'a',
color: '#3b82f6', color: theme.colors.fgMuted,
textDecoration: 'none', textDecoration: 'none',
fontWeight: 500, fontSize: 14,
states: { states: {
hover: { hover: {
textDecoration: 'underline' color: theme.colors.fg,
} }
}, },
selectors: { selectors: {
'&[aria-current]': { '&[aria-current]': {
color: '#1e40af', color: theme.colors.fg,
fontWeight: 600, textDecoration: 'underline',
textDecoration: 'underline'
} }
}, },
@ -61,52 +62,51 @@ const Nav = define('Nav', {
base: 'nav', base: 'nav',
display: 'flex', display: 'flex',
gap: 20, gap: theme.spacing.lg,
marginBottom: 40, marginBottom: theme.spacing.xl,
padding: 20, padding: theme.spacing.lg,
background: 'white', background: theme.colors.bgElevated,
borderRadius: 8, border: `1px solid ${theme.colors.border}`,
boxShadow: '0 1px 3px rgba(0,0,0,0.1)' borderRadius: theme.radius.sm,
}) })
const P = define('P', { const P = define('P', {
color: '#6b7280', color: theme.colors.fgMuted,
fontSize: 18, fontSize: 16,
marginBottom: 48, marginBottom: theme.spacing.xxl,
}) })
const ExamplesGrid = define('ExamplesGrid', { const ExamplesGrid = define('ExamplesGrid', {
display: 'grid', display: 'grid',
gap: 20, gap: theme.spacing.lg,
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))' gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))'
}) })
const ExampleCard = define('ExampleCard', { const ExampleCard = define('ExampleCard', {
base: 'a', base: 'a',
background: 'white', background: theme.colors.bgElevated,
padding: 24, padding: theme.spacing.lg,
borderRadius: 12, border: `1px solid ${theme.colors.border}`,
boxShadow: '0 1px 3px rgba(0,0,0,0.1)', borderRadius: theme.radius.sm,
textDecoration: 'none', textDecoration: 'none',
transition: 'all 0.2s ease',
display: 'block', display: 'block',
states: { states: {
hover: { hover: {
transform: 'translateY(-2px)', borderColor: theme.colors.borderActive,
boxShadow: '0 4px 12px rgba(0,0,0,0.15)'
} }
}, },
parts: { parts: {
H2: { H2: {
color: '#111827', color: theme.colors.fg,
margin: '0 0 8px 0', margin: `0 0 ${theme.spacing.sm}px 0`,
fontSize: 20, fontSize: 18,
fontWeight: 400,
}, },
P: { P: {
color: '#6b7280', color: theme.colors.fgMuted,
margin: 0, margin: 0,
fontSize: 14, fontSize: 14,
} }
@ -130,27 +130,27 @@ const ExampleCard = define('ExampleCard', {
const HomePage = () => ( const HomePage = () => (
<> <>
<P>Explore component examples built with Forge - Client-side SPA version</P> <P>Client-side rendered examples. Click around, check the source.</P>
<ExamplesGrid> <ExamplesGrid>
<ExampleCard href="/spa/profile" <ExampleCard href="/spa/profile"
title="Profile Card" title="Profile Card"
desc="User profile component with variants for size, theme, and verified status" desc="Parts, variants, custom render. Size/theme switching."
/> />
<ExampleCard href="/spa/buttons" <ExampleCard href="/spa/buttons"
title="Buttons" title="Buttons"
desc="Button component with intent, size, and disabled variants" desc="Intent, size, disabled states. Basic variant patterns."
/> />
<ExampleCard href="/spa/navigation" <ExampleCard href="/spa/navigation"
title="Navigation" title="Navigation"
desc="Navigation patterns including tabs, pills, vertical nav, and breadcrumbs" desc="Tabs, pills, breadcrumbs, vertical nav. No router required."
/> />
<ExampleCard href="/spa/form" <ExampleCard href="/spa/form"
title="Forms" title="Forms"
desc="Form inputs with validation states, checkboxes, textareas, and buttons" desc="Inputs, validation states, checkboxes, textareas."
/> />
</ExamplesGrid> </ExamplesGrid>
</> </>
@ -179,6 +179,20 @@ export function route(path: string) {
} }
} }
const HomeLink = define('HomeLink', {
base: 'a',
color: theme.colors.fgMuted,
textDecoration: 'none',
fontSize: 14,
states: {
hover: {
color: theme.colors.fg,
}
}
})
export function App() { export function App() {
const path = window.location.pathname const path = window.location.pathname
@ -186,7 +200,7 @@ export function App() {
<Main> <Main>
<Container> <Container>
<Nav> <Nav>
<a href="/" style="color: #3b82f6; text-decoration: none; font-weight: 500;">Home</a> <HomeLink href="/">Home</HomeLink>
<Link href="/spa" aria-current={path === '/spa' || path === '/spa/' ? 'page' : undefined}>SPA Examples</Link> <Link href="/spa" aria-current={path === '/spa' || path === '/spa/' ? 'page' : undefined}>SPA Examples</Link>
<Link href="/spa/profile" aria-current={path === '/spa/profile' ? 'page' : undefined}>Profile</Link> <Link href="/spa/profile" aria-current={path === '/spa/profile' ? 'page' : undefined}>Profile</Link>
<Link href="/spa/buttons" aria-current={path === '/spa/buttons' ? 'page' : undefined}>Buttons</Link> <Link href="/spa/buttons" aria-current={path === '/spa/buttons' ? 'page' : undefined}>Buttons</Link>

View File

@ -1,12 +1,51 @@
import { define, Styles } from '../../src' import { define, Styles } from '../../src'
export const theme = {
colors: {
bg: '#0a0a0a',
bgElevated: '#111',
bgHover: '#1a1a1a',
fg: '#00ff00',
fgMuted: '#888',
fgDim: '#444',
border: '#222',
borderActive: '#00ff00',
accent: '#00ff00',
accentDim: '#008800',
},
fonts: {
mono: "'Monaco', 'Menlo', 'Consolas', monospace",
sans: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
},
spacing: {
xs: 8,
sm: 12,
md: 16,
lg: 24,
xl: 32,
xxl: 48,
},
radius: {
sm: 4,
md: 8,
lg: 12,
}
}
export const Body = define('Body', { export const Body = define('Body', {
base: 'body', base: 'body',
margin: 0, margin: 0,
padding: '40px 20px', padding: theme.spacing.xl,
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif", fontFamily: theme.fonts.mono,
background: '#f3f4f6', background: theme.colors.bg,
color: theme.colors.fg,
}) })
const Container = define('Container', { const Container = define('Container', {
@ -17,20 +56,23 @@ const Container = define('Container', {
export const Header = define('Header', { export const Header = define('Header', {
base: 'h1', base: 'h1',
marginBottom: 40, marginBottom: theme.spacing.xl,
color: '#111827' color: theme.colors.fg,
fontSize: 28,
fontWeight: 400,
}) })
export const ExampleSection = define('ExampleSection', { export const ExampleSection = define('ExampleSection', {
marginBottom: 40, marginBottom: theme.spacing.xl,
parts: { parts: {
Header: { Header: {
base: 'h2', base: 'h2',
marginBottom: 16, marginBottom: theme.spacing.md,
color: '#374151', color: theme.colors.fgMuted,
fontSize: 18 fontSize: 16,
fontWeight: 400,
} }
}, },
render({ props, parts: { Root, Header } }) { render({ props, parts: { Root, Header } }) {
@ -47,32 +89,31 @@ const Nav = define('SSR_Nav', {
base: 'nav', base: 'nav',
display: 'flex', display: 'flex',
gap: 20, gap: theme.spacing.lg,
marginBottom: 40, marginBottom: theme.spacing.xl,
padding: 20, padding: theme.spacing.lg,
background: 'white', background: theme.colors.bgElevated,
borderRadius: 8, border: `1px solid ${theme.colors.border}`,
boxShadow: '0 1px 3px rgba(0,0,0,0.1)' borderRadius: theme.radius.sm,
}) })
const NavLink = define('SSR_NavLink', { const NavLink = define('SSR_NavLink', {
base: 'a', base: 'a',
color: '#3b82f6', color: theme.colors.fgMuted,
textDecoration: 'none', textDecoration: 'none',
fontWeight: 500, fontSize: 14,
states: { states: {
hover: { hover: {
textDecoration: 'underline' color: theme.colors.fg,
} }
}, },
selectors: { selectors: {
'&[aria-current]': { '&[aria-current]': {
color: '#1e40af', color: theme.colors.fg,
fontWeight: 600, textDecoration: 'underline',
textDecoration: 'underline'
} }
} }
}) })

View File

@ -1,4 +1,5 @@
import { createScope, Styles } from '../../src' import { createScope, Styles } from '../../src'
import { theme } from './helpers'
const { define } = createScope('Landing') const { define } = createScope('Landing')
@ -6,85 +7,60 @@ const Page = define('Page', {
base: 'body', base: 'body',
margin: 0, margin: 0,
padding: 0, padding: theme.spacing.xl,
minHeight: '100vh', minHeight: '100vh',
display: 'flex', fontFamily: theme.fonts.mono,
alignItems: 'center', background: theme.colors.bg,
justifyContent: 'center', color: theme.colors.fg,
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
}) })
const Container = define('Container', { const Container = define('Container', {
textAlign: 'center', maxWidth: 800,
color: 'white', margin: '0 auto',
}) })
const Title = define('Title', { const Pre = define('Pre', {
base: 'h1', base: 'pre',
fontSize: 48, fontSize: 14,
fontWeight: 700, lineHeight: 1.4,
marginBottom: 50, marginBottom: theme.spacing.xl,
color: 'white', color: theme.colors.fg,
whiteSpace: 'pre',
}) })
const Subtitle = define('Subtitle', { const P = define('P', {
base: 'p', base: 'p',
fontSize: 20, fontSize: 16,
marginBottom: 48, lineHeight: 1.6,
color: 'rgba(255, 255, 255, 0.9)', marginBottom: theme.spacing.xl,
color: theme.colors.fgMuted,
}) })
const ButtonGroup = define('ButtonGroup', { const LinkSection = define('LinkSection', {
display: 'flex', marginTop: theme.spacing.xxl,
gap: 50, paddingTop: theme.spacing.xl,
justifyContent: 'center', borderTop: `1px solid ${theme.colors.border}`,
flexWrap: 'wrap',
}) })
const ChoiceCard = define('ChoiceCard', { const Link = define('Link', {
base: 'a', base: 'a',
display: 'block', display: 'inline-block',
padding: 40, marginRight: theme.spacing.xl,
background: 'white', padding: `${theme.spacing.sm}px ${theme.spacing.lg}px`,
borderRadius: 16, background: theme.colors.bgElevated,
border: `1px solid ${theme.colors.border}`,
color: theme.colors.fg,
textDecoration: 'none', textDecoration: 'none',
color: '#111827', fontSize: 14,
boxShadow: '0 10px 30px rgba(0, 0, 0, 0.2)',
transition: 'all 0.3s ease',
minWidth: 250,
states: { states: {
':hover': { ':hover': {
transform: 'translateY(-8px)', background: theme.colors.bgHover,
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.3)', borderColor: theme.colors.borderActive,
} }
},
parts: {
Icon: {
fontSize: 48,
marginBottom: 16,
},
Title: {
base: 'h2',
fontSize: 24,
fontWeight: 600,
marginBottom: 8,
color: '#111827',
}
},
render({ props, parts: { Root, Icon, Title, Description } }) {
return (
<Root href={props.href}>
<Icon>{props.icon}</Icon>
<Title>{props.title}</Title>
</Root>
)
} }
}) })
@ -93,26 +69,30 @@ export const LandingPage = () => (
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Forge - Choose Your Rendering Mode</title> <title>forge</title>
<Styles /> <Styles />
</head> </head>
<Page> <Page>
<Container> <Container>
<Title>Welcome to Forge</Title> <Pre>{`╔═╝╔═║╔═║╔═╝╔═╝
`}</Pre>
<ButtonGroup> <P>
<ChoiceCard Typed, local, variant-driven CSS. No globals, no selector hell, no inline styles.
href="/ssr" Built for TSX. Compiles to real CSS.
icon="🖥️" </P>
title="SSR Examples"
/>
<ChoiceCard <P>
href="/spa" CSS is hostile to humans at scale. Forge fixes that by making styles local,
icon="⚡" typed, and composable. Parts replace selectors. Variants replace inline styles.
title="SPA Examples" Everything deterministic.
/> </P>
</ButtonGroup>
<LinkSection>
<Link href="/ssr">SSR demos </Link>
<Link href="/spa">SPA demos </Link>
</LinkSection>
</Container> </Container>
</Page> </Page>
</html> </html>

View File

@ -1,48 +1,47 @@
import { define } from '../../src' import { define } from '../../src'
import { Layout } from './helpers' import { Layout, theme } from './helpers'
import { ButtonExamplesContent } from '../button' import { ButtonExamplesContent } from '../button'
import { ProfileExamplesContent } from '../profile' import { ProfileExamplesContent } from '../profile'
import { NavigationExamplesContent } from '../navigation' import { NavigationExamplesContent } from '../navigation'
import { FormExamplesContent } from '../form' import { FormExamplesContent } from '../form'
const P = define('SSR_P', { const P = define('SSR_P', {
color: '#6b7280', color: theme.colors.fgMuted,
fontSize: 18, fontSize: 16,
marginBottom: 48, marginBottom: theme.spacing.xxl,
}) })
const ExamplesGrid = define('SSR_ExamplesGrid', { const ExamplesGrid = define('SSR_ExamplesGrid', {
display: 'grid', display: 'grid',
gap: 20, gap: theme.spacing.lg,
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))' gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))'
}) })
const ExampleCard = define('SSR_ExampleCard', { const ExampleCard = define('SSR_ExampleCard', {
base: 'a', base: 'a',
background: 'white', background: theme.colors.bgElevated,
padding: 24, padding: theme.spacing.lg,
borderRadius: 12, border: `1px solid ${theme.colors.border}`,
boxShadow: '0 1px 3px rgba(0,0,0,0.1)', borderRadius: theme.radius.sm,
textDecoration: 'none', textDecoration: 'none',
transition: 'all 0.2s ease',
display: 'block', display: 'block',
states: { states: {
hover: { hover: {
transform: 'translateY(-2px)', borderColor: theme.colors.borderActive,
boxShadow: '0 4px 12px rgba(0,0,0,0.15)'
} }
}, },
parts: { parts: {
H2: { H2: {
color: '#111827', color: theme.colors.fg,
margin: '0 0 8px 0', margin: `0 0 ${theme.spacing.sm}px 0`,
fontSize: 20, fontSize: 18,
fontWeight: 400,
}, },
P: { P: {
color: '#6b7280', color: theme.colors.fgMuted,
margin: 0, margin: 0,
fontSize: 14, fontSize: 14,
} }
@ -60,27 +59,27 @@ const ExampleCard = define('SSR_ExampleCard', {
export const IndexPage = ({ path }: any) => ( export const IndexPage = ({ path }: any) => (
<Layout title="Forge Examples" path={path}> <Layout title="Forge Examples" path={path}>
<P>Explore component examples built with Forge</P> <P>Server-rendered demos. View source to see the static CSS.</P>
<ExamplesGrid> <ExamplesGrid>
<ExampleCard href="/ssr/profile" <ExampleCard href="/ssr/profile"
title="Profile Card" title="Profile Card"
desc="User profile component with variants for size, theme, and verified status" desc="Parts, variants, custom render. Size/theme switching."
/> />
<ExampleCard href="/ssr/buttons" <ExampleCard href="/ssr/buttons"
title="Buttons" title="Buttons"
desc="Button component with intent, size, and disabled variants" desc="Intent, size, disabled states. Basic variant patterns."
/> />
<ExampleCard href="/ssr/navigation" <ExampleCard href="/ssr/navigation"
title="Navigation" title="Navigation"
desc="Navigation patterns including tabs, pills, vertical nav, and breadcrumbs" desc="Tabs, pills, breadcrumbs, vertical nav. No router required."
/> />
<ExampleCard href="/ssr/form" <ExampleCard href="/ssr/form"
title="Forms" title="Forms"
desc="Form inputs with validation states, checkboxes, textareas, and buttons" desc="Inputs, validation states, checkboxes, textareas."
/> />
</ExamplesGrid> </ExamplesGrid>
</Layout> </Layout>