UNPKG

@twind/preset-typography

Version:

A twind preset that provides a set of `prose` classes you can use to add beautiful typographic defaults to any vanilla HTML you don't control, like HTML rendered from Markdown, or pulled from a CMS.

633 lines (632 loc) 25.6 kB
import { withAutocomplete, toColorValue } from '@twind/core'; /** * 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 */ function presetTypography({ className ='prose' , defaultColor ='gray' , extend ={} , colors ={} } = {}) { 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: null === colors.dark ? 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 base': { '--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 + '-', /** * 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` */ /** * Default color to use. * * @defaultValue 'gray' */ /** * @defaultValue '700' */ /** * @defaultValue '900' */ /** * @defaultValue '600' */ /** * @defaultValue '900' */ /** * @defaultValue '900' */ /** * @defaultValue '500' */ /** * @defaultValue '300' */ /** * @defaultValue '200' */ /** * @defaultValue '900' */ /** * @defaultValue '200' */ /** * @defaultValue '500' */ /** * @defaultValue '900' */ /** * @defaultValue '200' */ /** * @defaultValue '800' */ /** * @defaultValue '300' */ /** * @defaultValue '200' */ // invert colors (dark mode) /** * @defaultValue '300' */ /** * @defaultValue '#fff' */ /** * @defaultValue '400' */ /** * @defaultValue '#fff' */ /** * @defaultValue '#fff' */ /** * @defaultValue '400' */ /** * @defaultValue '600' */ /** * @defaultValue '700' */ /** * @defaultValue '100' */ /** * @defaultValue '700' */ /** * @defaultValue '400' */ /** * @defaultValue '#fff' */ /** * @defaultValue '300' */ /** * @defaultValue 'rgb(0 0 0 / 50%)' */ /** * @defaultValue '600' */ /** * @defaultValue '700' */ /** * Extend or override CSS selectors with CSS declaration block. * * @defaultValue undefined */ // indirection wrapper to remove autocomplete functions from production bundles withAutocomplete(({ $$ }, context)=>{ let css = getFontSize(context.theme('fontSize', $$)); return css && { '@layer components': css }; }, (_, { theme })=>Object.keys(theme('fontSize'))) ], // for colors: prose-sky [ className + '-', withAutocomplete(({ $$ }, context)=>getColors($$, context), (_, { theme })=>Object.keys(theme('colors')).filter((key)=>key && 'DEFAULT' != key && !/[.-]/.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)' } }) ] ] }; function getColors(colorName, context) { let properties = {}, darkProperties = {}, set = (key, shade, target)=>{ let color = context.theme(`colors.${colorName}.${shade}`, shade); target['--tw-prose-' + key] = toColorValue(color); // support auto dark colors let darkColor = target != darkProperties && context.d('colors', `${colorName}-${shade}`, color); darkColor && (darkProperties['--tw-prose-' + key] = toColorValue(darkColor)); }; for(let key in colors){ let shade = colors[key]; 'dark' != key && shade && set(key, shade, properties); } for(let key1 in colors.dark || {}){ let shade1 = colors.dark[key1]; shade1 && (colors.dark ? // explicit dark colors - need to use `dark:prose-invert` set('invert-' + key1, shade1, properties) : // auto dark colors set(key1, shade1, darkProperties)); } return Object.keys(properties).length ? { '@layer defaults': { '&': properties, [context.v('dark')]: darkProperties } } : void 0; } } function adjustSelectors(className, context, css) { let result = {}; for(let selector in css)result[adjustSelector(className, selector, context, (selector)=>`.${context.e(context.h(className))}${selector}`)] = css[selector]; return result; } function adjustSelector(className, selector, { e , h }, replace) { // 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(_) { return _ ? 'string' == typeof _ ? { fontSize: _ } : { fontSize: _[0], ...'string' == typeof _[1] ? { lineHeight: _[1] } : _[1] } : void 0; } function em(px, base) { return `${(px / base).toFixed(3).replace(/^0|\.?0+$/g, '')}em`; } export { presetTypography as default }; //# sourceMappingURL=preset-typography.dev.js.map