Compare commits

..

1 Commits
main ... react

Author SHA1 Message Date
82ed03d521 react & preact support 2026-02-01 23:10:22 -08:00
8 changed files with 96 additions and 1139 deletions

View File

@ -11,6 +11,8 @@
Forge is a typed, local, variant-driven way to organize CSS and create Forge is a typed, local, variant-driven way to organize CSS and create
self-contained TSX components out of discrete parts. self-contained TSX components out of discrete parts.
Works with **React**, **Preact**, and **Hono JSX**.
## css problems ## css problems
- Styles are global and open - anything can override anything anywhere. - Styles are global and open - anything can override anything anywhere.
@ -28,6 +30,42 @@ self-contained TSX components out of discrete parts.
- Themes are easy. - Themes are easy.
- Errors and feedback are provided. - Errors and feedback are provided.
## setup
Install forge and configure your JSX runtime:
```bash
npm install @because/forge
```
Then set `jsxImportSource` in your `tsconfig.json`:
```json
// React
{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "react" } }
// Preact
{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" } }
// Hono
{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "hono/jsx" } }
```
Include the generated CSS in your app:
```tsx
// SSR - render <Styles /> in your <head>
import { Styles } from "@because/forge"
<head>
<Styles />
</head>
// Or get raw CSS string
import { stylesToCSS } from "@because/forge"
const css = stylesToCSS()
```
## examples ## examples
### styles ### styles

File diff suppressed because it is too large Load Diff

View File

