126 lines
4.1 KiB
TypeScript
126 lines
4.1 KiB
TypeScript
import type { JSX } from 'hono/jsx'
|
|
import { type TagDef, UnitlessProps } from './types'
|
|
|
|
const styles: Record<string, Record<string, string>> = {}
|
|
export const Styles = () => <style dangerouslySetInnerHTML={{ __html: stylesToCSS(styles) }} />
|
|
|
|
// turns style object into string CSS definition
|
|
function stylesToCSS(styles: Record<string, Record<string, string>>): string {
|
|
let out: string[] = []
|
|
|
|
for (const [selector, style] of Object.entries(styles)) {
|
|
out.push(`.${selector} {`)
|
|
for (const [name, value] of Object.entries(style)) {
|
|
out.push(` ${name}: ${value};`)
|
|
}
|
|
out.push(`}\n`)
|
|
}
|
|
|
|
return out.join('\n')
|
|
}
|
|
|
|
// creates a CSS class name
|
|
function makeClassName(baseName: string, partName?: string, variantName?: string, variantKey?: string): string {
|
|
const cls = partName ? `${baseName}_${partName}` : baseName
|
|
|
|
if (variantName && variantKey) {
|
|
return cls + `.${variantName}-${variantKey}`
|
|
} else {
|
|
return cls
|
|
}
|
|
}
|
|
|
|
// 'fontSize' => 'font-size'
|
|
function camelToDash(name: string): string {
|
|
let out = ''
|
|
|
|
for (const letter of name.split(''))
|
|
out += letter.toUpperCase() === letter ? `-${letter.toLowerCase()}` : letter
|
|
|
|
return out
|
|
}
|
|
|
|
// turns a TagDef into a JSX style object
|
|
function makeStyle(def: TagDef) {
|
|
const style: Record<string, string> = {}
|
|
|
|
for (const [name, value] of Object.entries(Object.assign({}, def.layout ?? {}, def.look ?? {})))
|
|
style[camelToDash(name)] = `${typeof value === 'number' && !UnitlessProps.has(name) ? `${value}px` : value}`
|
|
|
|
return style
|
|
}
|
|
|
|
// turns a TagDef into a JSX component
|
|
function makeComponent(baseName: string, rootDef: TagDef, rootProps: Record<string, any>, partName?: string) {
|
|
const def = partName ? rootDef.parts?.[partName]! : rootDef
|
|
const base = def.base ?? 'div'
|
|
const Tag = (base) as keyof JSX.IntrinsicElements
|
|
|
|
const classNames = [makeClassName(baseName, partName)]
|
|
|
|
for (const [key, value] of Object.entries(rootProps)) {
|
|
const variantConfig = rootDef.variants?.[key]
|
|
if (!variantConfig) continue
|
|
|
|
const variantName = key
|
|
const variantKey = value
|
|
|
|
let variantDef: TagDef | undefined
|
|
if ('parts' in variantConfig || 'layout' in variantConfig || 'look' in variantConfig) {
|
|
if (value === true) variantDef = variantConfig as TagDef
|
|
} else {
|
|
variantDef = (variantConfig as Record<string, TagDef>)[value as string]
|
|
}
|
|
if (!variantDef) continue
|
|
|
|
classNames.push(`${variantName}-${variantKey}`)
|
|
}
|
|
|
|
return ({ children, ...props }: { children: any, [key: string]: any }) =>
|
|
<Tag class={classNames.join(' ')} {...props}>{children}</Tag>
|
|
}
|
|
|
|
// adds CSS styles for tag definition
|
|
function registerStyles(name: string, def: TagDef) {
|
|
const rootClassName = makeClassName(name)
|
|
styles[rootClassName] ??= makeStyle(def)
|
|
|
|
for (const [partName, partDef] of Object.entries(def.parts ?? {})) {
|
|
const partClassName = makeClassName(name, partName)
|
|
styles[partClassName] ??= makeStyle(partDef)
|
|
}
|
|
|
|
for (const [variantName, variantConfig] of Object.entries(def.variants ?? {})) {
|
|
for (const [variantKey, variantDef] of Object.entries(variantConfig as Record<string, TagDef>)) {
|
|
const className = makeClassName(name, undefined, variantName, variantKey)
|
|
styles[className] ??= makeStyle({ layout: variantDef.layout, look: variantDef.look })
|
|
|
|
for (const [partName, partDef] of Object.entries(variantDef.parts ?? {})) {
|
|
const partClassName = makeClassName(name, partName, variantName, variantKey)
|
|
styles[partClassName] ??= makeStyle(partDef)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let anonComponents = 1
|
|
|
|
// 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
|
|
|
|
registerStyles(name, def)
|
|
|
|
return (props: Record<string, any>) => {
|
|
const parts: Record<string, Function> = {}
|
|
|
|
for (const [part] of Object.entries(def.parts ?? {}))
|
|
parts[part] = makeComponent(name, def, props, part)
|
|
|
|
parts.Root = makeComponent(name, def, props)
|
|
return def.render ? def.render({ props, parts }) : <parts.Root {...props}>{props.children}</parts.Root>
|
|
}
|
|
}
|
|
|
|
define.Styles = Styles |