aliaset
Version:
twind monorepo
310 lines (267 loc) • 8.51 kB
text/typescript
// Based on https://github.com/modulz/stitches
// License MIT
// eslint-disable @typescript-eslint/ban-types
import type { Falsey, MatchResult } from './types'
import { parse } from './parse'
import { Layer } from './internal/precedence'
import { escape, hash } from './utils'
import { define } from './internal/define'
export type StrictMorphVariant<T> = T extends number
? `${T}` | T
: T extends 'true'
? true | T
: T extends 'false'
? false | T
: T
export type MorphVariant<T> = T extends number
? `${T}` | T
: T extends 'true'
? boolean | T
: T extends 'false'
? boolean | T
: T extends `${number}`
? number | T
: T
export type StyleTokenValue = string | Falsey
// No support for thunks yet — these may use props that are not in the generated class name
// and may therefore override each other
export type StyleToken = StyleTokenValue
/**
* Allows to extract the supported properties of a style function.
*
* Here is an example for `react`
* ```js
* import { HTMLAttributes } from "react";
* import { style, PropsOf } from "@twind/core";
* const button = style({ ... })
* type ButtonProps = PropsOf<typeof button>
* export const Button = (props: ButtonProps & HTMLAttributes<HTMLButtonElement>) => {
* return <button className={style(props)} {...rest} />
* }
* ```
*/
export type PropsOf<T> = T extends Style<infer Variants>
? {
[key in keyof Variants]: MorphVariant<keyof Variants[key]>
}
: never
export type DefaultVariants<Variants> = {
[key in keyof Variants]?:
| StrictMorphVariant<keyof Variants[key]>
| (Record<string, StrictMorphVariant<keyof Variants[key]>> & {
/** initial breakpoint */
_?: StrictMorphVariant<keyof Variants[key]>
})
}
export type VariantsProps<Variants> = {
[key in keyof Variants]?:
| MorphVariant<keyof Variants[key]>
// TODO possible breakpoint from theme
| (Record<string, MorphVariant<keyof Variants[key]>> & {
/** initial breakpoint */
_?: MorphVariant<keyof Variants[key]>
})
}
export type When<Variants> = {
[key in keyof Variants]?: StrictMorphVariant<keyof Variants[key]>
}
// eslint-disable-next-line @typescript-eslint/ban-types
export interface StyleConfig<Variants, BaseVariants = {}> {
/** Used as prefix */
label?: string
base?: StyleToken
props?: Variants & {
[variant in keyof BaseVariants]?: {
[key in keyof BaseVariants[variant]]?: StyleToken
}
}
defaults?: DefaultVariants<Variants & BaseVariants>
when?: [match: When<Variants & BaseVariants>, then: StyleToken][]
}
export interface StyleFunction {
<Variants>(config?: StyleConfig<Variants>): Style<Variants>
<Variants, BaseVariants>(
base: Style<BaseVariants>,
config?: StyleConfig<Variants, BaseVariants>,
): Style<Variants & BaseVariants>
}
export type StyleProps<Variants> = VariantsProps<Variants>
export interface Style<Variants> {
/**
* CSS Class associated with the current component.
*
* ```jsx
* const button = style({
* base: css({
* color: "DarkSlateGray"
* })
* })
*
* <div className={button()} />
* ```
* <br />
*/
(props?: StyleProps<Variants>): string
/**
* To be used as resolve within config.rules:
*
* ```js
* {
* rules: [
* // label?prop=value&other=propValue
* // if the style has base eg no prop is required
* ['label(\\?.+)?', style( /* ... *\/ )],
*
* // if the style requires at least one prop
* ['label\\?(.+)', style( /* ... *\/ )],
* ]
* }
* ```
*
* The first group is used to extract the props using {@link !URLSearchParams | URLSearchParams}.
*/
(match: MatchResult): string
readonly defaults: StyleProps<Variants>
/**
* CSS Class associated with the current component.
*
* ```js
* const button = style({
* base: css`
* color: "DarkSlateGray"
* `
* })
*
* <div className={button.className} />
* ```
*/
readonly className: string
/**
* CSS Selector associated with the current component.
*
* ```js
* const button = style({
* base: css({
* color: "DarkSlateGray"
* })
* })
*
* const Card = styled({
* base: css`
* & ${button.selector} {
* boxShadow: "0 0 0 5px"
* }
* `
* })
* ```
*/
readonly selector: string
}
/**
* @group Class Name Generators
*/
export const style = (<Variants, BaseVariants>(
base: Style<BaseVariants> | StyleConfig<Variants>,
config?: StyleConfig<Variants, BaseVariants>,
): Style<Variants & BaseVariants> =>
(typeof base == 'function' ? createStyle(config, base) : createStyle(base)) as Style<
Variants & BaseVariants
> &
string) as StyleFunction
function createStyle<Variants, BaseVariants>(
config: StyleConfig<Variants, BaseVariants> = {},
parent?: Style<BaseVariants>,
): Style<Variants & BaseVariants> {
const { label = 'style', base, props: variants = {}, defaults: localDefaults, when = [] } = config
const defaults = { ...parent?.defaults, ...localDefaults }
const id = hash(JSON.stringify([label, parent?.className, base, variants, defaults, when]))
// Layers:
// component: 0b010
// props: 0b011
// when: 0b100
const className = register('', base || '', Layer.c)
function register(mq: string, token: string, layer: number): string {
return define(
// `<name>#<id>` or `<parent>~<name>#<id>`
((parent ? parent.className.replace(/#.+$/, '~') : '') + label + mq + id).replace(
/[: ,()[\]]/,
'',
),
layer,
token && parse(token),
)
}
return Object.defineProperties(
function style(allProps) {
let isWithinRuleDeclaration
if (Array.isArray(allProps)) {
isWithinRuleDeclaration = true
allProps = Object.fromEntries(new URLSearchParams(allProps[1]).entries()) as VariantsProps<
Variants & BaseVariants
>
}
const props = { ...defaults, ...allProps }
// If this style is used within config.rules we do NOT include the marker classes
let classNames = isWithinRuleDeclaration
? ''
: (parent ? parent(props) + ' ' : '') + className
let token: StyleToken
for (const variantKey in variants) {
const variant = (variants as Record<string, Record<string, StyleToken>>)[variantKey]
const propsValue = (props as Record<string, unknown>)[variantKey]
if (propsValue === Object(propsValue)) {
// inline responsive breakpoints
let mq = ''
token = ''
for (const breakpoint in propsValue as Record<string, string>) {
const breakpointToken = variant[(propsValue as Record<string, string>)[breakpoint]]
if (breakpointToken) {
mq += '@' + breakpoint + '-' + (propsValue as Record<string, string>)[breakpoint]
token +=
(token && ' ') +
(breakpoint == '_' ? breakpointToken : breakpoint + ':(' + breakpointToken + ')')
}
}
if (token) {
classNames +=
' ' + register('--' + variantKey + '-' + mq, token, 0b011 << 27 /* Shifts.layer */)
}
} else if ((token = variant[propsValue as string])) {
classNames +=
' ' +
register(
'--' + variantKey + '-' + (propsValue as string),
token,
0b011 << 27 /* Shifts.layer */,
)
}
}
when.forEach((match, index) => {
let mq = ''
for (const variantKey in match[0]) {
const propsValue = (props as Record<string, unknown>)[variantKey]
// TODO we ignore inline responsive breakpoints for now — what be the result??
if (
propsValue !== Object(propsValue) &&
'' + propsValue == '' + (match[0] as Record<string, string>)[variantKey]
) {
mq += (mq && '_') + variantKey + '-' + (propsValue as string)
} else {
mq = ''
break
}
}
if (mq && (token = match[1])) {
classNames +=
' ' + register('-' + index + '--' + mq, token, 0b100 << 27 /* Shifts.layer */)
}
})
return classNames
} as Style<Variants & BaseVariants>,
Object.getOwnPropertyDescriptors({
className,
defaults,
selector: '.' + escape(className),
}),
)
}