aliaset
Version:
twind monorepo
788 lines (755 loc) • 22.4 kB
text/typescript
import { DEV } from 'distilt/env'
import {
CSSNested,
Preset,
CustomProperties,
CSSObject,
Context,
BaseTheme,
ColorValue,
RuleResolver,
AutocompleteProvider,
withAutocomplete,
} from '@twind/core'
import { toColorValue } from '@twind/core'
declare module '@twind/core' {
export interface CustomProperties {
'--tw-prose-body'?: string
'--tw-prose-lead'?: string
'--tw-prose-headings'?: string
'--tw-prose-links'?: string
'--tw-prose-bold'?: string
'--tw-prose-counters'?: string
'--tw-prose-bullets'?: string
'--tw-prose-hr'?: string
'--tw-prose-quote-borders'?: string
'--tw-prose-captions'?: string
'--tw-prose-code'?: string
'--tw-prose-pre-code'?: string
'--tw-prose-pre-bg'?: string
'--tw-prose-th-borders'?: string
'--tw-prose-td-borders'?: string
}
}
export type FontSizeValue =
| string
| [size: string, lineHeight: string]
| [size: string, options: { lineHeight?: string; letterSpacing?: string }]
export interface TypographyTheme extends BaseTheme {
fontSize: Record<string, FontSizeValue>
}
export interface TypographyOptions {
/**
* The class name to use the typographic utilities.
* To undo the styles to the elements, use it like
* `not-${className}` which is by default `not-prose`.
*
* Note: `not` utility is only available in class.
*
* @defaultValue `prose`
*/
className?: string
/**
* Default color to use.
*
* @defaultValue 'gray'
*/
defaultColor?: string
colors?: {
/**
* @defaultValue '700'
*/
body?: string
/**
* @defaultValue '900'
*/
headings?: string
/**
* @defaultValue '600'
*/
lead?: string
/**
* @defaultValue '900'
*/
links?: string
/**
* @defaultValue '900'
*/
bold?: string
/**
* @defaultValue '500'
*/
counters?: string
/**
* @defaultValue '300'
*/
bullets?: string
/**
* @defaultValue '200'
*/
hr?: string
/**
* @defaultValue '900'
*/
quotes?: string
/**
* @defaultValue '200'
*/
'quote-borders'?: string
/**
* @defaultValue '500'
*/
captions?: string
/**
* @defaultValue '900'
*/
code?: string
/**
* @defaultValue '200'
*/
'pre-code'?: string
/**
* @defaultValue '800'
*/
'pre-bg'?: string
/**
* @defaultValue '300'
*/
'th-borders'?: string
/**
* @defaultValue '200'
*/
'td-borders'?: string
// invert colors (dark mode)
dark?:
| null
| undefined
| {
/**
* @defaultValue '300'
*/
body?: string
/**
* @defaultValue '#fff'
*/
headings?: string
/**
* @defaultValue '400'
*/
lead?: string
/**
* @defaultValue '#fff'
*/
links?: string
/**
* @defaultValue '#fff'
*/
bold?: string
/**
* @defaultValue '400'
*/
counters?: string
/**
* @defaultValue '600'
*/
bullets?: string
/**
* @defaultValue '700'
*/
hr?: string
/**
* @defaultValue '100'
*/
quotes?: string
/**
* @defaultValue '700'
*/
'quote-borders'?: string
/**
* @defaultValue '400'
*/
captions?: string
/**
* @defaultValue '#fff'
*/
code?: string
/**
* @defaultValue '300'
*/
'pre-code'?: string
/**
* @defaultValue 'rgb(0 0 0 / 50%)'
*/
'pre-bg'?: string
/**
* @defaultValue '600'
*/
'th-borders'?: string
/**
* @defaultValue '700'
*/
'td-borders'?: string
}
}
/**
* Extend or override CSS selectors with CSS declaration block.
*
* @defaultValue undefined
*/
extend?: CSSNested
}
// indirection wrapper to remove autocomplete functions from production bundles
function withAutocomplete$(
resolver: RuleResolver<TypographyTheme>,
autocomplete: AutocompleteProvider<TypographyTheme> | false,
): RuleResolver<TypographyTheme> {
if (DEV) {
return withAutocomplete(resolver, autocomplete)
}
return resolver
}
/**
* Twind Preset for Typography
*
* ```js
* // twind.config.js
* import { defineConfig } from '@twind/core'
* import presetTypography from '@twind/preset-typography'
*
* export default defineConfig({
* presets: [
* presetTypography(),
* ],
* })
* ```
*
* @returns typography preset
*/
export default function presetTypography({
className = 'prose',
defaultColor = 'gray',
extend = {},
colors = {},
}: TypographyOptions = {}): Preset<TypographyTheme> {
colors = {
body: '700',
headings: '900',
lead: '600',
links: '900',
bold: '900',
counters: '500',
bullets: '300',
hr: '200',
quotes: '900',
'quote-borders': '200',
captions: '500',
code: '900',
'pre-code': '200',
'pre-bg': '800',
'th-borders': '300',
'td-borders': '200',
...colors,
// invert colors (dark mode)
dark:
colors.dark === null
? null
: {
body: '300',
headings: '#fff',
lead: '400',
links: '#fff',
bold: '#fff',
counters: '400',
bullets: '600',
hr: '700',
quotes: '100',
'quote-borders': '700',
captions: '400',
code: '#fff',
'pre-code': '300',
'pre-bg': 'rgb(0 0 0 / 50%)',
'th-borders': '600',
'td-borders': '700',
...colors.dark,
},
}
return {
// for element modifiers: prose-img:rounded-xl, prose-headings
// & :is()
// & :is(:where(code):not(:where([class~="not-prose"] *)))
variants: [
['headings', 'h1,h2,h3,h4,h5,h6,th'],
['h1'],
['h2'],
['h3'],
['h4'],
['h5'],
['h6'],
['p'],
['a'],
['blockquote'],
['figure'],
['figcaption'],
['strong'],
['em'],
['code'],
['pre'],
['ol'],
['ul'],
['li'],
['table'],
['thead'],
['tr'],
['th'],
['td'],
['img'],
['video'],
['hr'],
['lead', '.lead'],
].map(([name, selector = name]) => [
`${className}-${name}`,
(_, context) =>
adjustSelector(
className,
selector[0] == '.' ? '.' + context.e(context.h(selector.slice(1))) : selector,
context,
(selector) => `& :is(${selector.trim()})`,
),
]),
rules: [
// marker classes lead and not-prose
[`(lead|not-${className})`, ({ 1: $1 }, { h }) => [{ c: h($1) }]],
[
`${className}-invert`,
{
'@layer defaults': {
'--tw-prose-body': 'var(--tw-prose-invert-body)',
'--tw-prose-headings': 'var(--tw-prose-invert-headings)',
'--tw-prose-lead': 'var(--tw-prose-invert-lead)',
'--tw-prose-links': 'var(--tw-prose-invert-links)',
'--tw-prose-bold': 'var(--tw-prose-invert-bold)',
'--tw-prose-counters': 'var(--tw-prose-invert-counters)',
'--tw-prose-bullets': 'var(--tw-prose-invert-bullets)',
'--tw-prose-hr': 'var(--tw-prose-invert-hr)',
'--tw-prose-quotes': 'var(--tw-prose-invert-quotes)',
'--tw-prose-quote-borders': 'var(--tw-prose-invert-quote-borders)',
'--tw-prose-captions': 'var(--tw-prose-invert-captions)',
'--tw-prose-code': 'var(--tw-prose-invert-code)',
'--tw-prose-pre-code': 'var(--tw-prose-invert-pre-code)',
'--tw-prose-pre-bg': 'var(--tw-prose-invert-pre-bg)',
'--tw-prose-th-borders': 'var(--tw-prose-invert-th-borders)',
'--tw-prose-td-borders': 'var(--tw-prose-invert-td-borders)',
},
},
],
// for type scale: prose-xl
[
className + '-',
withAutocomplete$(({ $$ }, context) => {
const css = getFontSize(context.theme('fontSize', $$))
return css && { '@layer components': css }
}, DEV && ((_, { theme }) => Object.keys(theme('fontSize')))),
],
// for colors: prose-sky
[
className + '-',
withAutocomplete$(
({ $$ }, context) => getColors($$, context),
DEV &&
((_, { theme }) =>
Object.keys(theme('colors')).filter(
(key) => key && key != 'DEFAULT' && !/[.-]/.test(key),
)),
),
],
// prose
[
className,
(_, context) =>
({
// layer defaults
...getColors(defaultColor, context),
'@layer base': [
adjustSelectors(className, context, {
a: {
color: 'var(--tw-prose-links)',
textDecorationLine: 'underline',
fontWeight: '500',
},
strong: {
color: 'var(--tw-prose-bold)',
fontWeight: '600',
},
'a strong,blockquote strong,thead th strong': {
color: 'inherit',
},
ul: {
listStyleType: 'disc',
},
ol: {
listStyleType: 'decimal',
},
'ol[type="A"]': {
listStyleType: 'upper-alpha',
},
'ol[type="a"]': {
listStyleType: 'lower-alpha',
},
'ol[type="A" s]': {
listStyleType: 'upper-alpha',
},
'ol[type="a" s]': {
listStyleType: 'lower-alpha',
},
'ol[type="I"]': {
listStyleType: 'upper-roman',
},
'ol[type="i"]': {
listStyleType: 'lower-roman',
},
'ol[type="I" s]': {
listStyleType: 'upper-roman',
},
'ol[type="i" s]': {
listStyleType: 'lower-roman',
},
'ol[type="1"]': {
listStyleType: 'decimal',
},
'ol,ul': {
marginTop: em(20, 16),
marginBottom: em(20, 16),
paddingLeft: em(26, 16),
},
li: {
marginTop: em(8, 16),
marginBottom: em(8, 16),
},
'ol>li,ul>li': {
paddingLeft: em(6, 16),
},
'>ul>li p': {
marginTop: em(12, 16),
marginBottom: em(12, 16),
},
'>ul>li>*:first-child,>ol>li>*:last-child': {
marginTop: em(20, 16),
},
'>ul>li>*:last-child,>ol>li>*:last-child': {
marginBottom: em(20, 16),
},
'ol>li::marker': {
fontWeight: '400',
color: 'var(--tw-prose-counters)',
},
'ul>li::marker': {
color: 'var(--tw-prose-bullets)',
},
'ul ul,ul ol,ol ul,ol ol': {
marginTop: em(12, 16),
marginBottom: em(12, 16),
},
hr: {
borderColor: 'var(--tw-prose-hr)',
borderTopWidth: '1',
marginTop: em(48, 16),
marginBottom: em(48, 16),
},
blockquote: {
marginTop: em(32, 20),
marginBottom: em(32, 20),
paddingLeft: em(20, 20),
fontWeight: '500',
fontStyle: 'italic',
color: 'var(--tw-prose-quotes)',
borderLeftWidth: '0.25rem',
borderLeftColor: 'var(--tw-prose-quote-borders)',
quotes: '"\\201C""\\201D""\\2018""\\2019"',
},
'blockquote p:first-of-type::before': {
content: 'open-quote',
},
'blockquote p:last-of-type::after': {
content: 'close-quote',
},
p: {
marginTop: em(20, 16),
marginBottom: em(20, 16),
},
h1: {
color: 'var(--tw-prose-headings)',
fontWeight: '800',
fontSize: em(36, 16),
marginTop: '0',
marginBottom: em(32, 36),
lineHeight: 1.15,
},
'h1 strong': {
fontWeight: '900',
color: 'inherit',
},
h2: {
color: 'var(--tw-prose-headings)',
fontWeight: '700',
fontSize: em(24, 16),
marginTop: em(48, 24),
marginBottom: em(24, 24),
lineHeight: '1.35',
},
'h2 strong': {
fontWeight: '800',
color: 'inherit',
},
h3: {
color: 'var(--tw-prose-headings)',
fontWeight: '600',
fontSize: em(20, 16),
marginTop: em(32, 20),
marginBottom: em(12, 20),
lineHeight: '1.6',
},
'h3 strong': {
fontWeight: '700',
color: 'inherit',
},
h4: {
color: 'var(--tw-prose-headings)',
fontWeight: '600',
marginTop: em(24, 16),
marginBottom: em(8, 16),
lineHeight: '1.5',
},
'h4 strong': {
fontWeight: '700',
color: 'inherit',
},
'hr+*,h2+*,h3+*,h4+*': {
marginTop: '0',
},
'img,video,figure': {
marginTop: em(32, 16),
marginBottom: em(32, 16),
},
'figure>*': {
marginTop: '0',
marginBottom: '0',
},
figcaption: {
color: 'var(--tw-prose-captions)',
fontSize: em(14, 16),
lineHeight: '1.4',
marginTop: em(12, 14),
},
code: {
color: 'var(--tw-prose-code)',
fontWeight: '600',
fontSize: em(14, 16),
},
'code::before,code::after': {
content: '"`"',
},
'h2 code': {
fontSize: em(21, 24),
},
'h3 code': {
fontSize: em(18, 20),
},
'a code,h1 code,h2 code,h3 code,h4 code,blockquote code,thead th code': {
color: 'inherit',
},
pre: {
color: 'var(--tw-prose-pre-code)',
backgroundColor: 'var(--tw-prose-pre-bg)',
overflowX: 'auto',
fontWeight: '400',
fontSize: em(14, 16),
lineHeight: '1.7',
marginTop: em(24, 14),
marginBottom: em(24, 14),
borderRadius: '0.375rem',
paddingTop: em(12, 14),
paddingRight: em(16, 14),
paddingBottom: em(12, 14),
paddingLeft: em(16, 14),
},
'pre code': {
backgroundColor: 'transparent',
borderWidth: '0',
borderRadius: '0',
padding: '0',
fontWeight: 'inherit',
color: 'inherit',
fontSize: 'inherit',
fontFamily: 'inherit',
lineHeight: 'inherit',
},
'pre code::before': {
content: 'none',
},
'pre code::after': {
content: 'none',
},
table: {
width: '100%',
tableLayout: 'auto',
textAlign: 'left',
marginTop: em(32, 16),
marginBottom: em(32, 16),
fontSize: em(14, 16),
lineHeight: '1.7',
},
thead: {
borderBottomWidth: '1px',
borderBottomColor: 'var(--tw-prose-th-borders)',
},
'thead th': {
color: 'var(--tw-prose-headings)',
fontWeight: '600',
verticalAlign: 'bottom',
paddingRight: em(8, 14),
paddingBottom: em(8, 14),
paddingLeft: em(8, 14),
},
'thead th:first-child': {
paddingLeft: '0',
},
'thead th:last-child': {
paddingRight: '0',
},
'tbody tr': {
borderBottomWidth: '1px',
borderBottomColor: 'var(--tw-prose-td-borders)',
},
'tbody tr:last-child': {
borderBottomWidth: '0',
},
'tbody td,tfoot td': {
verticalAlign: 'baseline',
paddingTop: em(8, 14),
paddingRight: em(8, 14),
paddingBottom: em(8, 14),
paddingLeft: em(8, 14),
},
'tbody td:first-child,tfoot td:first-child': {
paddingLeft: '0',
},
'tbody td:last-child,tfoot td:last-child': {
paddingRight: '0',
},
[`.${context.e(context.h('lead'))}`]: {
color: 'var(--tw-prose-lead)',
fontSize: em(20, 16),
lineHeight: '1.6',
marginTop: em(24, 20),
marginBottom: em(24, 20),
},
'>:first-child': {
marginTop: '0',
},
'>:last-child': {
marginBottom: '0',
},
}),
adjustSelectors(className, context, extend),
],
'@layer components': {
...getFontSize(context.theme('fontSize', 'base')),
color: 'var(--tw-prose-body)',
maxWidth: 'theme(max-w.prose, 65ch)',
},
} as CSSObject),
],
],
}
function getColors<Theme extends BaseTheme>(
colorName: string,
context: Context<Theme>,
): CSSObject | undefined {
const properties: CustomProperties = {}
const darkProperties: CustomProperties = {}
const set = (key: string, shade: string, target: CustomProperties) => {
const color = context.theme(`colors.${colorName}.${shade}`, shade) as ColorValue
target[('--tw-prose-' + key) as keyof CustomProperties] = toColorValue(color)
// support auto dark colors
const darkColor =
target != darkProperties && context.d('colors', `${colorName}.${shade}`, color)
if (darkColor) {
darkProperties[('--tw-prose-' + key) as keyof CustomProperties] = toColorValue(darkColor)
}
}
for (const key in colors) {
const shade = colors[key as keyof typeof colors]
if (key != 'dark' && shade) {
set(key, shade as string, properties)
}
}
for (const key in colors.dark || {}) {
const shade = (colors.dark as Record<string, string>)[key]
if (shade) {
if (colors.dark) {
// explicit dark colors - need to use `dark:prose-invert`
set('invert-' + key, shade, properties)
} else {
// auto dark colors
set(key, shade, darkProperties)
}
}
}
return Object.keys(properties).length
? ({
'@layer defaults': {
'&': properties,
[context.v('dark') as string]: darkProperties,
} as CSSNested,
} as CSSObject)
: undefined
}
}
function adjustSelectors<Theme extends BaseTheme>(
className: string,
context: Context<Theme>,
css: CSSNested,
): CSSNested {
const result: CSSNested = {}
for (const selector in css) {
result[
adjustSelector(
className,
selector,
context,
(selector) => `.${context.e(context.h(className))}${selector}`,
)
] = css[selector]
}
return result
}
function adjustSelector<Theme extends BaseTheme>(
className: string,
selector: string,
{ e, h }: Context<Theme>,
replace: (selector: string) => string,
): string {
// pseudo elements can't be matched
return selector.replace(
// 1. if there no pseudo use whole selector
// 2. if there are pseudo replace prefix
/^[^>:]+$|(>)?((?:[^:,]+(?::[\w-]+)?)|:[\w-]+)(::[\w-]+)?/g,
(_, prefix = ' ', selector = _, pseudoElement = '') =>
replace(
`${prefix}:where(${selector}):not(:where(.${e(h('not-' + className))} *))${pseudoElement}`,
),
)
}
function getFontSize(_: FontSizeValue | undefined): CSSObject | undefined {
return _
? typeof _ == 'string'
? { fontSize: _ }
: {
fontSize: _[0],
...(typeof _[1] == 'string' ? { lineHeight: _[1] } : _[1]),
}
: undefined
}
function em(px: number, base: number) {
return `${(px / base).toFixed(3).replace(/^0|\.?0+$/g, '')}em`
}