Compare commits
7 Commits
c561207128
...
718dc3b73d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
718dc3b73d | ||
|
|
8a4408fe22 | ||
|
|
dafbce762b | ||
|
|
855112ac82 | ||
|
|
f66a47d0bb | ||
|
|
c04300802d | ||
|
|
f643f8b2eb |
214
README.md
214
README.md
|
|
@ -1,37 +1,52 @@
|
|||
# Forge
|
||||
# ⚒️ forge
|
||||
|
||||
## Why Forge?
|
||||
```
|
||||
╔═╝╔═║╔═║╔═╝╔═╝
|
||||
╔═╝║ ║╔╔╝║ ║╔═╝
|
||||
╝ ══╝╝ ╝══╝══╝
|
||||
```
|
||||
|
||||
CSS is powerful, but hostile.
|
||||
## overview
|
||||
|
||||
### Problems with CSS
|
||||
Forge is a typed, local, variant-driven way to organize CSS and create
|
||||
self-contained TSX components out of discrete parts.
|
||||
|
||||
- Styles are **global and open** — anything can override anything.
|
||||
- There’s **no link** between a class in markup and its definition.
|
||||
- Inline styles exist because there’s **no structured way to vary styles per instance**.
|
||||
- Overrides are silent — conflicts happen without feedback.
|
||||
- Complex components require selector gymnastics and reach-in styling.
|
||||
## css problems
|
||||
|
||||
### 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.
|
||||
- **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.
|
||||
## forge solutions
|
||||
|
||||
### 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.
|
||||
- Themes are easy.
|
||||
- Errors and feedback are provided.
|
||||
|
||||
- A typed, local, variant-driven way to author CSS.
|
||||
- A system that optimizes for **people typing at a keyboard**, not selectors in a cascade.
|
||||
## examples
|
||||
|
||||
### What Forge Is Not
|
||||
### styles
|
||||
|
||||
- Not a new component model.
|
||||
- Not a new language.
|
||||
- Not a CSS replacement — it compiles _to_ CSS, but removes the chaos.
|
||||
```tsx
|
||||
import { define } from "forge"
|
||||
|
||||
Example:
|
||||
export const Button = define("button", {
|
||||
base: "button",
|
||||
|
||||
padding: 20,
|
||||
background: "blue",
|
||||
})
|
||||
|
||||
// Usage
|
||||
<Button>Click me</Button>
|
||||
```
|
||||
|
||||
### variants
|
||||
|
||||
```tsx
|
||||
import { define } from "forge"
|
||||
|
|
@ -43,7 +58,7 @@ export const Button = define("button", {
|
|||
background: "blue",
|
||||
|
||||
variants: {
|
||||
kind: {
|
||||
status: {
|
||||
danger: { background: "red" },
|
||||
warning: { background: "yellow" },
|
||||
}
|
||||
|
|
@ -52,25 +67,29 @@ export const Button = define("button", {
|
|||
|
||||
// Usage
|
||||
<Button>Click me</Button>
|
||||
<Button kind="danger">Click me carefully</Button>
|
||||
<Button kind="warning">Click me?</Button>
|
||||
<Button status="danger">Click me carefully</Button>
|
||||
<Button status="warning">Click me?</Button>
|
||||
```
|
||||
|
||||
### parts + `render()`
|
||||
|
||||
```typescript
|
||||
export const Profile = define("div", {
|
||||
padding: 50,
|
||||
background: "red",
|
||||
|
||||
parts: {
|
||||
Header: { display: "flex" },
|
||||
Avatar: { base: 'img', width: 50 },
|
||||
Avatar: { base: "img", width: 50 },
|
||||
Bio: { color: "gray" },
|
||||
},
|
||||
|
||||
variants: {
|
||||
size: {
|
||||
small: {
|
||||
parts: { Avatar: { width: 20 }}
|
||||
}
|
||||
}
|
||||
parts: { Avatar: { width: 20 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
render({ props, parts: { Root, Header, Avatar, Bio } }) {
|
||||
|
|
@ -86,7 +105,138 @@ export const Profile = define("div", {
|
|||
})
|
||||
|
||||
// Usage:
|
||||
import { Profile } from './whatever'
|
||||
import { Profile } from "./whatever"
|
||||
|
||||
<Profile pic={user.pic} bio={user.bio} />
|
||||
console.log(<Profile pic={user.pic} bio={user.bio} />)
|
||||
console.log(<Profile size="small" pic={user.pic} bio={user.bio} />)
|
||||
```
|
||||
|
||||
### selectors
|
||||
|
||||
Use `selectors` to write custom CSS selectors. Reference the current
|
||||
element with `&` and other parts with `@PartName`:
|
||||
|
||||
```tsx
|
||||
const Checkbox = define("Checkbox", {
|
||||
parts: {
|
||||
Input: {
|
||||
base: "input[type=checkbox]",
|
||||
display: "none",
|
||||
},
|
||||
Label: {
|
||||
base: "label",
|
||||
padding: 10,
|
||||
cursor: "pointer",
|
||||
color: "gray",
|
||||
|
||||
selectors: {
|
||||
// style Label when Input is checked
|
||||
"@Input:checked + &": {
|
||||
color: "green",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
// style Label when Input is disabled
|
||||
"@Input:disabled + &": {
|
||||
opacity: 0.5,
|
||||
cursor: "not-allowed",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
render({ props, parts: { Root, Input, Label } }) {
|
||||
return (
|
||||
<Root>
|
||||
<Label>
|
||||
<Input checked={props.checked} />
|
||||
{props.label}
|
||||
</Label>
|
||||
</Root>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
// Usage
|
||||
<Checkbox label="Agree to terms" checked />
|
||||
```
|
||||
|
||||
## themes
|
||||
|
||||
built-in support for CSS variables with full type safety:
|
||||
|
||||
```tsx
|
||||
// themes.tsx - Define your themes
|
||||
import { createThemes } from "forge"
|
||||
|
||||
export const theme = createThemes({
|
||||
dark: {
|
||||
bgColor: "#0a0a0a",
|
||||
fgColor: "#00ff00",
|
||||
sm: 12,
|
||||
lg: 24,
|
||||
},
|
||||
light: {
|
||||
bgColor: "#f5f5f0",
|
||||
fgColor: "#0a0a0a",
|
||||
sm: 12,
|
||||
lg: 24,
|
||||
},
|
||||
})
|
||||
|
||||
// Use theme() in your components
|
||||
import { define } from "forge"
|
||||
import { theme } from "./themes"
|
||||
|
||||
const Button = define("Button", {
|
||||
padding: theme("spacing-sm"),
|
||||
background: theme("colors-bg"),
|
||||
color: theme("colors-fg"),
|
||||
})
|
||||
```
|
||||
|
||||
Theme switching is done via the `data-theme` attribute:
|
||||
|
||||
```tsx
|
||||
// Toggle between themes
|
||||
document.body.setAttribute("data-theme", "dark")
|
||||
document.body.setAttribute("data-theme", "light")
|
||||
```
|
||||
|
||||
The `theme()` function is fully typed based on your theme keys, giving
|
||||
you autocomplete and type checking throughout your codebase.
|
||||
|
||||
## scopes
|
||||
|
||||
Sometimes you want your parts named things like ButtonRow, ButtonCell,
|
||||
ButtonTable, etc, but all those Button's are repetitive:
|
||||
|
||||
```typescript
|
||||
const { define } = createScope("Button")
|
||||
|
||||
// css class becomes "Button"
|
||||
const Button = define("Root", {
|
||||
// becomes "Button"
|
||||
// ...
|
||||
})
|
||||
|
||||
// css class becomes "ButtonRow"
|
||||
const ButtonRow = define("Row", {
|
||||
// ...
|
||||
})
|
||||
|
||||
// css class becomes "ButtonContainer"
|
||||
const ButtonContainer = define("Container", {
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,75 +1,89 @@
|
|||
import { createScope } from '../src'
|
||||
import { ExampleSection } from './ssr/helpers'
|
||||
import { theme } from './ssr/themes'
|
||||
|
||||
const { define } = createScope('Button')
|
||||
|
||||
const Button = define('Root', {
|
||||
base: 'button',
|
||||
|
||||
padding: "12px 24px",
|
||||
padding: `${theme('spacing-sm')} ${theme('spacing-lg')}`,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 8,
|
||||
background: "#3b82f6",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: 8,
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
gap: theme('spacing-xs'),
|
||||
background: theme('colors-accent'),
|
||||
color: theme('colors-bg'),
|
||||
border: `1px solid ${theme('colors-accent')}`,
|
||||
borderRadius: theme('radius-sm'),
|
||||
fontSize: 14,
|
||||
fontWeight: 400,
|
||||
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": {
|
||||
transform: 'translateY(-2px)',
|
||||
filter: 'brightness(1.05)'
|
||||
background: theme('colors-accentDim'),
|
||||
borderColor: theme('colors-accentDim'),
|
||||
},
|
||||
":not(:disabled):active": {
|
||||
transform: 'translateY(1px)',
|
||||
boxShadow: '0 2px 3px rgba(0, 0, 0, 0.2)'
|
||||
},
|
||||
},
|
||||
|
||||
variants: {
|
||||
intent: {
|
||||
primary: {
|
||||
background: "#3b82f6",
|
||||
color: "white",
|
||||
boxShadow: "0 4px 6px rgba(59, 130, 246, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1)",
|
||||
background: theme('colors-accent'),
|
||||
color: theme('colors-bg'),
|
||||
border: `1px solid ${theme('colors-accent')}`,
|
||||
},
|
||||
secondary: {
|
||||
background: "#f3f4f6",
|
||||
color: "#374151",
|
||||
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06)",
|
||||
background: theme('colors-bgElevated'),
|
||||
color: theme('colors-fg'),
|
||||
border: `1px solid ${theme('colors-border')}`,
|
||||
states: {
|
||||
":not(:disabled):hover": {
|
||||
borderColor: theme('colors-borderActive'),
|
||||
}
|
||||
}
|
||||
},
|
||||
danger: {
|
||||
background: "#ef4444",
|
||||
color: "white",
|
||||
boxShadow: "0 4px 6px rgba(239, 68, 68, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1)",
|
||||
background: "#ff0000",
|
||||
color: theme('colors-bg'),
|
||||
border: "1px solid #ff0000",
|
||||
states: {
|
||||
":not(:disabled):hover": {
|
||||
background: "#cc0000",
|
||||
borderColor: "#cc0000",
|
||||
}
|
||||
}
|
||||
},
|
||||
ghost: {
|
||||
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",
|
||||
color: theme('colors-fgMuted'),
|
||||
border: `1px solid ${theme('colors-border')}`,
|
||||
states: {
|
||||
":not(:disabled):hover": {
|
||||
color: theme('colors-fg'),
|
||||
borderColor: theme('colors-borderActive'),
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
size: {
|
||||
small: {
|
||||
padding: "8px 16px",
|
||||
fontSize: 14,
|
||||
padding: `${theme('spacing-xs')} ${theme('spacing-md')}`,
|
||||
fontSize: 12,
|
||||
},
|
||||
large: {
|
||||
padding: "16px 32px",
|
||||
fontSize: 18,
|
||||
padding: `${theme('spacing-md')} ${theme('spacing-xl')}`,
|
||||
fontSize: 16,
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
opacity: 0.5,
|
||||
opacity: 0.3,
|
||||
cursor: "not-allowed",
|
||||
},
|
||||
},
|
||||
|
|
@ -77,7 +91,7 @@ const Button = define('Root', {
|
|||
|
||||
const ButtonRow = define('Row', {
|
||||
display: 'flex',
|
||||
gap: 16,
|
||||
gap: theme('spacing-md'),
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import { define } from '../src'
|
||||
import { ExampleSection } from './ssr/helpers'
|
||||
import { theme } from './ssr/themes'
|
||||
|
||||
const Input = define('Input', {
|
||||
base: 'input',
|
||||
|
||||
padding: '12px 16px',
|
||||
fontSize: 16,
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: 8,
|
||||
background: 'white',
|
||||
color: '#111827',
|
||||
padding: `${theme('spacing-sm')} ${theme('spacing-md')}`,
|
||||
fontSize: 14,
|
||||
border: `1px solid ${theme('colors-border')}`,
|
||||
borderRadius: theme('radius-sm'),
|
||||
background: theme('colors-bgElevated'),
|
||||
color: theme('colors-fg'),
|
||||
transition: 'all 0.2s ease',
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
|
|
@ -17,12 +18,11 @@ const Input = define('Input', {
|
|||
states: {
|
||||
':focus': {
|
||||
outline: 'none',
|
||||
borderColor: '#3b82f6',
|
||||
boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1)'
|
||||
borderColor: theme('colors-borderActive'),
|
||||
},
|
||||
':disabled': {
|
||||
background: '#f3f4f6',
|
||||
color: '#9ca3af',
|
||||
background: theme('colors-bg'),
|
||||
color: theme('colors-fgDim'),
|
||||
cursor: 'not-allowed'
|
||||
}
|
||||
},
|
||||
|
|
@ -30,20 +30,18 @@ const Input = define('Input', {
|
|||
variants: {
|
||||
status: {
|
||||
error: {
|
||||
borderColor: '#ef4444',
|
||||
borderColor: '#ff0000',
|
||||
states: {
|
||||
':focus': {
|
||||
borderColor: '#ef4444',
|
||||
boxShadow: '0 0 0 3px rgba(239, 68, 68, 0.1)'
|
||||
borderColor: '#ff0000',
|
||||
}
|
||||
}
|
||||
},
|
||||
success: {
|
||||
borderColor: '#10b981',
|
||||
borderColor: theme('colors-accent'),
|
||||
states: {
|
||||
':focus': {
|
||||
borderColor: '#10b981',
|
||||
boxShadow: '0 0 0 3px rgba(16, 185, 129, 0.1)'
|
||||
borderColor: theme('colors-accent'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -54,12 +52,12 @@ const Input = define('Input', {
|
|||
const Textarea = define('Textarea', {
|
||||
base: 'textarea',
|
||||
|
||||
padding: '12px 16px',
|
||||
fontSize: 16,
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: 8,
|
||||
background: 'white',
|
||||
color: '#111827',
|
||||
padding: `${theme('spacing-sm')} ${theme('spacing-md')}`,
|
||||
fontSize: 14,
|
||||
border: `1px solid ${theme('colors-border')}`,
|
||||
borderRadius: theme('radius-sm'),
|
||||
background: theme('colors-bgElevated'),
|
||||
color: theme('colors-fg'),
|
||||
transition: 'all 0.2s ease',
|
||||
width: '100%',
|
||||
minHeight: 120,
|
||||
|
|
@ -70,32 +68,31 @@ const Textarea = define('Textarea', {
|
|||
states: {
|
||||
':focus': {
|
||||
outline: 'none',
|
||||
borderColor: '#3b82f6',
|
||||
boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1)'
|
||||
borderColor: theme('colors-borderActive'),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const FormGroup = define('FormGroup', {
|
||||
marginBottom: 24,
|
||||
marginBottom: theme('spacing-lg'),
|
||||
|
||||
parts: {
|
||||
Label: {
|
||||
base: 'label',
|
||||
display: 'block',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: '#374151',
|
||||
marginBottom: 8
|
||||
fontWeight: 400,
|
||||
color: theme('colors-fg'),
|
||||
marginBottom: theme('spacing-xs')
|
||||
},
|
||||
Helper: {
|
||||
fontSize: 13,
|
||||
color: '#6b7280',
|
||||
fontSize: 12,
|
||||
color: theme('colors-fgMuted'),
|
||||
marginTop: 6
|
||||
},
|
||||
Error: {
|
||||
fontSize: 13,
|
||||
color: '#ef4444',
|
||||
fontSize: 12,
|
||||
color: '#ff0000',
|
||||
marginTop: 6
|
||||
}
|
||||
},
|
||||
|
|
@ -116,29 +113,29 @@ const Checkbox = define('Checkbox', {
|
|||
parts: {
|
||||
Input: {
|
||||
base: 'input[type=checkbox]',
|
||||
width: 20,
|
||||
height: 20,
|
||||
width: 18,
|
||||
height: 18,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
Label: {
|
||||
base: 'label',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
gap: theme('spacing-sm'),
|
||||
cursor: 'pointer',
|
||||
fontSize: 16,
|
||||
color: '#374151',
|
||||
fontSize: 14,
|
||||
color: theme('colors-fgMuted'),
|
||||
|
||||
states: {
|
||||
':hover': {
|
||||
color: '#111827'
|
||||
color: theme('colors-fg')
|
||||
}
|
||||
},
|
||||
|
||||
selectors: {
|
||||
'@Input:disabled + &': {
|
||||
cursor: 'not-allowed',
|
||||
color: '#9ca3af'
|
||||
color: theme('colors-fgDim')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -164,19 +161,20 @@ const FormExamples = define('FormExamples', {
|
|||
const Button = define('FormButton', {
|
||||
base: 'button',
|
||||
|
||||
padding: '12px 24px',
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
border: 'none',
|
||||
borderRadius: 8,
|
||||
padding: `${theme('spacing-sm')} ${theme('spacing-lg')}`,
|
||||
fontSize: 14,
|
||||
fontWeight: 400,
|
||||
border: `1px solid ${theme('colors-accent')}`,
|
||||
borderRadius: theme('radius-sm'),
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
background: '#3b82f6',
|
||||
color: 'white',
|
||||
background: theme('colors-accent'),
|
||||
color: theme('colors-bg'),
|
||||
|
||||
states: {
|
||||
':hover': {
|
||||
background: '#2563eb'
|
||||
background: theme('colors-accentDim'),
|
||||
borderColor: theme('colors-accentDim'),
|
||||
},
|
||||
':active': {
|
||||
transform: 'translateY(1px)'
|
||||
|
|
@ -186,11 +184,12 @@ const Button = define('FormButton', {
|
|||
variants: {
|
||||
variant: {
|
||||
secondary: {
|
||||
background: '#6b7280',
|
||||
color: 'white',
|
||||
background: theme('colors-bgElevated'),
|
||||
color: theme('colors-fg'),
|
||||
border: `1px solid ${theme('colors-border')}`,
|
||||
states: {
|
||||
':hover': {
|
||||
background: '#4b5563'
|
||||
borderColor: theme('colors-borderActive'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -200,8 +199,8 @@ const Button = define('FormButton', {
|
|||
|
||||
const ButtonGroup = define('FormButtonGroup', {
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
marginTop: 24
|
||||
gap: theme('spacing-sm'),
|
||||
marginTop: theme('spacing-lg')
|
||||
})
|
||||
|
||||
export const FormExamplesContent = () => (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { define } from '../src'
|
||||
import { ExampleSection } from './ssr/helpers'
|
||||
import { theme } from './ssr/themes'
|
||||
|
||||
const TabSwitcher = define('TabSwitcher', {
|
||||
parts: {
|
||||
|
|
@ -10,41 +11,41 @@ const TabSwitcher = define('TabSwitcher', {
|
|||
TabBar: {
|
||||
display: 'flex',
|
||||
gap: 0,
|
||||
borderBottom: '2px solid #e5e7eb',
|
||||
marginBottom: 24,
|
||||
borderBottom: `1px solid ${theme('colors-border')}`,
|
||||
marginBottom: theme('spacing-lg'),
|
||||
},
|
||||
TabLabel: {
|
||||
base: 'label',
|
||||
|
||||
padding: '12px 24px',
|
||||
padding: `${theme('spacing-sm')} ${theme('spacing-lg')}`,
|
||||
position: 'relative',
|
||||
marginBottom: -2,
|
||||
marginBottom: -1,
|
||||
background: 'transparent',
|
||||
borderBottom: '2px solid transparent',
|
||||
color: '#6b7280',
|
||||
borderBottom: '1px solid transparent',
|
||||
color: theme('colors-fgMuted'),
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
|
||||
states: {
|
||||
':hover': {
|
||||
color: '#111827',
|
||||
color: theme('colors-fg'),
|
||||
}
|
||||
},
|
||||
|
||||
selectors: {
|
||||
'@Input:checked + &': {
|
||||
color: '#3b82f6',
|
||||
borderBottom: '2px solid #3b82f6'
|
||||
color: theme('colors-accent'),
|
||||
borderBottom: `1px solid ${theme('colors-accent')}`
|
||||
}
|
||||
}
|
||||
},
|
||||
Content: {
|
||||
display: 'none',
|
||||
padding: 20,
|
||||
background: '#f9fafb',
|
||||
borderRadius: 8,
|
||||
padding: theme('spacing-lg'),
|
||||
background: theme('colors-bgElevated'),
|
||||
border: `1px solid ${theme('colors-border')}`,
|
||||
borderRadius: theme('radius-sm'),
|
||||
|
||||
selectors: {
|
||||
'@Input:checked ~ &': {
|
||||
|
|
@ -91,36 +92,37 @@ const Pills = define('Pills', {
|
|||
},
|
||||
PillBar: {
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
gap: theme('spacing-xs'),
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
PillLabel: {
|
||||
base: 'label',
|
||||
|
||||
padding: '8px 16px',
|
||||
background: '#f3f4f6',
|
||||
border: 'none',
|
||||
padding: `${theme('spacing-xs')} ${theme('spacing-md')}`,
|
||||
background: theme('colors-bgElevated'),
|
||||
border: `1px solid ${theme('colors-border')}`,
|
||||
borderRadius: 20,
|
||||
color: '#6b7280',
|
||||
color: theme('colors-fgMuted'),
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
|
||||
states: {
|
||||
':hover': {
|
||||
background: '#e5e7eb',
|
||||
color: '#111827',
|
||||
borderColor: theme('colors-borderActive'),
|
||||
color: theme('colors-fg'),
|
||||
}
|
||||
},
|
||||
|
||||
selectors: {
|
||||
'@Input:checked + &': {
|
||||
background: '#3b82f6',
|
||||
color: 'white'
|
||||
background: theme('colors-accent'),
|
||||
borderColor: theme('colors-accent'),
|
||||
color: theme('colors-bg')
|
||||
},
|
||||
'@Input:checked + &:hover': {
|
||||
background: '#2563eb'
|
||||
background: theme('colors-accentDim'),
|
||||
borderColor: theme('colors-accentDim'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -164,34 +166,36 @@ const VerticalNav = define('VerticalNav', {
|
|||
NavLabel: {
|
||||
base: 'label',
|
||||
|
||||
padding: '12px 16px',
|
||||
padding: `${theme('spacing-sm')} ${theme('spacing-md')}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
gap: theme('spacing-sm'),
|
||||
|
||||
background: 'transparent',
|
||||
borderRadius: 8,
|
||||
color: '#6b7280',
|
||||
border: `1px solid transparent`,
|
||||
borderRadius: theme('radius-sm'),
|
||||
color: theme('colors-fgMuted'),
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
|
||||
states: {
|
||||
':hover': {
|
||||
background: '#f3f4f6',
|
||||
color: '#111827',
|
||||
background: theme('colors-bgElevated'),
|
||||
borderColor: theme('colors-border'),
|
||||
color: theme('colors-fg'),
|
||||
}
|
||||
},
|
||||
|
||||
selectors: {
|
||||
'@Input:checked + &': {
|
||||
background: '#eff6ff',
|
||||
color: '#3b82f6',
|
||||
background: theme('colors-bgElevated'),
|
||||
borderColor: theme('colors-accent'),
|
||||
color: theme('colors-accent'),
|
||||
},
|
||||
'@Input:checked + &:hover': {
|
||||
background: '#dbeafe',
|
||||
color: '#2563eb'
|
||||
borderColor: theme('colors-accentDim'),
|
||||
color: theme('colors-accentDim')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -201,7 +205,7 @@ const VerticalNav = define('VerticalNav', {
|
|||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 18,
|
||||
fontSize: 16,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -232,33 +236,32 @@ const VerticalNav = define('VerticalNav', {
|
|||
const Breadcrumbs = define('Breadcrumbs', {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
gap: theme('spacing-xs'),
|
||||
flexWrap: 'wrap',
|
||||
|
||||
parts: {
|
||||
Item: {
|
||||
base: 'a',
|
||||
|
||||
color: '#6b7280',
|
||||
color: theme('colors-fgMuted'),
|
||||
fontSize: 14,
|
||||
textDecoration: 'none',
|
||||
transition: 'color 0.2s ease',
|
||||
|
||||
states: {
|
||||
':hover': {
|
||||
color: '#3b82f6',
|
||||
color: theme('colors-accent'),
|
||||
}
|
||||
}
|
||||
},
|
||||
Separator: {
|
||||
color: '#d1d5db',
|
||||
color: theme('colors-fgDim'),
|
||||
fontSize: 14,
|
||||
userSelect: 'none',
|
||||
},
|
||||
Current: {
|
||||
color: '#111827',
|
||||
color: theme('colors-fg'),
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -287,26 +290,25 @@ const Breadcrumbs = define('Breadcrumbs', {
|
|||
const Tabs = define('Tabs', {
|
||||
display: 'flex',
|
||||
gap: 0,
|
||||
borderBottom: '2px solid #e5e7eb',
|
||||
borderBottom: `1px solid ${theme('colors-border')}`,
|
||||
|
||||
parts: {
|
||||
Tab: {
|
||||
base: 'button',
|
||||
padding: '12px 24px',
|
||||
padding: `${theme('spacing-sm')} ${theme('spacing-lg')}`,
|
||||
position: 'relative',
|
||||
marginBottom: -2,
|
||||
marginBottom: -1,
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
borderBottom: '2px solid transparent',
|
||||
color: '#6b7280',
|
||||
borderBottom: '1px solid transparent',
|
||||
color: theme('colors-fgMuted'),
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
|
||||
states: {
|
||||
':hover': {
|
||||
color: '#111827',
|
||||
color: theme('colors-fg'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -316,8 +318,8 @@ const Tabs = define('Tabs', {
|
|||
active: {
|
||||
parts: {
|
||||
Tab: {
|
||||
color: '#3b82f6',
|
||||
borderBottom: '2px solid #3b82f6',
|
||||
color: theme('colors-accent'),
|
||||
borderBottom: `1px solid ${theme('colors-accent')}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -338,26 +340,25 @@ const Tabs = define('Tabs', {
|
|||
|
||||
const SimplePills = define('SimplePills', {
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
gap: theme('spacing-xs'),
|
||||
flexWrap: 'wrap',
|
||||
|
||||
parts: {
|
||||
Pill: {
|
||||
base: 'button',
|
||||
padding: '8px 16px',
|
||||
background: '#f3f4f6',
|
||||
border: 'none',
|
||||
padding: `${theme('spacing-xs')} ${theme('spacing-md')}`,
|
||||
background: theme('colors-bgElevated'),
|
||||
border: `1px solid ${theme('colors-border')}`,
|
||||
borderRadius: 20,
|
||||
color: '#6b7280',
|
||||
color: theme('colors-fgMuted'),
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
|
||||
states: {
|
||||
':hover': {
|
||||
background: '#e5e7eb',
|
||||
color: '#111827',
|
||||
borderColor: theme('colors-borderActive'),
|
||||
color: theme('colors-fg'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -367,12 +368,13 @@ const SimplePills = define('SimplePills', {
|
|||
active: {
|
||||
parts: {
|
||||
Pill: {
|
||||
background: '#3b82f6',
|
||||
color: 'white',
|
||||
background: theme('colors-accent'),
|
||||
borderColor: theme('colors-accent'),
|
||||
color: theme('colors-bg'),
|
||||
states: {
|
||||
':hover': {
|
||||
background: '#2563eb',
|
||||
color: 'white',
|
||||
background: theme('colors-accentDim'),
|
||||
borderColor: theme('colors-accentDim'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -402,24 +404,24 @@ const SimpleVerticalNav = define('SimpleVerticalNav', {
|
|||
parts: {
|
||||
NavItem: {
|
||||
base: 'button',
|
||||
padding: '12px 16px',
|
||||
padding: `${theme('spacing-sm')} ${theme('spacing-md')}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
gap: theme('spacing-sm'),
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
borderRadius: 8,
|
||||
color: '#6b7280',
|
||||
border: `1px solid transparent`,
|
||||
borderRadius: theme('radius-sm'),
|
||||
color: theme('colors-fgMuted'),
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
textAlign: 'left',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
|
||||
states: {
|
||||
':hover': {
|
||||
background: '#f3f4f6',
|
||||
color: '#111827',
|
||||
background: theme('colors-bgElevated'),
|
||||
borderColor: theme('colors-border'),
|
||||
color: theme('colors-fg'),
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -429,7 +431,7 @@ const SimpleVerticalNav = define('SimpleVerticalNav', {
|
|||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 18,
|
||||
fontSize: 16,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -437,12 +439,13 @@ const SimpleVerticalNav = define('SimpleVerticalNav', {
|
|||
active: {
|
||||
parts: {
|
||||
NavItem: {
|
||||
background: '#eff6ff',
|
||||
color: '#3b82f6',
|
||||
background: theme('colors-bgElevated'),
|
||||
borderColor: theme('colors-accent'),
|
||||
color: theme('colors-accent'),
|
||||
states: {
|
||||
':hover': {
|
||||
background: '#dbeafe',
|
||||
color: '#2563eb',
|
||||
borderColor: theme('colors-accentDim'),
|
||||
color: theme('colors-accentDim'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,23 @@
|
|||
import { define } from '../src'
|
||||
import { ExampleSection } from './ssr/helpers'
|
||||
import { theme } from './ssr/themes'
|
||||
|
||||
const UserProfile = define('UserProfile', {
|
||||
base: 'div',
|
||||
|
||||
padding: 24,
|
||||
padding: theme('spacing-lg'),
|
||||
maxWidth: 600,
|
||||
margin: "0 auto",
|
||||
background: "white",
|
||||
borderRadius: 12,
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
|
||||
background: theme('colors-bgElevated'),
|
||||
borderRadius: theme('radius-md'),
|
||||
border: theme('colors-accent'),
|
||||
|
||||
parts: {
|
||||
Header: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 16,
|
||||
marginBottom: 16,
|
||||
gap: theme('spacing-md'),
|
||||
marginBottom: theme('spacing-md'),
|
||||
},
|
||||
Avatar: {
|
||||
base: 'img',
|
||||
|
|
@ -24,34 +25,34 @@ const UserProfile = define('UserProfile', {
|
|||
height: 64,
|
||||
borderRadius: "50%",
|
||||
objectFit: "cover",
|
||||
border: "3px solid #e5e7eb",
|
||||
border: `2px solid ${theme('colors-border')}`,
|
||||
},
|
||||
Info: {
|
||||
flex: 1,
|
||||
},
|
||||
Name: {
|
||||
marginBottom: 4,
|
||||
fontSize: 20,
|
||||
fontWeight: 600,
|
||||
color: "#111827",
|
||||
fontSize: 18,
|
||||
fontWeight: 400,
|
||||
color: theme('colors-fg'),
|
||||
},
|
||||
Handle: {
|
||||
fontSize: 14,
|
||||
color: "#6b7280",
|
||||
color: theme('colors-fgMuted'),
|
||||
},
|
||||
Bio: {
|
||||
marginBottom: 16,
|
||||
marginBottom: theme('spacing-md'),
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
lineHeight: 1.6,
|
||||
color: "#374151",
|
||||
color: theme('colors-fgMuted'),
|
||||
wordWrap: "break-word",
|
||||
},
|
||||
Stats: {
|
||||
display: "flex",
|
||||
gap: 24,
|
||||
paddingTop: 16,
|
||||
borderTop: "1px solid #e5e7eb",
|
||||
gap: theme('spacing-lg'),
|
||||
paddingTop: theme('spacing-md'),
|
||||
borderTop: `1px solid ${theme('colors-border')}`,
|
||||
},
|
||||
Stat: {
|
||||
display: "flex",
|
||||
|
|
@ -60,12 +61,12 @@ const UserProfile = define('UserProfile', {
|
|||
},
|
||||
StatValue: {
|
||||
fontSize: 18,
|
||||
fontWeight: 600,
|
||||
color: "#111827",
|
||||
fontWeight: 400,
|
||||
color: theme('colors-fg'),
|
||||
},
|
||||
StatLabel: {
|
||||
fontSize: 12,
|
||||
color: "#6b7280",
|
||||
color: theme('colors-fgMuted'),
|
||||
textTransform: "uppercase",
|
||||
},
|
||||
},
|
||||
|
|
@ -114,19 +115,7 @@ const UserProfile = define('UserProfile', {
|
|||
verified: {
|
||||
parts: {
|
||||
Avatar: {
|
||||
border: "3px solid #3b82f6",
|
||||
},
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
dark: {
|
||||
background: "#1f2937",
|
||||
parts: {
|
||||
Name: { color: "#f9fafb" },
|
||||
Handle: { color: "#9ca3af" },
|
||||
Bio: { color: "#d1d5db" },
|
||||
Stats: { borderTop: "1px solid #374151" },
|
||||
StatValue: { color: "#f9fafb" },
|
||||
border: `2px solid ${theme('colors-accent')}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -207,10 +196,9 @@ export const ProfileExamplesContent = () => (
|
|||
/>
|
||||
</ExampleSection>
|
||||
|
||||
<ExampleSection title="Verified User (Dark Theme)">
|
||||
<ExampleSection title="Verified User">
|
||||
<UserProfile
|
||||
verified={true}
|
||||
theme="dark"
|
||||
name="Jordan Smith"
|
||||
username="jordansmith"
|
||||
avatarUrl="https://i.pravatar.cc/150?img=8"
|
||||
|
|
|
|||
|
|
@ -1,16 +1,68 @@
|
|||
import { define } from '../../src'
|
||||
import { theme } from '../ssr/themes'
|
||||
import { ButtonExamplesContent } from '../button'
|
||||
import { ProfileExamplesContent } from '../profile'
|
||||
import { NavigationExamplesContent } from '../navigation'
|
||||
import { FormExamplesContent } from '../form'
|
||||
|
||||
// ThemePicker component
|
||||
const ThemePicker = define('SpaThemePicker', {
|
||||
marginLeft: 'auto',
|
||||
|
||||
parts: {
|
||||
Select: {
|
||||
base: 'select',
|
||||
|
||||
padding: `${theme('spacing-xs')} ${theme('spacing-md')}`,
|
||||
background: theme('colors-bgElevated'),
|
||||
border: `1px solid ${theme('colors-border')}`,
|
||||
borderRadius: theme('radius-sm'),
|
||||
color: theme('colors-fg'),
|
||||
fontSize: 14,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
|
||||
states: {
|
||||
':hover': {
|
||||
borderColor: theme('colors-borderActive'),
|
||||
},
|
||||
':focus': {
|
||||
outline: 'none',
|
||||
borderColor: theme('colors-borderActive'),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render({ parts: { Root, Select } }) {
|
||||
return (
|
||||
<Root>
|
||||
<Select id="theme-select" onChange={themeChanged}>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="light">Light</option>
|
||||
</Select>
|
||||
</Root>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
function themeChanged(e: Event) {
|
||||
const target = e.target as HTMLSelectElement
|
||||
const themeName = target.value
|
||||
document.body.setAttribute('data-theme', themeName)
|
||||
localStorage.setItem('theme', themeName)
|
||||
}
|
||||
|
||||
export const Main = define('SpaMain', {
|
||||
base: 'div',
|
||||
|
||||
minHeight: '100%',
|
||||
padding: '40px 20px',
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
||||
background: '#f3f4f6',
|
||||
height: '100%',
|
||||
padding: theme('spacing-xl'),
|
||||
fontFamily: theme('fonts-mono'),
|
||||
background: theme('colors-bg'),
|
||||
color: theme('colors-fg'),
|
||||
boxSizing: 'border-box',
|
||||
})
|
||||
|
||||
export const Container = define('SpaContainer', {
|
||||
|
|
@ -24,21 +76,20 @@ export const Container = define('SpaContainer', {
|
|||
const Link = define('Link', {
|
||||
base: 'a',
|
||||
|
||||
color: '#3b82f6',
|
||||
color: theme('colors-fgMuted'),
|
||||
textDecoration: 'none',
|
||||
fontWeight: 500,
|
||||
fontSize: 14,
|
||||
|
||||
states: {
|
||||
hover: {
|
||||
textDecoration: 'underline'
|
||||
color: theme('colors-fg'),
|
||||
}
|
||||
},
|
||||
|
||||
selectors: {
|
||||
'&[aria-current]': {
|
||||
color: '#1e40af',
|
||||
fontWeight: 600,
|
||||
textDecoration: 'underline'
|
||||
color: theme('colors-fg'),
|
||||
textDecoration: 'underline',
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -61,52 +112,51 @@ const Nav = define('Nav', {
|
|||
base: 'nav',
|
||||
|
||||
display: 'flex',
|
||||
gap: 20,
|
||||
marginBottom: 40,
|
||||
padding: 20,
|
||||
background: 'white',
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
|
||||
gap: theme('spacing-lg'),
|
||||
marginBottom: theme('spacing-xl'),
|
||||
padding: theme('spacing-lg'),
|
||||
background: theme('colors-bgElevated'),
|
||||
border: `1px solid ${theme('colors-border')}`,
|
||||
borderRadius: theme('radius-sm'),
|
||||
})
|
||||
|
||||
const P = define('P', {
|
||||
color: '#6b7280',
|
||||
fontSize: 18,
|
||||
marginBottom: 48,
|
||||
color: theme('colors-fgMuted'),
|
||||
fontSize: 16,
|
||||
marginBottom: theme('spacing-xxl'),
|
||||
})
|
||||
|
||||
const ExamplesGrid = define('ExamplesGrid', {
|
||||
display: 'grid',
|
||||
gap: 20,
|
||||
gap: theme('spacing-lg'),
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))'
|
||||
})
|
||||
|
||||
const ExampleCard = define('ExampleCard', {
|
||||
base: 'a',
|
||||
|
||||
background: 'white',
|
||||
padding: 24,
|
||||
borderRadius: 12,
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
||||
background: theme('colors-bgElevated'),
|
||||
padding: theme('spacing-lg'),
|
||||
border: `1px solid ${theme('colors-border')}`,
|
||||
borderRadius: theme('radius-sm'),
|
||||
textDecoration: 'none',
|
||||
transition: 'all 0.2s ease',
|
||||
display: 'block',
|
||||
|
||||
states: {
|
||||
hover: {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.15)'
|
||||
borderColor: theme('colors-borderActive'),
|
||||
}
|
||||
},
|
||||
|
||||
parts: {
|
||||
H2: {
|
||||
color: '#111827',
|
||||
margin: '0 0 8px 0',
|
||||
fontSize: 20,
|
||||
color: theme('colors-fg'),
|
||||
margin: `0 0 ${theme('spacing-sm')} 0`,
|
||||
fontSize: 18,
|
||||
fontWeight: 400,
|
||||
},
|
||||
P: {
|
||||
color: '#6b7280',
|
||||
color: theme('colors-fgMuted'),
|
||||
margin: 0,
|
||||
fontSize: 14,
|
||||
}
|
||||
|
|
@ -130,27 +180,27 @@ const ExampleCard = define('ExampleCard', {
|
|||
|
||||
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>
|
||||
<ExampleCard href="/spa/profile"
|
||||
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"
|
||||
title="Buttons"
|
||||
desc="Button component with intent, size, and disabled variants"
|
||||
desc="Intent, size, disabled states. Basic variant patterns."
|
||||
/>
|
||||
|
||||
<ExampleCard href="/spa/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"
|
||||
title="Forms"
|
||||
desc="Form inputs with validation states, checkboxes, textareas, and buttons"
|
||||
desc="Inputs, validation states, checkboxes, textareas."
|
||||
/>
|
||||
</ExamplesGrid>
|
||||
</>
|
||||
|
|
@ -179,6 +229,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() {
|
||||
const path = window.location.pathname
|
||||
|
||||
|
|
@ -186,12 +250,13 @@ export function App() {
|
|||
<Main>
|
||||
<Container>
|
||||
<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/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/navigation" aria-current={path === '/spa/navigation' ? 'page' : undefined}>Navigation</Link>
|
||||
<Link href="/spa/form" aria-current={path === '/spa/form' ? 'page' : undefined}>Forms</Link>
|
||||
<ThemePicker />
|
||||
</Nav>
|
||||
<div id="content">
|
||||
{route(path)}
|
||||
|
|
|
|||
|
|
@ -3,16 +3,30 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/main.css"/>
|
||||
<title>Forge SPA Examples</title>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<body data-theme="dark">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/spa.js"></script>
|
||||
<script>
|
||||
// Load saved theme and apply it
|
||||
const savedTheme = localStorage.getItem('theme') || 'dark'
|
||||
document.body.setAttribute('data-theme', savedTheme)
|
||||
|
||||
// Set initial select value after page loads
|
||||
window.addEventListener('load', () => {
|
||||
const select = document.getElementById('theme-select')
|
||||
if (select) select.value = savedTheme
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
30
examples/ssr/darkTheme.tsx
Normal file
30
examples/ssr/darkTheme.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
export default {
|
||||
'colors-bg': '#0a0a0a',
|
||||
'colors-bgElevated': '#111',
|
||||
'colors-bgHover': '#1a1a1a',
|
||||
|
||||
'colors-fg': '#00ff00',
|
||||
'colors-fgMuted': '#888',
|
||||
'colors-fgDim': '#444',
|
||||
|
||||
'colors-border': '#222',
|
||||
'colors-borderActive': '#00ff00',
|
||||
|
||||
'colors-accent': '#00ff00',
|
||||
'colors-accentDim': '#008800',
|
||||
|
||||
'fonts-mono': "'Monaco', 'Menlo', 'Consolas', monospace",
|
||||
'fonts-sans': "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
||||
|
||||
'spacing-xs': '8px',
|
||||
'spacing-sm': '12px',
|
||||
'spacing-md': '16px',
|
||||
'spacing-lg': '24px',
|
||||
'spacing-xl': '32px',
|
||||
'spacing-xxl': '48px',
|
||||
|
||||
'radius-sm': '4px',
|
||||
'radius-md': '8px',
|
||||
'radius-lg': '12px',
|
||||
} as const
|
||||
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
import { define, Styles } from '../../src'
|
||||
import { define } from '../../src'
|
||||
import { theme } from './themes'
|
||||
|
||||
export const Body = define('Body', {
|
||||
base: 'body',
|
||||
|
||||
margin: 0,
|
||||
padding: '40px 20px',
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
||||
background: '#f3f4f6',
|
||||
padding: theme('spacing-xl'),
|
||||
fontFamily: theme('fonts-mono'),
|
||||
background: theme('colors-bg'),
|
||||
color: theme('colors-fg'),
|
||||
})
|
||||
|
||||
const Container = define('Container', {
|
||||
|
|
@ -17,20 +19,23 @@ const Container = define('Container', {
|
|||
export const Header = define('Header', {
|
||||
base: 'h1',
|
||||
|
||||
marginBottom: 40,
|
||||
color: '#111827'
|
||||
marginBottom: theme('spacing-xl'),
|
||||
color: theme('colors-fg'),
|
||||
fontSize: 28,
|
||||
fontWeight: 400,
|
||||
})
|
||||
|
||||
export const ExampleSection = define('ExampleSection', {
|
||||
marginBottom: 40,
|
||||
marginBottom: theme('spacing-xl'),
|
||||
|
||||
parts: {
|
||||
Header: {
|
||||
base: 'h2',
|
||||
|
||||
marginBottom: 16,
|
||||
color: '#374151',
|
||||
fontSize: 18
|
||||
marginBottom: theme('spacing-md'),
|
||||
color: theme('colors-fgMuted'),
|
||||
fontSize: 16,
|
||||
fontWeight: 400,
|
||||
}
|
||||
},
|
||||
render({ props, parts: { Root, Header } }) {
|
||||
|
|
@ -43,44 +48,100 @@ export const ExampleSection = define('ExampleSection', {
|
|||
}
|
||||
})
|
||||
|
||||
const Nav = define('SSR_Nav', {
|
||||
const Nav = define({
|
||||
base: 'nav',
|
||||
|
||||
display: 'flex',
|
||||
gap: 20,
|
||||
marginBottom: 40,
|
||||
padding: 20,
|
||||
background: 'white',
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
|
||||
gap: theme('spacing-lg'),
|
||||
marginBottom: theme('spacing-xl'),
|
||||
padding: theme('spacing-lg'),
|
||||
background: theme('colors-bgElevated'),
|
||||
border: `1px solid ${theme('colors-border')}`,
|
||||
borderRadius: theme('radius-sm'),
|
||||
})
|
||||
|
||||
const NavLink = define('SSR_NavLink', {
|
||||
const NavLink = define({
|
||||
base: 'a',
|
||||
|
||||
color: '#3b82f6',
|
||||
color: theme('colors-fgMuted'),
|
||||
textDecoration: 'none',
|
||||
fontWeight: 500,
|
||||
fontSize: 14,
|
||||
|
||||
states: {
|
||||
hover: {
|
||||
textDecoration: 'underline'
|
||||
color: theme('colors-fg'),
|
||||
}
|
||||
},
|
||||
|
||||
selectors: {
|
||||
'&[aria-current]': {
|
||||
color: '#1e40af',
|
||||
fontWeight: 600,
|
||||
textDecoration: 'underline'
|
||||
color: theme('colors-fg'),
|
||||
textDecoration: 'underline',
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const ThemePicker = define('ThemePicker', {
|
||||
marginLeft: 'auto',
|
||||
|
||||
parts: {
|
||||
Select: {
|
||||
base: 'select',
|
||||
|
||||
padding: `${theme('spacing-xs')} ${theme('spacing-md')}`,
|
||||
background: theme('colors-bgElevated'),
|
||||
border: `1px solid ${theme('colors-border')}`,
|
||||
borderRadius: theme('radius-sm'),
|
||||
color: theme('colors-fg'),
|
||||
fontSize: 14,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
|
||||
states: {
|
||||
':hover': {
|
||||
borderColor: theme('colors-borderActive'),
|
||||
},
|
||||
':focus': {
|
||||
outline: 'none',
|
||||
borderColor: theme('colors-borderActive'),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render({ parts: { Root, Select } }) {
|
||||
return (
|
||||
<Root>
|
||||
<Select id="theme-select" onchange="window.switchTheme(this.value)">
|
||||
<option value="dark">Dark</option>
|
||||
<option value="light">Light</option>
|
||||
</Select>
|
||||
</Root>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export const Layout = define({
|
||||
render({ props }) {
|
||||
const path = props.path || ''
|
||||
|
||||
const themeScript = `
|
||||
function switchTheme(themeName) {
|
||||
document.body.setAttribute('data-theme', themeName)
|
||||
localStorage.setItem('theme', themeName)
|
||||
}
|
||||
|
||||
window.switchTheme = switchTheme
|
||||
|
||||
// Load saved theme or default to dark
|
||||
const savedTheme = localStorage.getItem('theme') || 'dark'
|
||||
document.body.setAttribute('data-theme', savedTheme)
|
||||
|
||||
// Set initial select value
|
||||
const select = document.getElementById('theme-select')
|
||||
if (select) select.value = savedTheme
|
||||
`
|
||||
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
|
|
@ -98,10 +159,12 @@ export const Layout = define({
|
|||
<NavLink href="/ssr/buttons" aria-current={path === '/ssr/buttons' ? 'page' : undefined}>Buttons</NavLink>
|
||||
<NavLink href="/ssr/navigation" aria-current={path === '/ssr/navigation' ? 'page' : undefined}>Navigation</NavLink>
|
||||
<NavLink href="/ssr/form" aria-current={path === '/ssr/form' ? 'page' : undefined}>Forms</NavLink>
|
||||
<ThemePicker />
|
||||
</Nav>
|
||||
<Header>{props.title}</Header>
|
||||
{props.children}
|
||||
</Container>
|
||||
<script dangerouslySetInnerHTML={{ __html: themeScript }}></script>
|
||||
</Body>
|
||||
</html>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { createScope, Styles } from '../../src'
|
||||
import { theme } from './themes'
|
||||
|
||||
const { define } = createScope('Landing')
|
||||
|
||||
|
|
@ -6,114 +7,110 @@ const Page = define('Page', {
|
|||
base: 'body',
|
||||
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
padding: theme('spacing-xl'),
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
fontFamily: theme('fonts-mono'),
|
||||
background: theme('colors-bg'),
|
||||
color: theme('colors-fg'),
|
||||
})
|
||||
|
||||
const Container = define('Container', {
|
||||
textAlign: 'center',
|
||||
color: 'white',
|
||||
maxWidth: 800,
|
||||
margin: '0 auto',
|
||||
})
|
||||
|
||||
const Title = define('Title', {
|
||||
base: 'h1',
|
||||
const Pre = define('Pre', {
|
||||
base: 'pre',
|
||||
|
||||
fontSize: 48,
|
||||
fontWeight: 700,
|
||||
marginBottom: 50,
|
||||
color: 'white',
|
||||
fontSize: 14,
|
||||
lineHeight: 1.4,
|
||||
marginBottom: theme('spacing-xl'),
|
||||
color: theme('colors-fg'),
|
||||
whiteSpace: 'pre',
|
||||
borderBottom: '1px solid var(--theme-colors-border)',
|
||||
})
|
||||
|
||||
const Subtitle = define('Subtitle', {
|
||||
const P = define('P', {
|
||||
base: 'p',
|
||||
|
||||
fontSize: 20,
|
||||
marginBottom: 48,
|
||||
color: 'rgba(255, 255, 255, 0.9)',
|
||||
fontSize: 16,
|
||||
lineHeight: 1.6,
|
||||
marginBottom: theme('spacing-xl'),
|
||||
color: theme('colors-fgMuted'),
|
||||
})
|
||||
|
||||
const ButtonGroup = define('ButtonGroup', {
|
||||
display: 'flex',
|
||||
gap: 50,
|
||||
justifyContent: 'center',
|
||||
flexWrap: 'wrap',
|
||||
const LinkSection = define('LinkSection', {
|
||||
marginTop: theme('spacing-xxl'),
|
||||
paddingTop: theme('spacing-xl'),
|
||||
borderTop: `1px solid ${theme('colors-border')}`,
|
||||
})
|
||||
|
||||
const ChoiceCard = define('ChoiceCard', {
|
||||
const Link = define('Link', {
|
||||
base: 'a',
|
||||
|
||||
display: 'block',
|
||||
padding: 40,
|
||||
background: 'white',
|
||||
borderRadius: 16,
|
||||
display: 'inline-block',
|
||||
marginRight: theme('spacing-xl'),
|
||||
padding: `${theme('spacing-sm')} ${theme('spacing-lg')}`,
|
||||
background: theme('colors-bgElevated'),
|
||||
border: `1px solid ${theme('colors-border')}`,
|
||||
color: theme('colors-fg'),
|
||||
textDecoration: 'none',
|
||||
color: '#111827',
|
||||
boxShadow: '0 10px 30px rgba(0, 0, 0, 0.2)',
|
||||
transition: 'all 0.3s ease',
|
||||
minWidth: 250,
|
||||
fontSize: 14,
|
||||
|
||||
states: {
|
||||
':hover': {
|
||||
transform: 'translateY(-8px)',
|
||||
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.3)',
|
||||
background: theme('colors-bgHover'),
|
||||
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>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export const LandingPage = () => (
|
||||
export const LandingPage = () => {
|
||||
const themeScript = `
|
||||
function switchTheme(themeName) {
|
||||
document.body.setAttribute('data-theme', themeName)
|
||||
localStorage.setItem('theme', themeName)
|
||||
}
|
||||
|
||||
window.switchTheme = switchTheme
|
||||
|
||||
// Load saved theme or default to dark
|
||||
const savedTheme = localStorage.getItem('theme') || 'dark'
|
||||
document.body.setAttribute('data-theme', savedTheme)
|
||||
`
|
||||
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Forge - Choose Your Rendering Mode</title>
|
||||
<title>forge</title>
|
||||
<Styles />
|
||||
</head>
|
||||
<Page>
|
||||
<Page data-theme="dark">
|
||||
<Container>
|
||||
<Title>Welcome to Forge</Title>
|
||||
<Pre>{`╔═╝╔═║╔═║╔═╝╔═╝
|
||||
╔═╝║ ║╔╔╝║ ║╔═╝
|
||||
╝ ══╝╝ ╝══╝══╝`}</Pre>
|
||||
|
||||
<ButtonGroup>
|
||||
<ChoiceCard
|
||||
href="/ssr"
|
||||
icon="🖥️"
|
||||
title="SSR Examples"
|
||||
/>
|
||||
<P>
|
||||
Typed, local, variant-driven CSS. No globals, no selector hell, no inline styles.
|
||||
Built for TSX. Compiles to real CSS.
|
||||
</P>
|
||||
|
||||
<ChoiceCard
|
||||
href="/spa"
|
||||
icon="⚡"
|
||||
title="SPA Examples"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<P>
|
||||
CSS is hostile to humans at scale. Forge fixes that by making styles local,
|
||||
typed, and composable. Parts replace selectors. Variants replace inline styles.
|
||||
Everything deterministic.
|
||||
</P>
|
||||
|
||||
<LinkSection>
|
||||
<Link href="/ssr">SSR demos →</Link>
|
||||
<Link href="/spa">SPA demos →</Link>
|
||||
</LinkSection>
|
||||
</Container>
|
||||
<script dangerouslySetInnerHTML={{ __html: themeScript }}></script>
|
||||
</Page>
|
||||
</html>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
29
examples/ssr/lightTheme.tsx
Normal file
29
examples/ssr/lightTheme.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
export default {
|
||||
'colors-bg': '#f5f5f0',
|
||||
'colors-bgElevated': '#fff',
|
||||
'colors-bgHover': '#e8e8e0',
|
||||
|
||||
'colors-fg': '#0a0a0a',
|
||||
'colors-fgMuted': '#666',
|
||||
'colors-fgDim': '#999',
|
||||
|
||||
'colors-border': '#ddd',
|
||||
'colors-borderActive': '#008800',
|
||||
|
||||
'colors-accent': '#008800',
|
||||
'colors-accentDim': '#00aa00',
|
||||
|
||||
'fonts-mono': "'Monaco', 'Menlo', 'Consolas', monospace",
|
||||
'fonts-sans': "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
||||
|
||||
'spacing-xs': '8px',
|
||||
'spacing-sm': '12px',
|
||||
'spacing-md': '16px',
|
||||
'spacing-lg': '24px',
|
||||
'spacing-xl': '32px',
|
||||
'spacing-xxl': '48px',
|
||||
|
||||
'radius-sm': '4px',
|
||||
'radius-md': '8px',
|
||||
'radius-lg': '12px',
|
||||
} as const
|
||||
|
|
@ -1,48 +1,48 @@
|
|||
import { define } from '../../src'
|
||||
import { Layout } from './helpers'
|
||||
import { theme } from './themes'
|
||||
import { ButtonExamplesContent } from '../button'
|
||||
import { ProfileExamplesContent } from '../profile'
|
||||
import { NavigationExamplesContent } from '../navigation'
|
||||
import { FormExamplesContent } from '../form'
|
||||
|
||||
const P = define('SSR_P', {
|
||||
color: '#6b7280',
|
||||
fontSize: 18,
|
||||
marginBottom: 48,
|
||||
color: theme('colors-fgMuted'),
|
||||
fontSize: 16,
|
||||
marginBottom: theme('spacing-xxl'),
|
||||
})
|
||||
|
||||
const ExamplesGrid = define('SSR_ExamplesGrid', {
|
||||
display: 'grid',
|
||||
gap: 20,
|
||||
gap: theme('spacing-lg'),
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))'
|
||||
})
|
||||
|
||||
const ExampleCard = define('SSR_ExampleCard', {
|
||||
base: 'a',
|
||||
|
||||
background: 'white',
|
||||
padding: 24,
|
||||
borderRadius: 12,
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
||||
background: theme('colors-bgElevated'),
|
||||
padding: theme('spacing-lg'),
|
||||
border: `1px solid ${theme('colors-border')}`,
|
||||
borderRadius: theme('radius-sm'),
|
||||
textDecoration: 'none',
|
||||
transition: 'all 0.2s ease',
|
||||
display: 'block',
|
||||
|
||||
states: {
|
||||
hover: {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.15)'
|
||||
borderColor: theme('colors-borderActive'),
|
||||
}
|
||||
},
|
||||
|
||||
parts: {
|
||||
H2: {
|
||||
color: '#111827',
|
||||
margin: '0 0 8px 0',
|
||||
fontSize: 20,
|
||||
color: theme('colors-fg'),
|
||||
margin: `0 0 ${theme('spacing-sm')} 0`,
|
||||
fontSize: 18,
|
||||
fontWeight: 400,
|
||||
},
|
||||
P: {
|
||||
color: '#6b7280',
|
||||
color: theme('colors-fgMuted'),
|
||||
margin: 0,
|
||||
fontSize: 14,
|
||||
}
|
||||
|
|
@ -60,27 +60,27 @@ const ExampleCard = define('SSR_ExampleCard', {
|
|||
|
||||
export const IndexPage = ({ path }: any) => (
|
||||
<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>
|
||||
<ExampleCard href="/ssr/profile"
|
||||
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"
|
||||
title="Buttons"
|
||||
desc="Button component with intent, size, and disabled variants"
|
||||
desc="Intent, size, disabled states. Basic variant patterns."
|
||||
/>
|
||||
|
||||
<ExampleCard href="/ssr/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"
|
||||
title="Forms"
|
||||
desc="Form inputs with validation states, checkboxes, textareas, and buttons"
|
||||
desc="Inputs, validation states, checkboxes, textareas."
|
||||
/>
|
||||
</ExamplesGrid>
|
||||
</Layout>
|
||||
|
|
|
|||
9
examples/ssr/themes.tsx
Normal file
9
examples/ssr/themes.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { createThemes } from '../../src'
|
||||
import darkTheme from './darkTheme'
|
||||
import lightTheme from './lightTheme'
|
||||
|
||||
// Register themes and get a typed theme function in one step
|
||||
export const theme = createThemes({
|
||||
dark: darkTheme,
|
||||
light: lightTheme
|
||||
})
|
||||
|
|
@ -2,6 +2,70 @@ import type { JSX } from 'hono/jsx'
|
|||
import { type TagDef, UnitlessProps, NonStyleKeys } from './types'
|
||||
|
||||
export const styles: Record<string, Record<string, string>> = {}
|
||||
const themes: Record<string, Record<string, any>> = {}
|
||||
|
||||
// Type registry for theme variables (will be auto-populated)
|
||||
let registeredThemeKeys: Set<string> = new Set()
|
||||
|
||||
// Clear all registered styles
|
||||
export function clearStyles() {
|
||||
for (const key in styles) delete styles[key]
|
||||
}
|
||||
|
||||
// Register a theme with CSS custom properties
|
||||
export function createTheme<const T extends Record<string, string | number>>(
|
||||
name: string,
|
||||
values: T
|
||||
): T {
|
||||
themes[name] = values as Record<string, string>
|
||||
|
||||
// track for runtime validation
|
||||
Object.keys(values).forEach(key => registeredThemeKeys.add(key))
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
// Generate CSS for all registered themes
|
||||
export function themesToCSS(): string {
|
||||
let out: string[] = []
|
||||
|
||||
for (const [name, vars] of Object.entries(themes)) {
|
||||
out.push(`[data-theme="${name}"] {`)
|
||||
for (const [key, value] of Object.entries(vars)) {
|
||||
out.push(` --theme-${key}: ${value};`)
|
||||
}
|
||||
out.push(`}\n`)
|
||||
}
|
||||
|
||||
return out.join('\n')
|
||||
}
|
||||
|
||||
// Helper type to extract theme keys from multiple theme objects
|
||||
type ThemeKeys<T> = T extends Record<string, any> ? keyof T : never
|
||||
|
||||
// Create a typed themeVar function from your themes
|
||||
export function createThemedVar<T extends Record<string, any>>(_themes: T) {
|
||||
return function themeVar<K extends ThemeKeys<T[keyof T]>>(name: K): string {
|
||||
return `var(--theme-${name as string})`
|
||||
}
|
||||
}
|
||||
|
||||
// Simplified API: register multiple themes and get typed themeVar in one call
|
||||
type Theme = Record<string, string | number>
|
||||
export function createThemes<T extends Record<string, Theme>>(themes: T) {
|
||||
const registeredThemes = {} as T
|
||||
|
||||
for (const [name, values] of Object.entries(themes)) {
|
||||
(registeredThemes as any)[name] = createTheme(name, values)
|
||||
}
|
||||
|
||||
return createThemedVar(registeredThemes)
|
||||
}
|
||||
|
||||
// Generic themeVar (untyped fallback)
|
||||
export function themeVar(name: string): string {
|
||||
return `var(--theme-${name as string})`
|
||||
}
|
||||
|
||||
// All CSS styles inside <style></style.
|
||||
// Use w/ SSR: <Styles/>
|
||||
|
|
@ -29,6 +93,14 @@ function injectStylesInBrowser() {
|
|||
export function stylesToCSS(): string {
|
||||
let out: string[] = []
|
||||
|
||||
// Include theme CSS first
|
||||
const themeCSS = themesToCSS()
|
||||
if (themeCSS) {
|
||||
out.push(themeCSS)
|
||||
out.push('\n')
|
||||
}
|
||||
|
||||
// Then component styles
|
||||
for (const [selector, style] of Object.entries(styles)) {
|
||||
if (Object.keys(style).length === 0) continue
|
||||
|
||||
|
|
@ -197,9 +269,6 @@ function registerStyles(name: string, def: TagDef) {
|
|||
injectStylesInBrowser()
|
||||
}
|
||||
|
||||
// automatic names
|
||||
let anonComponents = 1
|
||||
|
||||
// module-level scoping
|
||||
export function createScope(scope: string) {
|
||||
return {
|
||||
|
|
@ -207,15 +276,15 @@ export function createScope(scope: string) {
|
|||
if (typeof nameOrDef === 'string')
|
||||
return define(`${scope}${nameOrDef === 'Root' ? '' : nameOrDef}`, defIfNamed)
|
||||
else
|
||||
return define(`${scope}Def${anonComponents++}`, nameOrDef as TagDef)
|
||||
return define(`${scope}${anonName(nameOrDef)}`, nameOrDef as TagDef)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
const name = defIfNamed ? (nameOrDef as string) : anonName(def)
|
||||
|
||||
if (styles[name]) throw `${name} is already defined! Must use unique names.`
|
||||
registerStyles(name, def)
|
||||
|
|
@ -231,5 +300,22 @@ export function define(nameOrDef: string | TagDef, defIfNamed?: TagDef) {
|
|||
}
|
||||
}
|
||||
|
||||
// automatic names
|
||||
const anonComponents: Record<string, number> = {}
|
||||
|
||||
// div tag -> Div1
|
||||
function anonName(def: TagDef): string {
|
||||
const base = (def.base ?? 'div')
|
||||
const count = (anonComponents[base] ??= 1)
|
||||
anonComponents[base] += 1
|
||||
return tagName(base) + String(count)
|
||||
}
|
||||
|
||||
// a => Anchor, nav => Nav
|
||||
function tagName(base: string): string {
|
||||
const capitalized = base.slice(0, 1).toUpperCase() + base.slice(1)
|
||||
return capitalized === 'A' ? 'Anchor' : capitalized
|
||||
}
|
||||
|
||||
// shortcut so you only have to import one thing, if you want
|
||||
define.Styles = Styles
|
||||
Loading…
Reference in New Issue
Block a user