@ -319,6 +319,44 @@ const MARKDOWN_CONTENT = `
Forge is a typed, local, variant-driven way to organize CSS and create Forge is a typed, local, variant-driven way to organize CSS and create
self-contained TSX components out of discrete parts. self-contained TSX components out of discrete parts.
Works with **React**, **Preact**, and **Hono JSX**.
## setup
Install forge and configure your JSX runtime:
\`\`\`bash
npm install @because/forge
\`\`\`
Then set \`jsxImportSource\` in your \`tsconfig.json\`:
\`\`\`tsx
// React
{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "react" } }
// Preact
{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" } }
// Hono
{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "hono/jsx" } }
\`\`\`
Include the generated CSS in your app:
\`\`\`tsx
// SSR - render <Styles /> in your <head>
import { Styles } from "@because/forge"
<head>
<Styles />
</head>
// Or get raw CSS string
import { stylesToCSS } from "@because/forge"
const css = stylesToCSS()
\`\`\`
## css problems ## css problems
- Styles are global and open - anything can override anything anywhere. - Styles are global and open - anything can override anything anywhere.

View File

@ -1,6 +1,6 @@
{ {
"name": "@because/forge", "name": "@because/forge",
"version": "0.0.3", "version": "0.0.1",
"type": "module", "type": "module",
"main": "src/index.tsx", "main": "src/index.tsx",
"module": "src/index.tsx", "module": "src/index.tsx",
@ -30,5 +30,16 @@
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5" "typescript": "^5"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"preact": {
"optional": true
},
"hono": {
"optional": true
}
} }
} }

View File

@ -286,18 +286,14 @@ export function define(nameOrDef: string | TagDef, defIfNamed?: TagDef) {
if (styles[`.${name}`]) throw `${name} is already defined! Must use unique names.` if (styles[`.${name}`]) throw `${name} is already defined! Must use unique names.`
registerStyles(name, def) registerStyles(name, def)
const currentProps: Record<string, any> = {}
const Root = makeComponent(name, def, currentProps)
return (props: Record<string, any>) => { return (props: Record<string, any>) => {
for (const key in currentProps) delete currentProps[key] const parts: Record<string, Function> = {}
Object.assign(currentProps, props)
const parts: Record<string, Function> = { Root }
for (const [part] of Object.entries(def.parts ?? {})) for (const [part] of Object.entries(def.parts ?? {}))
parts[part] = makeComponent(name, def, props, part) parts[part] = makeComponent(name, def, props, part)
return def.render?.({ props, parts }) ?? <Root {...props}>{props.children}</Root> parts.Root = makeComponent(name, def, props)
return def.render?.({ props, parts }) ?? <parts.Root {...props}>{props.children}</parts.Root>
} }
} }

View File

@ -1,13 +1,13 @@
import { define } from '../index' import { stylesToCSS } from '../index'
// Uses Hono JSX's toString() for tests - framework-specific
export function renderToString(jsx: any): string { export function renderToString(jsx: any): string {
return jsx.toString() return jsx.toString()
} }
// Get generated CSS directly from styles registry
export function getStylesCSS(): string { export function getStylesCSS(): string {
const StylesComponent = define.Styles return stylesToCSS()
const result = StylesComponent() as any
return result.props.dangerouslySetInnerHTML.__html as string
} }
export function parseCSS(css: string): Record<string, Record<string, string>> { export function parseCSS(css: string): Record<string, Record<string, string>> {

View File

@ -31,24 +31,7 @@ export type TagDef = {
rowGap?: number | string rowGap?: number | string
gap?: number | string gap?: number | string
// multi-column layout
columns?: string
columnCount?: number | 'auto'
columnWidth?: number | string
columnRule?: string
columnRuleColor?: string
columnRuleStyle?: 'none' | 'solid' | 'dashed' | 'dotted' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset' | 'hidden'
columnRuleWidth?: number | string
columnFill?: 'auto' | 'balance' | 'balance-all'
columnSpan?: 'none' | 'all'
contain?: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint' contain?: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint'
contentVisibility?: 'visible' | 'hidden' | 'auto'
// container queries
container?: string
containerType?: 'normal' | 'size' | 'inline-size'
containerName?: string
display?: 'block' | 'inline' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'inline-grid' | 'flow-root' | 'none' | 'contents' | 'table' | 'table-row' | 'table-cell' display?: 'block' | 'inline' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'inline-grid' | 'flow-root' | 'none' | 'contents' | 'table' | 'table-row' | 'table-cell'
@ -150,14 +133,6 @@ export type TagDef = {
// visual/theme-related // visual/theme-related
animation?: string animation?: string
animationName?: string
animationDuration?: string
animationTimingFunction?: string
animationDelay?: string
animationIterationCount?: number | 'infinite'
animationDirection?: 'normal' | 'reverse' | 'alternate' | 'alternate-reverse'
animationFillMode?: 'none' | 'forwards' | 'backwards' | 'both'
animationPlayState?: 'running' | 'paused'
appearance?: 'none' | 'auto' | 'button' | 'textfield' | 'searchfield' | 'textarea' | 'checkbox' | 'radio' appearance?: 'none' | 'auto' | 'button' | 'textfield' | 'searchfield' | 'textarea' | 'checkbox' | 'radio'
backdropFilter?: string backdropFilter?: string
@ -195,12 +170,6 @@ export type TagDef = {
borderTopStyle?: 'none' | 'solid' | 'dashed' | 'dotted' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset' | 'hidden' borderTopStyle?: 'none' | 'solid' | 'dashed' | 'dotted' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset' | 'hidden'
borderTopWidth?: number | string borderTopWidth?: number | string
borderWidth?: number | string borderWidth?: number | string
borderImage?: string
borderImageSource?: string
borderImageSlice?: number | string
borderImageWidth?: number | string
borderImageOutset?: number | string
borderImageRepeat?: 'stretch' | 'repeat' | 'round' | 'space' | string
// table-ish // table-ish
borderCollapse?: 'collapse' | 'separate' borderCollapse?: 'collapse' | 'separate'
@ -213,14 +182,10 @@ export type TagDef = {
clipPath?: string clipPath?: string
color?: string color?: string
colorScheme?: 'normal' | 'light' | 'dark' | 'light dark' | string
content?: string content?: string
counterReset?: string
counterIncrement?: string
cursor?: 'auto' | 'default' | 'none' | 'context-menu' | 'help' | 'pointer' | 'progress' | 'wait' | 'cell' | 'crosshair' | 'text' | 'vertical-text' | 'alias' | 'copy' | 'move' | 'no-drop' | 'not-allowed' | 'grab' | 'grabbing' | 'e-resize' | 'n-resize' | 'ne-resize' | 'nw-resize' | 's-resize' | 'se-resize' | 'sw-resize' | 'w-resize' | 'ew-resize' | 'ns-resize' | 'nesw-resize' | 'nwse-resize' | 'col-resize' | 'row-resize' | 'all-scroll' | 'zoom-in' | 'zoom-out' cursor?: 'auto' | 'default' | 'none' | 'context-menu' | 'help' | 'pointer' | 'progress' | 'wait' | 'cell' | 'crosshair' | 'text' | 'vertical-text' | 'alias' | 'copy' | 'move' | 'no-drop' | 'not-allowed' | 'grab' | 'grabbing' | 'e-resize' | 'n-resize' | 'ne-resize' | 'nw-resize' | 's-resize' | 'se-resize' | 'sw-resize' | 'w-resize' | 'ew-resize' | 'ns-resize' | 'nesw-resize' | 'nwse-resize' | 'col-resize' | 'row-resize' | 'all-scroll' | 'zoom-in' | 'zoom-out'
filter?: string filter?: string
imageRendering?: 'auto' | 'crisp-edges' | 'pixelated'
font?: string font?: string
fontFamily?: string fontFamily?: string
@ -243,7 +208,6 @@ export type TagDef = {
mixBlendMode?: 'normal' | 'multiply' | 'screen' | 'overlay' | 'darken' | 'lighten' | 'color-dodge' | 'color-burn' | 'hard-light' | 'soft-light' | 'difference' | 'exclusion' | 'hue' | 'saturation' | 'color' | 'luminosity' mixBlendMode?: 'normal' | 'multiply' | 'screen' | 'overlay' | 'darken' | 'lighten' | 'color-dodge' | 'color-burn' | 'hard-light' | 'soft-light' | 'difference' | 'exclusion' | 'hue' | 'saturation' | 'color' | 'luminosity'
objectFit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down' objectFit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down'
objectPosition?: string
opacity?: number opacity?: number
@ -283,37 +247,15 @@ export type TagDef = {
wordBreak?: 'normal' | 'break-all' | 'keep-all' | 'break-word' wordBreak?: 'normal' | 'break-all' | 'keep-all' | 'break-word'
wordSpacing?: number | string wordSpacing?: number | string
wordWrap?: 'normal' | 'break-word' | 'anywhere' wordWrap?: 'normal' | 'break-word' | 'anywhere'
textRendering?: 'auto' | 'optimizeSpeed' | 'optimizeLegibility' | 'geometricPrecision'
textWrap?: 'wrap' | 'nowrap' | 'balance' | 'pretty' | 'stable'
// CSS shapes
shapeOutside?: string
shapeMargin?: number | string
shapeImageThreshold?: number
// fragmentation / breaks
breakBefore?: 'auto' | 'avoid' | 'always' | 'all' | 'avoid-page' | 'page' | 'left' | 'right' | 'recto' | 'verso' | 'avoid-column' | 'column' | 'avoid-region' | 'region'
breakAfter?: 'auto' | 'avoid' | 'always' | 'all' | 'avoid-page' | 'page' | 'left' | 'right' | 'recto' | 'verso' | 'avoid-column' | 'column' | 'avoid-region' | 'region'
breakInside?: 'auto' | 'avoid' | 'avoid-page' | 'avoid-column' | 'avoid-region'
orphans?: number
widows?: number
transform?: string transform?: string
transformOrigin?: string transformOrigin?: string
transformStyle?: 'flat' | 'preserve-3d' transformStyle?: 'flat' | 'preserve-3d'
transformBox?: 'content-box' | 'border-box' | 'fill-box' | 'stroke-box' | 'view-box'
rotate?: string
scale?: string | number
translate?: string
perspective?: number | string perspective?: number | string
perspectiveOrigin?: string perspectiveOrigin?: string
backfaceVisibility?: 'visible' | 'hidden' backfaceVisibility?: 'visible' | 'hidden'
transition?: string transition?: string
transitionProperty?: string
transitionDuration?: string
transitionTimingFunction?: string
transitionDelay?: string
visibility?: 'visible' | 'hidden' | 'collapse' visibility?: 'visible' | 'hidden' | 'collapse'
willChange?: 'auto' | 'scroll-position' | 'contents' willChange?: 'auto' | 'scroll-position' | 'contents'
@ -358,8 +300,6 @@ export const UnitlessProps = new Set([
'opacity', 'opacity',
'order', 'order',
'orphans', 'orphans',
'scale',
'shapeImageThreshold',
'widows', 'widows',
'zIndex' 'zIndex'
]) ])

View File

@ -6,6 +6,7 @@
"module": "Preserve", "module": "Preserve",
"moduleDetection": "force", "moduleDetection": "force",
"jsx": "react-jsx", "jsx": "react-jsx",
// Hono for development - consumers override with their JSX runtime (react, preact, etc.)
"jsxImportSource": "hono/jsx", "jsxImportSource": "hono/jsx",
"allowJs": true, "allowJs": true,