This commit is contained in:
Chris Wanstrath 2025-12-26 21:41:36 -08:00
parent 097dae4f2a
commit 966a0ce4b0
8 changed files with 571 additions and 530 deletions

View File

@ -6,16 +6,15 @@ Example:
import { define } from "forge" import { define } from "forge"
export const Button = define("button", { export const Button = define("button", {
layout: { base: "button",
padding: 20,
}, padding: 20,
look: { background: "blue",
background: "blue",
},
variants: { variants: {
kind: { kind: {
danger: { look: { background: "red" } }, danger: { background: "red" },
warning: { look: { background: "yellow" } }, warning: { background: "yellow" },
} }
}, },
}) })
@ -26,35 +25,31 @@ export const Button = define("button", {
<Button kind="warning">Click me?</Button> <Button kind="warning">Click me?</Button>
export const Profile = define("div", { export const Profile = define("div", {
layout: { padding: 50,
padding: 50, background: "red",
},
look: {
background: "red",
},
parts: { parts: {
header: { layout: { display: "flex" } }, Header: { display: "flex" },
avatar: { layout: { width: 50 } }, Avatar: { base: 'img', width: 50 },
bio: { look: { color: "gray" } }, Bio: { color: "gray" },
}, },
variants: { variants: {
size: { size: {
small: { small: {
parts: { avatar: { layout: { width: 20 }}} parts: { Avatar: { width: 20 }}
} }
} }
}, },
render({ props, parts: { root, header, avatar, bio } }) { render({ props, parts: { Root, Header, Avatar, Bio } }) {
return ( return (
<root> <Root>
<header> <Header>
<avatar src={props.pic} /> <Avatar src={props.pic} />
<bio>{props.bio}</bio> <Bio>{props.bio}</Bio>
</header> </Header>
</root> </Root>
) )
}, },
}) })

View File

@ -4,109 +4,80 @@ import { Layout, ExampleSection } from './helpers'
const Button = define('Button', { const Button = define('Button', {
base: 'button', base: 'button',
layout: { padding: "12px 24px",
padding: "12px 24px", display: "inline-flex",
display: "inline-flex", alignItems: "center",
alignItems: "center", justifyContent: "center",
justifyContent: "center", gap: 8,
gap: 8, background: "#3b82f6",
}, color: "white",
border: "none",
look: { borderRadius: 8,
background: "#3b82f6", fontSize: 16,
color: "white", fontWeight: 600,
border: "none", cursor: "pointer",
borderRadius: 8, transition: "all 0.2s ease",
fontSize: 16, userSelect: "none",
fontWeight: 600, boxShadow: "0 4px 6px rgba(59, 130, 246, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1)",
cursor: "pointer", transform: "translateY(0)",
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: { states: {
":not(:disabled):hover": { ":not(:disabled):hover": {
look: { transform: 'translateY(-2px) !important',
transform: 'translateY(-2px) !important', filter: 'brightness(1.05)'
filter: 'brightness(1.05)'
}
}, },
":not(:disabled):active": { ":not(:disabled):active": {
look: { transform: 'translateY(1px) !important',
transform: 'translateY(1px) !important', boxShadow: '0 2px 3px rgba(0, 0, 0, 0.2) !important'
boxShadow: '0 2px 3px rgba(0, 0, 0, 0.2) !important'
}
}, },
}, },
variants: { variants: {
intent: { intent: {
primary: { primary: {
look: { background: "#3b82f6",
background: "#3b82f6", color: "white",
color: "white", boxShadow: "0 4px 6px rgba(59, 130, 246, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1)",
boxShadow: "0 4px 6px rgba(59, 130, 246, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1)",
},
}, },
secondary: { secondary: {
look: { background: "#f3f4f6",
background: "#f3f4f6", color: "#374151",
color: "#374151", boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06)",
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06)",
},
}, },
danger: { danger: {
look: { background: "#ef4444",
background: "#ef4444", color: "white",
color: "white", boxShadow: "0 4px 6px rgba(239, 68, 68, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1)",
boxShadow: "0 4px 6px rgba(239, 68, 68, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1)",
},
}, },
ghost: { ghost: {
look: { background: "transparent",
background: "transparent", color: "#aaa",
color: "#aaa", boxShadow: "0 4px 6px rgba(0, 0, 0, 0.2), 0 2px 4px rgba(0, 0, 0, 0.1)",
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.2), 0 2px 4px rgba(0, 0, 0, 0.1)", border: "1px solid #eee",
border: "1px solid #eee",
},
}, },
}, },
size: { size: {
small: { small: {
layout: { padding: "8px 16px",
padding: "8px 16px", fontSize: 14,
},
look: {
fontSize: 14,
},
}, },
large: { large: {
layout: { padding: "16px 32px",
padding: "16px 32px", fontSize: 18,
},
look: {
fontSize: 18,
},
}, },
}, },
disabled: { disabled: {
look: { opacity: 0.5,
opacity: 0.5, cursor: "not-allowed",
cursor: "not-allowed",
},
}, },
}, },
}) })
const ButtonRow = define('ButtonRow', { const ButtonRow = define('ButtonRow', {
layout: { display: 'flex',
display: 'flex', gap: 16,
gap: 16, flexWrap: 'wrap',
flexWrap: 'wrap', alignItems: 'center',
alignItems: 'center',
}
}) })
export const ButtonExamplesPage = () => ( export const ButtonExamplesPage = () => (

View File

@ -2,48 +2,35 @@ import { define, Styles } from '../src'
export const Body = define('Body', { export const Body = define('Body', {
base: 'body', base: 'body',
layout: {
margin: 0,
padding: '40px 20px',
}, margin: 0,
look: { padding: '40px 20px',
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif", fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
background: '#f3f4f6', background: '#f3f4f6',
}
}) })
const Container = define('Container', { const Container = define('Container', {
layout: { maxWidth: 1200,
maxWidth: 1200, margin: '0 auto'
margin: '0 auto'
}
}) })
export const Header = define('Header', { export const Header = define('Header', {
base: 'h1', base: 'h1',
layout: {
marginBottom: 40, marginBottom: 40,
}, color: '#111827'
look: {
color: '#111827'
}
}) })
export const ExampleSection = define('ExampleSection', { export const ExampleSection = define('ExampleSection', {
layout: { marginBottom: 40,
marginBottom: 40
},
parts: { parts: {
Header: { Header: {
base: 'h2', base: 'h2',
layout: {
marginBottom: 16 marginBottom: 16,
}, color: '#374151',
look: { fontSize: 18
color: '#374151',
fontSize: 18
}
} }
}, },
render({ props, parts: { Root, Header } }) { render({ props, parts: { Root, Header } }) {

View File

@ -2,37 +2,29 @@ import { define } from '../src'
import { Layout, ExampleSection } from './helpers' import { Layout, ExampleSection } from './helpers'
const Tabs = define('Tabs', { const Tabs = define('Tabs', {
layout: { display: 'flex',
display: 'flex', gap: 0,
gap: 0, borderBottom: '2px solid #e5e7eb',
},
look: {
borderBottom: '2px solid #e5e7eb',
},
parts: { parts: {
Tab: { Tab: {
base: 'button', base: 'button',
layout: {
padding: '12px 24px', padding: '12px 24px',
position: 'relative', position: 'relative',
marginBottom: -2, marginBottom: -2,
}, background: 'transparent',
look: { border: 'none',
background: 'transparent', borderBottom: '2px solid transparent',
border: 'none', color: '#6b7280',
borderBottom: '2px solid transparent', fontSize: 14,
color: '#6b7280', fontWeight: 500,
fontSize: 14, cursor: 'pointer',
fontWeight: 500, transition: 'all 0.2s ease',
cursor: 'pointer',
transition: 'all 0.2s ease',
},
states: { states: {
':hover': { ':hover': {
look: { color: '#111827',
color: '#111827',
}
} }
} }
} }
@ -42,10 +34,8 @@ const Tabs = define('Tabs', {
active: { active: {
parts: { parts: {
Tab: { Tab: {
look: { color: '#3b82f6',
color: '#3b82f6', borderBottom: '2px solid #3b82f6',
borderBottom: '2px solid #3b82f6',
},
} }
} }
} }
@ -69,34 +59,28 @@ const Tabs = define('Tabs', {
}) })
const Pills = define('Pills', { const Pills = define('Pills', {
layout: { display: 'flex',
display: 'flex', gap: 8,
gap: 8, flexWrap: 'wrap',
flexWrap: 'wrap',
},
parts: { parts: {
Pill: { Pill: {
base: 'button', base: 'button',
layout: {
padding: '8px 16px', padding: '8px 16px',
}, background: '#f3f4f6',
look: { border: 'none',
background: '#f3f4f6', borderRadius: 20,
border: 'none', color: '#6b7280',
borderRadius: 20, fontSize: 14,
color: '#6b7280', fontWeight: 500,
fontSize: 14, cursor: 'pointer',
fontWeight: 500, transition: 'all 0.2s ease',
cursor: 'pointer',
transition: 'all 0.2s ease',
},
states: { states: {
':hover': { ':hover': {
look: { background: '#e5e7eb',
background: '#e5e7eb', color: '#111827',
color: '#111827',
}
} }
} }
} }
@ -106,16 +90,12 @@ const Pills = define('Pills', {
active: { active: {
parts: { parts: {
Pill: { Pill: {
look: { background: '#3b82f6',
background: '#3b82f6', color: 'white',
color: 'white',
},
states: { states: {
':hover': { ':hover': {
look: { background: '#2563eb',
background: '#2563eb', color: 'white',
color: 'white',
}
} }
} }
} }
@ -141,52 +121,43 @@ const Pills = define('Pills', {
}) })
const VerticalNav = define('VerticalNav', { const VerticalNav = define('VerticalNav', {
layout: { display: 'flex',
display: 'flex', flexDirection: 'column',
flexDirection: 'column', gap: 4,
gap: 4, width: 240,
width: 240,
},
parts: { parts: {
NavItem: { NavItem: {
base: 'a', base: 'a',
layout: {
padding: '12px 16px', padding: '12px 16px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 12, gap: 12,
},
look: { background: 'transparent',
background: 'transparent', borderRadius: 8,
borderRadius: 8, color: '#6b7280',
color: '#6b7280', fontSize: 14,
fontSize: 14, fontWeight: 500,
fontWeight: 500, textDecoration: 'none',
textDecoration: 'none', cursor: 'pointer',
cursor: 'pointer', transition: 'all 0.2s ease',
transition: 'all 0.2s ease',
},
states: { states: {
':hover': { ':hover': {
look: { background: '#f3f4f6',
background: '#f3f4f6', color: '#111827',
color: '#111827',
}
} }
} }
}, },
Icon: { Icon: {
layout: { width: 20,
width: 20, height: 20,
height: 20, display: 'flex',
display: 'flex', alignItems: 'center',
alignItems: 'center', justifyContent: 'center',
justifyContent: 'center', fontSize: 18,
},
look: {
fontSize: 18,
}
} }
}, },
@ -194,16 +165,12 @@ const VerticalNav = define('VerticalNav', {
active: { active: {
parts: { parts: {
NavItem: { NavItem: {
look: { background: '#eff6ff',
background: '#eff6ff', color: '#3b82f6',
color: '#3b82f6',
},
states: { states: {
':hover': { ':hover': {
look: { background: '#dbeafe',
background: '#dbeafe', color: '#2563eb',
color: '#2563eb',
}
} }
} }
} }
@ -230,43 +197,35 @@ const VerticalNav = define('VerticalNav', {
}) })
const Breadcrumbs = define('Breadcrumbs', { const Breadcrumbs = define('Breadcrumbs', {
layout: { display: 'flex',
display: 'flex', alignItems: 'center',
alignItems: 'center', gap: 8,
gap: 8, flexWrap: 'wrap',
flexWrap: 'wrap',
},
parts: { parts: {
Item: { Item: {
base: 'a', base: 'a',
look: {
color: '#6b7280', color: '#6b7280',
fontSize: 14, fontSize: 14,
textDecoration: 'none', textDecoration: 'none',
transition: 'color 0.2s ease', transition: 'color 0.2s ease',
},
states: { states: {
':hover': { ':hover': {
look: { color: '#3b82f6',
color: '#3b82f6',
}
} }
} }
}, },
Separator: { Separator: {
look: { color: '#d1d5db',
color: '#d1d5db', fontSize: 14,
fontSize: 14, userSelect: 'none',
userSelect: 'none',
}
}, },
Current: { Current: {
look: { color: '#111827',
color: '#111827', fontSize: 14,
fontSize: 14, fontWeight: 500,
fontWeight: 500,
}
} }
}, },

View File

@ -4,167 +4,129 @@ import { Layout, ExampleSection } from './helpers'
const UserProfile = define('UserProfile', { const UserProfile = define('UserProfile', {
base: 'div', base: 'div',
layout: { padding: 24,
padding: 24, maxWidth: 600,
maxWidth: 600, margin: "0 auto",
margin: "0 auto", background: "white",
}, borderRadius: 12,
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
look: {
background: "white",
borderRadius: 12,
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
},
parts: { parts: {
Header: { Header: {
layout: { display: "flex",
display: "flex", alignItems: "center",
alignItems: "center", gap: 16,
gap: 16, marginBottom: 16,
marginBottom: 16,
},
}, },
Avatar: { Avatar: {
base: 'img', base: 'img',
layout: { width: 64,
width: 64, height: 64,
height: 64, borderRadius: "50%",
}, objectFit: "cover",
look: { border: "3px solid #e5e7eb",
borderRadius: "50%",
objectFit: "cover",
border: "3px solid #e5e7eb",
},
}, },
Info: { Info: {
layout: { flex: 1,
flex: 1,
},
}, },
Name: { Name: {
layout: { marginBottom: 4,
marginBottom: 4, fontSize: 20,
}, fontWeight: 600,
look: { color: "#111827",
fontSize: 20,
fontWeight: 600,
color: "#111827",
},
}, },
Handle: { Handle: {
look: { fontSize: 14,
fontSize: 14, color: "#6b7280",
color: "#6b7280",
},
}, },
Bio: { Bio: {
layout: { marginBottom: 16,
marginBottom: 16, width: "100%",
width: "100%", fontSize: 14,
}, lineHeight: 1.6,
look: { color: "#374151",
fontSize: 14, wordWrap: "break-word",
lineHeight: 1.6,
color: "#374151",
wordWrap: "break-word",
},
}, },
Stats: { Stats: {
layout: { display: "flex",
display: "flex", gap: 24,
gap: 24, paddingTop: 16,
paddingTop: 16, borderTop: "1px solid #e5e7eb",
},
look: {
borderTop: "1px solid #e5e7eb",
},
}, },
Stat: { Stat: {
layout: { display: "flex",
display: "flex", flexDirection: "column",
flexDirection: "column", gap: 4,
gap: 4,
},
}, },
StatValue: { StatValue: {
look: { fontSize: 18,
fontSize: 18, fontWeight: 600,
fontWeight: 600, color: "#111827",
color: "#111827",
},
}, },
StatLabel: { StatLabel: {
look: { fontSize: 12,
fontSize: 12, color: "#6b7280",
color: "#6b7280", textTransform: "uppercase",
textTransform: "uppercase",
},
}, },
}, },
variants: { variants: {
size: { size: {
compact: { compact: {
layout: { padding: 16, maxWidth: 300 }, padding: 16,
maxWidth: 300,
parts: { parts: {
Avatar: { layout: { width: 48, height: 48 } }, Avatar: { width: 48, height: 48 },
Name: { look: { fontSize: 16 } }, Name: { fontSize: 16 },
Bio: { look: { fontSize: 13 } }, Bio: { fontSize: 13 },
}, },
}, },
skinny: { skinny: {
layout: { padding: 20, maxWidth: 125 }, padding: 20,
maxWidth: 125,
parts: { parts: {
Header: { Header: {
layout: { flexDirection: "column",
flexDirection: "column", alignItems: "center",
alignItems: "center", gap: 12,
gap: 12,
}
}, },
Avatar: { layout: { width: 80, height: 80 } }, Avatar: { width: 80, height: 80 },
Info: { layout: { flex: 0, width: "100%" } }, Info: { flex: 0, width: "100%" },
Name: { look: { textAlign: "center", fontSize: 18 } }, Name: { textAlign: "center", fontSize: 18 },
Handle: { look: { textAlign: "center" } }, Handle: { textAlign: "center" },
Bio: { look: { textAlign: "center", fontSize: 13 } }, Bio: { textAlign: "center", fontSize: 13 },
Stats: { Stats: {
layout: { flexDirection: "column",
flexDirection: "column", gap: 16,
gap: 16,
}
}, },
Stat: { layout: { alignItems: "center" } }, Stat: { alignItems: "center" },
}, },
}, },
large: { large: {
layout: { padding: 32, maxWidth: 800 }, padding: 32,
maxWidth: 800,
parts: { parts: {
Avatar: { layout: { width: 96, height: 96 } }, Avatar: { width: 96, height: 96 },
Name: { look: { fontSize: 24 } }, Name: { fontSize: 24 },
}, },
}, },
}, },
verified: { verified: {
parts: { parts: {
Avatar: { Avatar: {
look: { border: "3px solid #3b82f6",
border: "3px solid #3b82f6",
},
}, },
}, },
}, },
theme: { theme: {
dark: { dark: {
look: { background: "#1f2937",
background: "#1f2937",
},
parts: { parts: {
Name: { look: { color: "#f9fafb" } }, Name: { color: "#f9fafb" },
Handle: { look: { color: "#9ca3af" } }, Handle: { color: "#9ca3af" },
Bio: { look: { color: "#d1d5db" } }, Bio: { color: "#d1d5db" },
Stats: { look: { borderTop: "1px solid #374151" } }, Stats: { borderTop: "1px solid #374151" },
StatValue: { look: { color: "#f9fafb" } }, StatValue: { color: "#f9fafb" },
}, },
}, },
}, },

View File

@ -1,5 +1,5 @@
import type { JSX } from 'hono/jsx' import type { JSX } from 'hono/jsx'
import { type TagDef, UnitlessProps } from './types' import { type TagDef, UnitlessProps, NonStyleKeys } from './types'
export const styles: Record<string, Record<string, string>> = {} export const styles: Record<string, Record<string, string>> = {}
export const Styles = () => <style dangerouslySetInnerHTML={{ __html: stylesToCSS(styles) }} /> export const Styles = () => <style dangerouslySetInnerHTML={{ __html: stylesToCSS(styles) }} />
@ -45,8 +45,10 @@ function camelToDash(name: string): string {
function makeStyle(def: TagDef) { function makeStyle(def: TagDef) {
const style: Record<string, string> = {} const style: Record<string, string> = {}
for (const [name, value] of Object.entries(Object.assign({}, def.layout ?? {}, def.look ?? {}))) for (const [name, value] of Object.entries(def)) {
if (NonStyleKeys.has(name)) continue
style[camelToDash(name)] = `${typeof value === 'number' && !UnitlessProps.has(name) ? `${value}px` : value}` style[camelToDash(name)] = `${typeof value === 'number' && !UnitlessProps.has(name) ? `${value}px` : value}`
}
return style return style
} }
@ -57,28 +59,37 @@ function makeComponent(baseName: string, rootDef: TagDef, rootProps: Record<stri
const base = def.base ?? 'div' const base = def.base ?? 'div'
const Tag = (base) as keyof JSX.IntrinsicElements const Tag = (base) as keyof JSX.IntrinsicElements
const classNames = [makeClassName(baseName, partName)] return ({ children, ...props }: { children: any, [key: string]: any }) => {
const classNames = [makeClassName(baseName, partName)]
for (const [key, value] of Object.entries(rootProps)) { const allProps = { ...rootProps, ...props }
const variantConfig = rootDef.variants?.[key]
if (!variantConfig) continue
const variantName = key for (const [key, value] of Object.entries(allProps)) {
const variantKey = value const variantConfig = rootDef.variants?.[key]
if (!variantConfig) continue
let variantDef: TagDef | undefined // Remove variant prop from being passed to HTML element
if ('parts' in variantConfig || 'layout' in variantConfig || 'look' in variantConfig) { delete props[key]
if (value === true) variantDef = variantConfig as TagDef
} else { const variantName = key
variantDef = (variantConfig as Record<string, TagDef>)[value as string] const variantKey = value
let variantDef: TagDef | undefined
// Distinguish boolean variants from keyed variants:
// - Boolean variants: component({ variant: true }) → variantConfig is a TagDef
// - Keyed variants: component({ variant: 'key' }) → variantConfig[key] is a TagDef
if (value === true) {
variantDef = variantConfig as TagDef
} else if (typeof value === 'string') {
variantDef = (variantConfig as Record<string, TagDef>)[value]
}
if (!variantDef) continue
classNames.push(variantKey === true ? variantName : `${variantName}-${variantKey}`)
} }
if (!variantDef) continue
classNames.push(variantKey === true ? variantName : `${variantName}-${variantKey}`) return <Tag class={classNames.join(' ')} {...props}>{children}</Tag>
} }
return ({ children, ...props }: { children: any, [key: string]: any }) =>
<Tag class={classNames.join(' ')} {...props}>{children}</Tag>
} }
// adds CSS styles for tag definition // adds CSS styles for tag definition
@ -98,13 +109,17 @@ function registerStyles(name: string, def: TagDef) {
} }
for (const [variantName, variantConfig] of Object.entries(def.variants ?? {})) { for (const [variantName, variantConfig] of Object.entries(def.variants ?? {})) {
// Check if it's a boolean variant (has layout/look/parts directly) or a keyed variant // Detect boolean vs keyed variants by checking if config has structural keys or looks like a TagDef
if ('parts' in variantConfig || 'layout' in variantConfig || 'look' in variantConfig) { const isBooleanVariant = 'parts' in variantConfig || 'styles' in variantConfig || 'states' in variantConfig ||
// Boolean variant - treat variantConfig as TagDef // If first key is camelCase or contains CSS-like properties, treat as boolean variant
Object.keys(variantConfig).some(k => k !== k.toLowerCase() || typeof (variantConfig as any)[k] !== 'object')
if (isBooleanVariant) {
// Boolean variant - variantConfig is a TagDef
const variantDef = variantConfig as TagDef const variantDef = variantConfig as TagDef
const baseClassName = makeClassName(name) const baseClassName = makeClassName(name)
const className = `${baseClassName}.${variantName}` const className = `${baseClassName}.${variantName}`
styles[className] ??= makeStyle({ layout: variantDef.layout, look: variantDef.look }) styles[className] ??= makeStyle(variantDef)
for (const [state, style] of Object.entries(variantDef.states ?? {})) for (const [state, style] of Object.entries(variantDef.states ?? {}))
styles[`${className}${state}`] = makeStyle(style) styles[`${className}${state}`] = makeStyle(style)
@ -119,7 +134,7 @@ function registerStyles(name: string, def: TagDef) {
// Keyed variant - iterate over the keys // Keyed variant - iterate over the keys
for (const [variantKey, variantDef] of Object.entries(variantConfig as Record<string, TagDef>)) { for (const [variantKey, variantDef] of Object.entries(variantConfig as Record<string, TagDef>)) {
const className = makeClassName(name, undefined, variantName, variantKey) const className = makeClassName(name, undefined, variantName, variantKey)
styles[className] ??= makeStyle({ layout: variantDef.layout, look: variantDef.look }) styles[className] ??= makeStyle(variantDef)
for (const [state, style] of Object.entries(variantDef.states ?? {})) for (const [state, style] of Object.entries(variantDef.states ?? {}))
styles[`${className}${state}`] = makeStyle(style) styles[`${className}${state}`] = makeStyle(style)

View File

@ -5,7 +5,7 @@ import { renderToString, getStylesCSS, parseCSS } from './test_helpers'
describe('define - basic functionality', () => { describe('define - basic functionality', () => {
test('creates a component function', () => { test('creates a component function', () => {
const Component = define({ const Component = define({
layout: { display: 'flex' } display: 'flex'
}) })
expect(typeof Component).toBe('function') expect(typeof Component).toBe('function')
@ -13,7 +13,7 @@ describe('define - basic functionality', () => {
test('component returns a JSX element', () => { test('component returns a JSX element', () => {
const Component = define({ const Component = define({
layout: { display: 'flex' } display: 'flex'
}) })
const result = Component({}) const result = Component({})
@ -23,7 +23,7 @@ describe('define - basic functionality', () => {
test('applies className to rendered element', () => { test('applies className to rendered element', () => {
const Component = define('MyComponent', { const Component = define('MyComponent', {
layout: { display: 'flex' } display: 'flex'
}) })
const html = renderToString(Component({})) const html = renderToString(Component({}))
@ -31,8 +31,8 @@ describe('define - basic functionality', () => {
}) })
test('generates unique anonymous component names', () => { test('generates unique anonymous component names', () => {
const Component1 = define({ layout: { display: 'flex' } }) const Component1 = define({ display: 'flex' })
const Component2 = define({ layout: { display: 'block' } }) const Component2 = define({ display: 'block' })
const html1 = renderToString(Component1({})) const html1 = renderToString(Component1({}))
const html2 = renderToString(Component2({})) const html2 = renderToString(Component2({}))
@ -45,7 +45,7 @@ describe('define - basic functionality', () => {
test('renders default div element', () => { test('renders default div element', () => {
const Component = define('DivTest', { const Component = define('DivTest', {
layout: { display: 'flex' } display: 'flex'
}) })
const html = renderToString(Component({})) const html = renderToString(Component({}))
@ -56,7 +56,7 @@ describe('define - basic functionality', () => {
test('respects custom base element', () => { test('respects custom base element', () => {
const Component = define('ButtonTest', { const Component = define('ButtonTest', {
base: 'button', base: 'button',
look: { color: 'blue' } color: 'blue'
}) })
const html = renderToString(Component({})) const html = renderToString(Component({}))
@ -83,11 +83,9 @@ describe('define - basic functionality', () => {
describe('CSS generation - camelCase to kebab-case', () => { describe('CSS generation - camelCase to kebab-case', () => {
test('converts camelCase properties to kebab-case', () => { test('converts camelCase properties to kebab-case', () => {
define('CamelTest', { define('CamelTest', {
layout: { flexDirection: 'column',
flexDirection: 'column', alignItems: 'center',
alignItems: 'center', justifyContent: 'space-between'
justifyContent: 'space-between'
}
}) })
const css = getStylesCSS() const css = getStylesCSS()
@ -98,10 +96,8 @@ describe('CSS generation - camelCase to kebab-case', () => {
test('handles consecutive capital letters', () => { test('handles consecutive capital letters', () => {
define('ConsecutiveTest', { define('ConsecutiveTest', {
look: { backgroundColor: 'red',
backgroundColor: 'red', borderRadius: 5
borderRadius: 5
}
}) })
const css = getStylesCSS() const css = getStylesCSS()
@ -113,12 +109,10 @@ describe('CSS generation - camelCase to kebab-case', () => {
describe('CSS generation - numeric values and units', () => { describe('CSS generation - numeric values and units', () => {
test('adds px unit to numeric layout values', () => { test('adds px unit to numeric layout values', () => {
define('NumericTest', { define('NumericTest', {
layout: { width: 100,
width: 100, height: 200,
height: 200, padding: 16,
padding: 16, margin: 8
margin: 8
}
}) })
const styles = parseCSS(getStylesCSS()) const styles = parseCSS(getStylesCSS())
@ -130,12 +124,10 @@ describe('CSS generation - numeric values and units', () => {
test('preserves string values without adding px', () => { test('preserves string values without adding px', () => {
define('StringTest', { define('StringTest', {
layout: { width: '100%',
width: '100%', height: 'auto',
height: 'auto', margin: '0 auto',
margin: '0 auto', padding: '1rem'
padding: '1rem'
}
}) })
const styles = parseCSS(getStylesCSS()) const styles = parseCSS(getStylesCSS())
@ -147,18 +139,14 @@ describe('CSS generation - numeric values and units', () => {
test('does not add px to unitless properties', () => { test('does not add px to unitless properties', () => {
define('UnitlessTest', { define('UnitlessTest', {
layout: { flex: 1,
flex: 1, flexGrow: 2,
flexGrow: 2, flexShrink: 1,
flexShrink: 1, zIndex: 10,
zIndex: 10, order: 3,
order: 3 opacity: 0.5,
}, fontWeight: 700,
look: { lineHeight: 1.5
opacity: 0.5,
fontWeight: 700,
lineHeight: 1.5
}
}) })
const styles = parseCSS(getStylesCSS()) const styles = parseCSS(getStylesCSS())
@ -174,14 +162,10 @@ describe('CSS generation - numeric values and units', () => {
test('handles numeric zero values correctly', () => { test('handles numeric zero values correctly', () => {
define('ZeroTest', { define('ZeroTest', {
layout: { margin: 0,
margin: 0, padding: 0,
padding: 0, zIndex: 0,
zIndex: 0 opacity: 0
},
look: {
opacity: 0
}
}) })
const styles = parseCSS(getStylesCSS()) const styles = parseCSS(getStylesCSS())
@ -195,12 +179,10 @@ describe('CSS generation - numeric values and units', () => {
describe('CSS generation - layout and look', () => { describe('CSS generation - layout and look', () => {
test('generates CSS for layout properties', () => { test('generates CSS for layout properties', () => {
define('LayoutTest', { define('LayoutTest', {
layout: { display: 'flex',
display: 'flex', flexDirection: 'column',
flexDirection: 'column', gap: 16,
gap: 16, padding: 20
padding: 20
}
}) })
const css = getStylesCSS() const css = getStylesCSS()
@ -213,12 +195,10 @@ describe('CSS generation - layout and look', () => {
test('generates CSS for look properties', () => { test('generates CSS for look properties', () => {
define('LookTest', { define('LookTest', {
look: { color: 'blue',
color: 'blue', backgroundColor: 'white',
backgroundColor: 'white', fontSize: 16,
fontSize: 16, fontWeight: 600
fontWeight: 600
}
}) })
const css = getStylesCSS() const css = getStylesCSS()
@ -231,14 +211,10 @@ describe('CSS generation - layout and look', () => {
test('combines layout and look properties', () => { test('combines layout and look properties', () => {
define('CombinedTest', { define('CombinedTest', {
layout: { display: 'flex',
display: 'flex', padding: 16,
padding: 16 color: 'blue',
}, backgroundColor: 'white'
look: {
color: 'blue',
backgroundColor: 'white'
}
}) })
const styles = parseCSS(getStylesCSS()) const styles = parseCSS(getStylesCSS())
@ -253,11 +229,11 @@ describe('CSS generation - layout and look', () => {
describe('CSS generation - parts', () => { describe('CSS generation - parts', () => {
test('generates separate CSS for each part', () => { test('generates separate CSS for each part', () => {
define('PartTest', { define('PartTest', {
layout: { display: 'flex' }, display: 'flex',
parts: { parts: {
Header: { base: 'header', look: { color: 'red', fontSize: 20 } }, Header: { base: 'header', color: 'red', fontSize: 20 },
Body: { base: 'main', layout: { padding: 20 } }, Body: { base: 'main', padding: 20 },
Footer: { base: 'footer', look: { fontSize: 12 } } Footer: { base: 'footer', fontSize: 12 }
} }
}) })
@ -271,7 +247,7 @@ describe('CSS generation - parts', () => {
test('part className format is ComponentName_PartName', () => { test('part className format is ComponentName_PartName', () => {
define('ComponentWithParts', { define('ComponentWithParts', {
parts: { parts: {
MyPart: { look: { color: 'green' } } MyPart: { color: 'green' }
} }
}) })
@ -345,10 +321,10 @@ describe('components with parts', () => {
describe('variants - boolean variants', () => { describe('variants - boolean variants', () => {
test('applies boolean variant class when true', () => { test('applies boolean variant class when true', () => {
const Component = define('BoolVariant', { const Component = define('BoolVariant', {
layout: { display: 'flex' }, display: 'flex',
variants: { variants: {
primary: { primary: {
look: { color: 'blue' } color: 'blue'
} }
} }
}) })
@ -362,7 +338,7 @@ describe('variants - boolean variants', () => {
const Component = define('BoolVariantFalse', { const Component = define('BoolVariantFalse', {
variants: { variants: {
active: { active: {
look: { backgroundColor: 'green' } backgroundColor: 'green'
} }
} }
}) })
@ -379,10 +355,10 @@ describe('variants - boolean variants', () => {
test('generates CSS for component with boolean variant', () => { test('generates CSS for component with boolean variant', () => {
define('BoolVariantCSS', { define('BoolVariantCSS', {
layout: { display: 'block' }, display: 'block',
variants: { variants: {
active: { active: {
look: { backgroundColor: 'green' } backgroundColor: 'green'
} }
} }
}) })
@ -398,9 +374,9 @@ describe('variants - string/enum variants', () => {
const Component = define('StringVariant', { const Component = define('StringVariant', {
variants: { variants: {
size: { size: {
small: { layout: { padding: 8 } }, small: { padding: 8 },
medium: { layout: { padding: 16 } }, medium: { padding: 16 },
large: { layout: { padding: 24 } } large: { padding: 24 }
} }
} }
}) })
@ -416,9 +392,9 @@ describe('variants - string/enum variants', () => {
define('ColorVariant', { define('ColorVariant', {
variants: { variants: {
color: { color: {
red: { look: { color: 'red', backgroundColor: '#ffeeee' } }, red: { color: 'red', backgroundColor: '#ffeeee' },
blue: { look: { color: 'blue', backgroundColor: '#eeeeff' } }, blue: { color: 'blue', backgroundColor: '#eeeeff' },
green: { look: { color: 'green', backgroundColor: '#eeffee' } } green: { color: 'green', backgroundColor: '#eeffee' }
} }
} }
}) })
@ -433,12 +409,12 @@ describe('variants - string/enum variants', () => {
const Component = define('MultiVariant', { const Component = define('MultiVariant', {
variants: { variants: {
size: { size: {
small: { layout: { padding: 8 } }, small: { padding: 8 },
large: { layout: { padding: 24 } } large: { padding: 24 }
}, },
color: { color: {
red: { look: { color: 'red' } }, red: { color: 'red' },
blue: { look: { color: 'blue' } } blue: { color: 'blue' }
} }
} }
}) })
@ -452,7 +428,7 @@ describe('variants - string/enum variants', () => {
const Component = define('UndefinedVariant', { const Component = define('UndefinedVariant', {
variants: { variants: {
size: { size: {
small: { layout: { padding: 8 } } small: { padding: 8 }
} }
} }
}) })
@ -466,7 +442,7 @@ describe('variants - string/enum variants', () => {
const Component = define('NonVariantProps', { const Component = define('NonVariantProps', {
variants: { variants: {
size: { size: {
small: { layout: { padding: 8 } } small: { padding: 8 }
} }
} }
}) })
@ -488,12 +464,12 @@ describe('variants with parts', () => {
theme: { theme: {
dark: { dark: {
parts: { parts: {
Header: { look: { color: 'white', backgroundColor: 'black' } } Header: { color: 'white', backgroundColor: 'black' }
} }
}, },
light: { light: {
parts: { parts: {
Header: { look: { color: 'black', backgroundColor: 'white' } } Header: { color: 'black', backgroundColor: 'white' }
} }
} }
} }
@ -514,7 +490,7 @@ describe('variants with parts', () => {
}, },
variants: { variants: {
size: { size: {
large: { layout: { padding: 20 } } large: { padding: 20 }
} }
}, },
render: ({ props, parts }) => { render: ({ props, parts }) => {
@ -531,7 +507,7 @@ describe('variants with parts', () => {
describe('custom render function', () => { describe('custom render function', () => {
test('uses custom render when provided', () => { test('uses custom render when provided', () => {
const Component = define('CustomRender', { const Component = define('CustomRender', {
layout: { display: 'flex' }, display: 'flex',
render: ({ props, parts }) => { render: ({ props, parts }) => {
return <div class="custom-wrapper">{props.children}</div> return <div class="custom-wrapper">{props.children}</div>
} }
@ -596,7 +572,7 @@ describe('Styles component', () => {
}) })
test('Styles renders to HTML with CSS', () => { test('Styles renders to HTML with CSS', () => {
define('StylesComp1', { layout: { width: 100 } }) define('StylesComp1', { width: 100 })
const html = renderToString(define.Styles()) const html = renderToString(define.Styles())
expect(html).toContain('<style>') expect(html).toContain('<style>')
@ -606,8 +582,8 @@ describe('Styles component', () => {
}) })
test('Styles includes CSS for all registered components', () => { test('Styles includes CSS for all registered components', () => {
define('StylesComp2', { layout: { width: 100 } }) define('StylesComp2', { width: 100 })
define('StylesComp3', { layout: { height: 200 } }) define('StylesComp3', { height: 200 })
const css = getStylesCSS() const css = getStylesCSS()
expect(css).toContain('.StylesComp2') expect(css).toContain('.StylesComp2')
@ -620,8 +596,8 @@ describe('Styles component', () => {
define('StylesVariant', { define('StylesVariant', {
variants: { variants: {
size: { size: {
small: { layout: { padding: 8 } }, small: { padding: 8 },
large: { layout: { padding: 24 } } large: { padding: 24 }
} }
} }
}) })
@ -636,8 +612,8 @@ describe('Styles component', () => {
test('Styles includes part CSS', () => { test('Styles includes part CSS', () => {
define('StylesPart', { define('StylesPart', {
parts: { parts: {
Header: { look: { color: 'red' } }, Header: { color: 'red' },
Body: { look: { color: 'blue' } } Body: { color: 'blue' }
} }
}) })
@ -649,6 +625,125 @@ describe('Styles component', () => {
}) })
}) })
describe('variants on parts via props', () => {
test('applies boolean variant to part when passed as prop', () => {
const Component = define('PartVariantBool', {
parts: {
Tab: { base: 'button', color: 'gray' }
},
variants: {
active: {
parts: {
Tab: { color: 'blue' }
}
}
},
render: ({ props, parts }) => {
return (
<parts.Root>
<parts.Tab active={true}>Active Tab</parts.Tab>
<parts.Tab active={false}>Inactive Tab</parts.Tab>
</parts.Root>
)
}
})
const html = renderToString(Component({}))
// Should have one tab with active class, one without
expect(html).toContain('PartVariantBool_Tab active')
// Count occurrences - should have 2 tabs total
const tabCount = (html.match(/PartVariantBool_Tab/g) || []).length
expect(tabCount).toBe(2)
})
test('applies string variant to part when passed as prop', () => {
const Component = define('PartVariantString', {
parts: {
Pill: { base: 'button' }
},
variants: {
size: {
small: { parts: { Pill: { padding: 8 } } },
large: { parts: { Pill: { padding: 24 } } }
}
},
render: ({ props, parts }) => {
return (
<parts.Root>
<parts.Pill size="small">Small</parts.Pill>
<parts.Pill size="large">Large</parts.Pill>
</parts.Root>
)
}
})
const html = renderToString(Component({}))
expect(html).toContain('PartVariantString_Pill size-small')
expect(html).toContain('PartVariantString_Pill size-large')
})
test('does not pass variant props through to HTML', () => {
const Component = define('NoVariantLeakage', {
parts: {
Item: { base: 'div' }
},
variants: {
active: {
parts: { Item: { color: 'blue' } }
}
},
render: ({ props, parts }) => {
return (
<parts.Root>
<parts.Item active={true}>Item</parts.Item>
</parts.Root>
)
}
})
const html = renderToString(Component({}))
// Should have the class, but not the attribute
expect(html).toContain('class="NoVariantLeakage_Item active"')
expect(html).not.toContain('active="true"')
expect(html).not.toContain('active="false"')
})
test('combines root and part level variants', () => {
const Component = define('CombinedVariants', {
parts: {
NavItem: { base: 'a' }
},
variants: {
theme: {
dark: {
backgroundColor: 'black',
parts: { NavItem: { color: 'white' } }
}
},
active: {
parts: { NavItem: { fontWeight: 700 } }
}
},
render: ({ props, parts }) => {
return (
<parts.Root>
<parts.NavItem active={true}>Active Link</parts.NavItem>
<parts.NavItem active={false}>Inactive Link</parts.NavItem>
</parts.Root>
)
}
})
const html = renderToString(Component({ theme: 'dark' }))
// Root should have theme variant
expect(html).toContain('CombinedVariants theme-dark')
// Active NavItem should have both theme and active classes
expect(html).toContain('CombinedVariants_NavItem theme-dark active')
// Inactive NavItem should have only theme class
expect(html).toMatch(/CombinedVariants_NavItem theme-dark"[^>]*>Inactive/)
})
})
describe('edge cases', () => { describe('edge cases', () => {
test('handles empty definition', () => { test('handles empty definition', () => {
const Component = define({}) const Component = define({})
@ -673,7 +768,7 @@ describe('edge cases', () => {
const Component = define({ const Component = define({
variants: { variants: {
size: { size: {
small: { layout: { padding: 8 } } small: { padding: 8 }
} }
} }
}) })
@ -683,8 +778,8 @@ describe('edge cases', () => {
}) })
test('does not duplicate styles when registered multiple times', () => { test('does not duplicate styles when registered multiple times', () => {
define('NoDuplicate', { layout: { width: 100 } }) define('NoDuplicate', { width: 100 })
define('NoDuplicate', { layout: { width: 200 } }) define('NoDuplicate', { width: 200 })
const styles = parseCSS(getStylesCSS()) const styles = parseCSS(getStylesCSS())
// Should keep first value (??= operator) // Should keep first value (??= operator)
@ -693,18 +788,18 @@ describe('edge cases', () => {
test('handles complex nested structures', () => { test('handles complex nested structures', () => {
define('ComplexNested', { define('ComplexNested', {
layout: { display: 'grid' }, display: 'grid',
parts: { parts: {
Container: { layout: { padding: 16 } }, Container: { padding: 16 },
Item: { look: { fontSize: 14 } } Item: { fontSize: 14 }
}, },
variants: { variants: {
theme: { theme: {
dark: { dark: {
look: { backgroundColor: 'black' }, backgroundColor: 'black',
parts: { parts: {
Container: { look: { backgroundColor: '#222' } }, Container: { backgroundColor: '#222' },
Item: { look: { color: 'white' } } Item: { color: 'white' }
} }
} }
} }

View File

@ -1,37 +1,39 @@
export type TagDef = { export type TagDef = {
className?: string className?: string
base?: string base?: string
layout?: Layout
look?: Look
states?: Record<string, TagDef>, states?: Record<string, TagDef>,
parts?: Record<string, TagDef> parts?: Record<string, TagDef>
variants?: Record<string, TagDef | Record<string, TagDef>> variants?: Record<string, TagDef | Record<string, TagDef>>
render?: (obj: any) => any render?: (obj: any) => any
}
type Layout = { // layout-related
alignContent?: string alignContent?: 'normal' | 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly' | 'stretch' | 'start' | 'end' | 'baseline' | string
alignItems?: string alignItems?: 'normal' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline' | 'start' | 'end' | 'self-start' | 'self-end' | string
alignSelf?: string alignSelf?: 'auto' | 'normal' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline' | 'start' | 'end' | 'self-start' | 'self-end' | string
aspectRatio?: number | string
bottom?: number | string bottom?: number | string
display?: string boxSizing?: 'content-box' | 'border-box'
columnGap?: number | string
contain?: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint' | string
display?: 'block' | 'inline' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'inline-grid' | 'flow-root' | 'none' | 'contents' | 'table' | 'table-row' | 'table-cell' | string
flex?: number | string flex?: number | string
flexBasis?: number | string flexBasis?: number | string
flexDirection?: string flexDirection?: 'row' | 'row-reverse' | 'column' | 'column-reverse'
flexGrow?: number flexGrow?: number
flexShrink?: number flexShrink?: number
flexWrap?: string flexWrap?: 'nowrap' | 'wrap' | 'wrap-reverse'
gap?: number | string gap?: number | string
gridAutoFlow?: string gridAutoFlow?: 'row' | 'column' | 'dense' | 'row dense' | 'column dense'
gridColumn?: string gridColumn?: string
gridGap?: number | string gridGap?: number | string
gridRow?: string gridRow?: string
gridTemplateColumns?: string gridTemplateColumns?: string
gridTemplateRows?: string gridTemplateRows?: string
height?: number | string height?: number | string
justifyContent?: string inset?: number | string
justifyItems?: string justifyContent?: 'normal' | 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly' | 'start' | 'end' | 'left' | 'right' | 'stretch' | string
justifySelf?: string justifyItems?: 'normal' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline' | 'start' | 'end' | 'self-start' | 'self-end' | 'left' | 'right' | string
justifySelf?: 'auto' | 'normal' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline' | 'start' | 'end' | 'self-start' | 'self-end' | 'left' | 'right' | string
left?: number | string left?: number | string
margin?: number | string margin?: number | string
marginBottom?: number | string marginBottom?: number | string
@ -43,70 +45,125 @@ type Layout = {
minHeight?: number | string minHeight?: number | string
minWidth?: number | string minWidth?: number | string
order?: number order?: number
overflow?: string overflow?: 'visible' | 'hidden' | 'scroll' | 'auto' | 'clip'
overflowX?: string overflowX?: 'visible' | 'hidden' | 'scroll' | 'auto' | 'clip'
overflowY?: string overflowY?: 'visible' | 'hidden' | 'scroll' | 'auto' | 'clip'
padding?: number | string padding?: number | string
paddingBottom?: number | string paddingBottom?: number | string
paddingLeft?: number | string paddingLeft?: number | string
paddingRight?: number | string paddingRight?: number | string
paddingTop?: number | string paddingTop?: number | string
position?: string placeContent?: string
placeItems?: string
placeSelf?: string
position?: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky'
right?: number | string right?: number | string
rowGap?: number | string
top?: number | string top?: number | string
verticalAlign?: 'baseline' | 'top' | 'middle' | 'bottom' | 'text-top' | 'text-bottom' | 'sub' | 'super' | string
width?: number | string width?: number | string
zIndex?: number zIndex?: number
}
type Look = { // visual/theme-related
animation?: string animation?: string
appearance?: 'none' | 'auto' | 'button' | 'textfield' | 'searchfield' | 'textarea' | 'checkbox' | 'radio' | string
backdropFilter?: string backdropFilter?: string
background?: string background?: string
backgroundAttachment?: 'scroll' | 'fixed' | 'local'
backgroundClip?: 'border-box' | 'padding-box' | 'content-box' | 'text'
backgroundColor?: string backgroundColor?: string
backgroundImage?: string
backgroundPosition?: string
backgroundRepeat?: 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat' | 'space' | 'round'
backgroundSize?: 'auto' | 'cover' | 'contain' | string
border?: string border?: string
borderBottom?: string borderBottom?: string
borderBottomColor?: string
borderBottomLeftRadius?: number | string
borderBottomRightRadius?: number | string
borderBottomStyle?: 'none' | 'solid' | 'dashed' | 'dotted' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset' | 'hidden'
borderBottomWidth?: number | string
borderColor?: string borderColor?: string
borderLeft?: string borderLeft?: string
borderLeftColor?: string
borderLeftStyle?: 'none' | 'solid' | 'dashed' | 'dotted' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset' | 'hidden'
borderLeftWidth?: number | string
borderRadius?: number | string borderRadius?: number | string
borderRight?: string borderRight?: string
borderStyle?: string borderRightColor?: string
borderRightStyle?: 'none' | 'solid' | 'dashed' | 'dotted' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset' | 'hidden'
borderRightWidth?: number | string
borderStyle?: 'none' | 'solid' | 'dashed' | 'dotted' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset' | 'hidden'
borderTop?: string borderTop?: string
borderTopColor?: string
borderTopLeftRadius?: number | string
borderTopRightRadius?: number | string
borderTopStyle?: 'none' | 'solid' | 'dashed' | 'dotted' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset' | 'hidden'
borderTopWidth?: number | string
borderWidth?: number | string borderWidth?: number | string
boxShadow?: string boxShadow?: string
clipPath?: string
color?: string color?: string
cursor?: string content?: 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' | string
filter?: string filter?: string
fontFamily?: string fontFamily?: string
fontSize?: number | string fontSize?: number | string
fontStyle?: string fontStyle?: 'normal' | 'italic' | 'oblique'
fontWeight?: number | string fontWeight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 'normal' | 'bold' | 'bolder' | 'lighter' | number
isolation?: 'auto' | 'isolate'
letterSpacing?: number | string letterSpacing?: number | string
lineHeight?: number | string lineHeight?: number | string
mixBlendMode?: string listStyle?: string
objectFit?: string listStyleImage?: string
listStylePosition?: 'inside' | 'outside'
listStyleType?: 'none' | 'disc' | 'circle' | 'square' | 'decimal' | 'decimal-leading-zero' | 'lower-roman' | 'upper-roman' | 'lower-alpha' | 'upper-alpha' | 'lower-greek' | 'lower-latin' | 'upper-latin' | string
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'
opacity?: number opacity?: number
outline?: string outline?: string
outlineColor?: string outlineColor?: string
outlineOffset?: number | string outlineOffset?: number | string
outlineStyle?: string outlineStyle?: 'none' | 'solid' | 'dashed' | 'dotted' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset'
outlineWidth?: number | string outlineWidth?: number | string
pointerEvents?: string pointerEvents?: 'auto' | 'none' | 'visiblePainted' | 'visibleFill' | 'visibleStroke' | 'visible' | 'painted' | 'fill' | 'stroke' | 'all'
textAlign?: string resize?: 'none' | 'both' | 'horizontal' | 'vertical' | 'block' | 'inline'
scrollBehavior?: 'auto' | 'smooth'
textAlign?: 'left' | 'right' | 'center' | 'justify' | 'start' | 'end'
textDecoration?: string textDecoration?: string
textOverflow?: string textDecorationColor?: string
textDecorationLine?: 'none' | 'underline' | 'overline' | 'line-through' | 'blink' | string
textDecorationStyle?: 'solid' | 'double' | 'dotted' | 'dashed' | 'wavy'
textDecorationThickness?: number | string
textIndent?: number | string
textOverflow?: 'clip' | 'ellipsis' | string
textShadow?: string textShadow?: string
textTransform?: string textTransform?: 'none' | 'capitalize' | 'uppercase' | 'lowercase' | 'full-width' | 'full-size-kana'
transform?: string transform?: string
transition?: string transition?: string
userSelect?: string userSelect?: 'auto' | 'none' | 'text' | 'contain' | 'all'
visibility?: string visibility?: 'visible' | 'hidden' | 'collapse'
whiteSpace?: string whiteSpace?: 'normal' | 'nowrap' | 'pre' | 'pre-wrap' | 'pre-line' | 'break-spaces'
wordWrap?: string willChange?: 'auto' | 'scroll-position' | 'contents' | string
overflowWrap?: string wordBreak?: 'normal' | 'break-all' | 'keep-all' | 'break-word'
wordSpacing?: number | string
wordWrap?: 'normal' | 'break-word' | 'anywhere'
overflowWrap?: 'normal' | 'break-word' | 'anywhere'
} }
export const NonStyleKeys = new Set([
'className',
'base',
'states',
'parts',
'variants',
'render',
'styles',
])
export const UnitlessProps = new Set([ export const UnitlessProps = new Set([
'animationIterationCount', 'animationIterationCount',
'aspectRatio',
'columnCount', 'columnCount',
'flex', 'flex',
'flexGrow', 'flexGrow',