UNPKG

tailwind-vertical-rhythm

Version:
213 lines (182 loc) 5.9 kB
// @ts-check // Based on code by Jess Telford <jess@cete.io> // https://github.com/ceteio/styled-components-rhythm const warned = new Set() /** * @param {string} message */ function warn(message) { if (warned.has(message)) { return } warned.add(message) // @ts-ignore console.warn(`tailwind-vertical-rhythm: ${message}`) } /** * @param {number} value * @param {number} multiple * @param {'nearest'|'up'|'down'} direction */ function roundToMultiple(value, multiple, direction = 'nearest') { const valueRoundedDown = Math.floor(value / multiple) * multiple // purposely avoiding floating point and division const isHalfOrOver = (value - valueRoundedDown) * 2 >= multiple if (direction === 'up' || (direction === 'nearest' && isHalfOrOver)) { // force rounding up return valueRoundedDown + multiple } else { // force rounding down return valueRoundedDown } } /** * @param {number} capHeightFraction * @param {number} lineHeightRem * @param {number} fontSizeRem */ function rhythmShift(capHeightFraction, lineHeightRem, fontSizeRem) { const capHeight = fontSizeRem * capHeightFraction return (lineHeightRem - capHeight) / 2 } /** * @param {number} rhythmHeight * @param {number} capHeightFraction * @param {number} fontSizeRem * @param {number} desiredLineHeight */ function rhythmLineHeight(rhythmHeight, capHeightFraction, fontSizeRem, desiredLineHeight) { const baseFontSizePx = 16 const fontSizePx = fontSizeRem * baseFontSizePx const desiredHeightPx = desiredLineHeight * fontSizePx const capHeightPx = capHeightFraction * fontSizePx const rhythmHeightPx = rhythmHeight * baseFontSizePx // Rounded to the nearest rhythm line let roundedHeightPx = roundToMultiple(desiredHeightPx, rhythmHeightPx) // Disallow line heights below the cap height if (roundedHeightPx < capHeightPx) { roundedHeightPx = roundToMultiple(capHeightPx, rhythmHeightPx, 'up') } // convert back to a value relative to the font size rem return roundedHeightPx / baseFontSizePx } /** * @param {string} value * @param {string} path */ function validateIsUnitless(value, path) { const match = /^\s*((?:\d*\.)?\d+)\s*$/.exec(value) if (!match) { warn(`Ignoring ${path}, value '${value}' specifies an unit.`) return } return parseFloat(match[1]) } /** * @param {string} value * @param {string} path */ function validateIsRem(value, path) { if (Array.isArray(value)) { value = value[0] } const match = /^\s*((?:\d*\.)?\d+)\s*rem$/.exec(value) if (!match) { warn(`Ignoring ${path}, value ${JSON.stringify(value)} is not in rem.`) return } return parseFloat(match[1]) } /** * @param {(key: string) => { [s: string]: string; }} theme * @param {string} key * @param {(value: string, path: string) => number | undefined} validator */ function filterValidValues(theme, key, validator) { const entries = Object.entries(theme(key)) /** @type {[string, number][]} */ const mapped = entries.map( ([k, v]) => [k, validator(v, `theme.${key}.${k}`)] ) return mapped.filter(([_, v]) => typeof v !== 'undefined') } // @ts-ignore module.exports = function ({ addUtilities, config, e, theme, variants }) { const rhythmHeight = config('verticalRhythm.height') /** * @param {number} fontSizeRem * @param {number} desiredLineHeight * @param {number} capHeightFraction */ function setFontWithRhythm(fontSizeRem, desiredLineHeight, capHeightFraction) { const lineHeightRem = rhythmLineHeight( rhythmHeight, capHeightFraction, fontSizeRem, desiredLineHeight ) const shiftRem = rhythmShift(capHeightFraction, lineHeightRem, fontSizeRem) /** * @param {number} x */ function formatRem(x) { const rounded = Math.round(x * 1000) / 1000 const multiplier = rounded < 0 ? -1 : 1 const str = (multiplier * rounded).toString() + 'rem' const trimmed = /^0\./.test(str) ? str.substring(1) : str return multiplier === -1 ? '-' + trimmed : trimmed } return { fontSize: formatRem(fontSizeRem), paddingTop: formatRem(shiftRem), marginBottom: formatRem(-shiftRem), lineHeight: formatRem(lineHeightRem), } } const fontCapHeights = Object.entries(config('verticalRhythm.fontCapHeight')) const defaultLineHeight = config('verticalRhythm.defaultLineHeight') const fontSizes = filterValidValues(theme, 'fontSize', validateIsRem) const lineHeights = filterValidValues(theme, 'lineHeight', validateIsUnitless) if (defaultLineHeight) { /** @type {[string, number]|undefined} */ const defaultLineHeightEntry = lineHeights.find( ([k]) => k === defaultLineHeight ) if (defaultLineHeightEntry) { lineHeights.push(['', defaultLineHeightEntry[1]]) } } /** @type {Record<string, Record<string,any>>} */ const utilities = {} for (const [fontFamily, capHeightFraction] of fontCapHeights) { const fontFamilySelector = fontFamily === 'default' ? '' : e(fontFamily.replace(/\s+/, '-').toLowerCase()) for (const [fontSize, fontSizeRem] of fontSizes) { for (const [lineHeight, lineHeightFraction] of lineHeights) { const parts = [ '.vr', fontFamilySelector, lineHeight, fontSize ] const className = parts.filter(Boolean).join('-') utilities[className] = setFontWithRhythm( fontSizeRem, lineHeightFraction, capHeightFraction ) if (fontFamilySelector) { utilities[className].fontFamily = fontFamily } } } } addUtilities({ '.vr-debug': { background: 'linear-gradient(rgba(255, 0, 0, 0.15), rgba(255, 0, 0, 0.15) 1px, transparent 1px)', backgroundSize: `1px ${rhythmHeight}rem` } }) addUtilities(utilities, variants('fontSize')) }