diff --git a/.editorconfig b/.editorconfig index 696f400..2ee043a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,7 +5,7 @@ root = true charset = utf-8 end_of_line = lf indent_size = 4 -indent_style = space +indent_style = tab insert_final_newline = true trim_trailing_whitespace = true @@ -14,3 +14,4 @@ trim_trailing_whitespace = false [*.{json,yml}] indent_size = 2 +indent_style = space diff --git a/.eslintrc.js b/.eslintrc.js index 088134b..77b112f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,24 +1,24 @@ module.exports = { - extends: ['@zazen/eslint-config/typescript'], - env: { - node: true, - }, - rules: { - '@typescript-eslint/indent': 'off', - '@typescript-eslint/space-before-function-paren': 'off', + extends: ['@zazen/eslint-config/typescript'], + env: { + node: true, + }, + rules: { + '@typescript-eslint/indent': 'off', + '@typescript-eslint/space-before-function-paren': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - }, - overrides: [ - { - // Jest config - files: [ - '**/__tests__/**/*.{js,ts,tsx}', - '**/*.@(spec|test).{js,ts,tsx}', - ], - env: { - jest: true, - }, - }, - ], + '@typescript-eslint/explicit-function-return-type': 'off', + }, + overrides: [ + { + // Jest config + files: [ + '**/__tests__/**/*.{js,ts,tsx}', + '**/*.@(spec|test).{js,ts,tsx}', + ], + env: { + jest: true, + }, + }, + ], } diff --git a/README.md b/README.md index aa49b8e..88cc11b 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,11 @@ This plugin requires a `fontMetrics` key added to your Tailwind theme. It should ```ts { - ascent: number - descent: number - lineGap: number - unitsPerEm: number - capHeight: number + ascent: number + descent: number + lineGap: number + unitsPerEm: number + capHeight: number } ``` @@ -33,24 +33,24 @@ These values can be determined by using the [Capsize website](https://seek-oss.g ```js // tailwind.config.js module.exports = { - theme: { - fontFamily: { - sans: ['Inter', 'sans-serif'], - }, - fontMetrics: { - sans: { - capHeight: 2048, - ascent: 2728, - descent: -680, - lineGap: 0, - unitsPerEm: 2816, - }, - }, - fontSize: { ... }, - lineHeight: { ... }, - ... - }, - plugins: [require('tailwindcss-capsize')], + theme: { + fontFamily: { + sans: ['Inter', 'sans-serif'], + }, + fontMetrics: { + sans: { + capHeight: 2048, + ascent: 2728, + descent: -680, + lineGap: 0, + unitsPerEm: 2816, + }, + }, + fontSize: { ... }, + lineHeight: { ... }, + ... + }, + plugins: [require('tailwindcss-capsize')], } ``` @@ -94,12 +94,12 @@ By default the plugin replaces the `fontFamily`, `fontSize`, and `lineHeight` co ```diff .font-sans { -+ --ascent-scale: 0.9688; -+ --descent-scale: 0.2415; -+ --cap-height-scale: 0.7273; -+ --line-gap-scale: 0; -+ --line-height-scale: 1.2102; - font-family: Inter, sans-serif; ++ --ascent-scale: 0.9688; ++ --descent-scale: 0.2415; ++ --cap-height-scale: 0.7273; ++ --line-gap-scale: 0; ++ --line-height-scale: 1.2102; + font-family: Inter, sans-serif; } ``` @@ -119,7 +119,7 @@ Sometimes an interface calls for truncating text to a single line or clamping te

- Text to be truncated to a single line + Text to be truncated to a single line

``` @@ -129,7 +129,7 @@ To solve this, a child element to the element with the `.capsize` class should w

- Text to be truncated to a single line + Text to be truncated to a single line

``` diff --git a/package-lock.json b/package-lock.json index 2b4de3b..6611a69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tailwindcss-capsize", - "version": "3.0.1", + "version": "3.0.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "tailwindcss-capsize", - "version": "3.0.1", + "version": "3.0.4", "license": "ISC", "dependencies": { "@capsizecss/core": "^3.0.0" diff --git a/src/__tests__/plugin.test.ts b/src/__tests__/plugin.test.ts index 0358a56..24ee825 100644 --- a/src/__tests__/plugin.test.ts +++ b/src/__tests__/plugin.test.ts @@ -8,814 +8,814 @@ import tailwindcss from 'tailwindcss' const capsizePlugin = require('../../dist') const THEME_CONFIG = { - screens: { - sm: '640px', - }, - fontFamily: { - sans: ['Inter', 'sans-serif'], - }, - fontMetrics: { - sans: { - capHeight: 2048, - ascent: 2728, - descent: -680, - lineGap: 0, - unitsPerEm: 2816, - }, - }, + screens: { + sm: '640px', + }, + fontFamily: { + sans: ['Inter', 'sans-serif'], + }, + fontMetrics: { + sans: { + capHeight: 2048, + ascent: 2728, + descent: -680, + lineGap: 0, + unitsPerEm: 2816, + }, + }, } expect.extend({ - toMatchCss: (receivedCss: string, expectedCss: string) => { - let strip = (str: string) => str.replace(/[;\s]/g, '') - - if (strip(receivedCss) === strip(expectedCss)) { - return { - message: () => - `expected ${receivedCss} not to match CSS ${expectedCss}`, - pass: true, - } - } else { - let receivedCssFormatted = format(receivedCss, { parser: 'css' }) - let expectedCssFormatted = format(expectedCss, { parser: 'css' }) - let diff = diffStringsUnified( - receivedCssFormatted, - expectedCssFormatted, - ) - - return { - message: () => `expected CSS to match:\n${diff}`, - pass: false, - } - } - }, + toMatchCss: (receivedCss: string, expectedCss: string) => { + let strip = (str: string) => str.replace(/[;\s]/g, '') + + if (strip(receivedCss) === strip(expectedCss)) { + return { + message: () => + `expected ${receivedCss} not to match CSS ${expectedCss}`, + pass: true, + } + } else { + let receivedCssFormatted = format(receivedCss, { parser: 'css' }) + let expectedCssFormatted = format(expectedCss, { parser: 'css' }) + let diff = diffStringsUnified( + receivedCssFormatted, + expectedCssFormatted, + ) + + return { + message: () => `expected CSS to match:\n${diff}`, + pass: false, + } + } + }, }) describe('Plugin', () => { - describe('in "modern" mode', () => { - it('generates utility classes with a default root size', async () => { - return await postcss( - tailwindcss({ - theme: { - ...THEME_CONFIG, - fontSize: { - sm: '14px', - md: '1.5rem', - }, - lineHeight: { - sm: '20px', - md: '2.5rem', - }, - }, - corePlugins: false, - plugins: [capsizePlugin], - }), - ) - .process('@tailwind utilities', { - from: undefined, - }) - .then((result) => { - expect(result.css).toMatchCss(` - .font-sans { - --ascent-scale: 0.9688; - --descent-scale: 0.2415; - --cap-height-scale: 0.7273; - --line-gap-scale: 0; - --line-height-scale: 1.2102; - font-family: Inter, sans-serif; - } - - .text-sm { - --font-size-px: 14; - font-size: 14px; - } - - .text-md { - --font-size-px: 24; - font-size: 1.5rem; - } - - .leading-sm { - --line-height-offset: calc( - (((var(--line-height-scale) * var(--font-size-px)) - 20) / 2) / - var(--font-size-px) - ); - line-height: 20px; - } - - .leading-md { - --line-height-offset: calc( - (((var(--line-height-scale) * var(--font-size-px)) - 40) / 2) / - var(--font-size-px) - ); - line-height: 2.5rem; - } - - .capsize::before { - display: table; - content: ""; - margin-bottom: calc( - ( - ( - var(--ascent-scale) - var(--cap-height-scale) + - var(--line-gap-scale) / 2 - ) - var(--line-height-offset) - ) * -1em - ); - } - - .capsize::after { - display: table; - content: ""; - margin-top: calc( - ( - (var(--descent-scale) + var(--line-gap-scale) / 2) - - var(--line-height-offset) - ) * -1em - ); - } - - @media (min-width: 640px) { - .sm\\:font-sans { - --ascent-scale: 0.9688; - --descent-scale: 0.2415; - --cap-height-scale: 0.7273; - --line-gap-scale: 0; - --line-height-scale: 1.2102; - font-family: Inter, sans-serif; - } - - .sm\\:text-sm { - --font-size-px: 14; - font-size: 14px; - } - - .sm\\:text-md { - --font-size-px: 24; - font-size: 1.5rem; - } - - .sm\\:leading-sm { - --line-height-offset: calc( - (((var(--line-height-scale) * var(--font-size-px)) - 20) / 2) / - var(--font-size-px) - ); - line-height: 20px; - } - - .sm\\:leading-md { - --line-height-offset: calc( - (((var(--line-height-scale) * var(--font-size-px)) - 40) / 2) / - var(--font-size-px) - ); - line-height: 2.5rem; - } - } - `) - }) - }) - - it('generates utility classes with a custom root size', async () => { - return await postcss( - tailwindcss({ - theme: { - ...THEME_CONFIG, - screens: {}, - fontSize: { - sm: '14px', - md: '1.5rem', - }, - lineHeight: { - sm: '20px', - md: '2.5rem', - }, - }, - corePlugins: false, - plugins: [capsizePlugin({ rootSize: 12 })], - }), - ) - .process('@tailwind utilities', { - from: undefined, - }) - .then((result) => { - expect(result.css).toMatchCss(` - .font-sans { - --ascent-scale: 0.9688; - --descent-scale: 0.2415; - --cap-height-scale: 0.7273; - --line-gap-scale: 0; - --line-height-scale: 1.2102; - font-family: Inter, sans-serif; - } - - .text-sm { - --font-size-px: 14; - font-size: 14px; - } - - .text-md { - --font-size-px: 18; - font-size: 1.5rem; - } - - .leading-sm { - --line-height-offset: calc( - (((var(--line-height-scale) * var(--font-size-px)) - 20) / 2) / - var(--font-size-px) - ); - line-height: 20px; - } - - .leading-md { - --line-height-offset: calc( - (((var(--line-height-scale) * var(--font-size-px)) - 30) / 2) / - var(--font-size-px) - ); - line-height: 2.5rem; - } - - .capsize::before { - display: table; - content: ""; - margin-bottom: calc( - ( - ( - var(--ascent-scale) - var(--cap-height-scale) + - var(--line-gap-scale) / 2 - ) - var(--line-height-offset) - ) * -1em - ); - } - - .capsize::after { - display: table; - content: ""; - margin-top: calc( - ( - (var(--descent-scale) + var(--line-gap-scale) / 2) - - var(--line-height-offset) - ) * -1em - ); - } - `) - }) - }) - - it('works with unitless or percentage line-height values', async () => { - return await postcss( - tailwindcss({ - theme: { - ...THEME_CONFIG, - screens: {}, - fontSize: { - sm: '1rem', - }, - lineHeight: { - sm: '100%', - md: '1.5', - }, - }, - corePlugins: false, - plugins: [capsizePlugin], - }), - ) - .process('@tailwind utilities', { - from: undefined, - }) - .then((result) => { - expect(result.css).toMatchCss(` - .font-sans { - --ascent-scale: 0.9688; - --descent-scale: 0.2415; - --cap-height-scale: 0.7273; - --line-gap-scale: 0; - --line-height-scale: 1.2102; - font-family: Inter, sans-serif; - } - - .text-sm { - --font-size-px: 16; - font-size: 1rem; - } - - .leading-sm { - --line-height-offset: calc( - ( - ( - (var(--line-height-scale) * var(--font-size-px)) - - calc(1 * var(--font-size-px)) - ) / 2 - ) / var(--font-size-px) - ); - line-height: 100%; - } - - .leading-md { - --line-height-offset: calc( - ( - ( - (var(--line-height-scale) * var(--font-size-px)) - - calc(1.5 * var(--font-size-px)) - ) / 2 - ) / var(--font-size-px) - ); - line-height: 1.5; - } - - .capsize::before { - display: table; - content: ""; - margin-bottom: calc( - ( - ( - var(--ascent-scale) - var(--cap-height-scale) + - var(--line-gap-scale) / 2 - ) - var(--line-height-offset) - ) * -1em - ); - } - - .capsize::after { - display: table; - content: ""; - margin-top: calc( - ( - (var(--descent-scale) + var(--line-gap-scale) / 2) - - var(--line-height-offset) - ) * -1em - ); - } - `) - }) - }) - - it('works with default line-height values', async () => { - return await postcss( - tailwindcss({ - theme: { - ...THEME_CONFIG, - screens: {}, - fontSize: { - base: ['1rem', '1.5rem'], - }, - lineHeight: {}, - }, - corePlugins: false, - plugins: [capsizePlugin], - }), - ) - .process('@tailwind utilities', { - from: undefined, - }) - .then((result) => { - expect(result.css).toMatchCss(` - .font-sans { - --ascent-scale: 0.9688; - --descent-scale: 0.2415; - --cap-height-scale: 0.7273; - --line-gap-scale: 0; - --line-height-scale: 1.2102; - font-family: Inter, sans-serif; - } - - .text-base { - --font-size-px: 16; - font-size: 1rem; - --line-height-offset: calc( - (((var(--line-height-scale) * var(--font-size-px)) - 24) / 2) / - var(--font-size-px) - ); - line-height: 1.5rem; - } - - .capsize::before { - display: table; - content: ""; - margin-bottom: calc( - ( - ( - var(--ascent-scale) - var(--cap-height-scale) + - var(--line-gap-scale) / 2 - ) - var(--line-height-offset) - ) * -1em - ); - } - - .capsize::after { - display: table; - content: ""; - margin-top: calc( - ( - (var(--descent-scale) + var(--line-gap-scale) / 2) - - var(--line-height-offset) - ) * -1em - ); - } - `) - }) - }) - - it('generates utility classes with a custom activation class', async () => { - return await postcss( - tailwindcss({ - theme: { - ...THEME_CONFIG, - screens: {}, - fontSize: { - sm: '1rem', - }, - lineHeight: { - md: '1.5', - }, - }, - corePlugins: false, - plugins: [capsizePlugin({ className: 'leading-trim' })], - }), - ) - .process('@tailwind utilities', { - from: undefined, - }) - .then((result) => { - expect(result.css).toMatchCss(` - .font-sans { - --ascent-scale: 0.9688; - --descent-scale: 0.2415; - --cap-height-scale: 0.7273; - --line-gap-scale: 0; - --line-height-scale: 1.2102; - font-family: Inter, sans-serif; - } - - .text-sm { - --font-size-px: 16; - font-size: 1rem; - } - - .leading-md { - --line-height-offset: calc( - ( - ( - (var(--line-height-scale) * var(--font-size-px)) - - calc(1.5 * var(--font-size-px)) - ) / 2 - ) / var(--font-size-px) - ); - line-height: 1.5; - } - - .leading-trim::before { - display: table; - content: ""; - margin-bottom: calc( - ( - ( - var(--ascent-scale) - var(--cap-height-scale) + - var(--line-gap-scale) / 2 - ) - var(--line-height-offset) - ) * -1em - ); - } - - .leading-trim::after { - display: table; - content: ""; - margin-top: calc( - ( - (var(--descent-scale) + var(--line-gap-scale) / 2) - - var(--line-height-offset) - ) * -1em - ); - } - `) - }) - }) - }) - - describe('in "classic" mode', () => { - it('generates utility classes with a default root size', async () => { - return await postcss( - tailwindcss({ - theme: { - ...THEME_CONFIG, - fontSize: { - sm: '14px', - md: '1.5rem', - }, - lineHeight: { - sm: '20px', - md: '2.5rem', - }, - }, - corePlugins: false, - plugins: [capsizePlugin({ mode: 'classic' })], - }), - ) - .process('@tailwind utilities', { - from: undefined, - }) - .then((result) => { - expect(result.css).toMatchCss(` - .font-sans.text-sm.leading-sm.capsize::before, - .font-sans .text-sm.leading-sm.capsize::before, - .font-sans .text-sm .leading-sm.capsize::before, - .text-sm .font-sans.leading-sm.capsize::before, - .text-sm .font-sans .leading-sm.capsize::before { - content: ''; - margin-bottom: -0.3506em; - display: table; - } - - .font-sans.text-sm.leading-sm.capsize::after, - .font-sans .text-sm.leading-sm.capsize::after, - .font-sans .text-sm .leading-sm.capsize::after, - .text-sm .font-sans.leading-sm.capsize::after, - .text-sm .font-sans .leading-sm.capsize::after { - content: ''; - margin-top: -0.3506em; - display: table; - } - - .font-sans.text-sm.leading-md.capsize::before, - .font-sans .text-sm.leading-md.capsize::before, - .font-sans .text-sm .leading-md.capsize::before, - .text-sm .font-sans.leading-md.capsize::before, - .text-sm .font-sans .leading-md.capsize::before { - content: ''; - margin-bottom: -1.0649em; - display: table; - } - - .font-sans.text-sm.leading-md.capsize::after, - .font-sans .text-sm.leading-md.capsize::after, - .font-sans .text-sm .leading-md.capsize::after, - .text-sm .font-sans.leading-md.capsize::after, - .text-sm .font-sans .leading-md.capsize::after { - content: ''; - margin-top: -1.0649em; - display: table; - } - - .font-sans.text-md.leading-sm.capsize::before, - .font-sans .text-md.leading-sm.capsize::before, - .font-sans .text-md .leading-sm.capsize::before, - .text-md .font-sans.leading-sm.capsize::before, - .text-md .font-sans .leading-sm.capsize::before { - content: ''; - margin-bottom: -0.053em; - display: table; - } - - .font-sans.text-md.leading-sm.capsize::after, - .font-sans .text-md.leading-sm.capsize::after, - .font-sans .text-md .leading-sm.capsize::after, - .text-md .font-sans.leading-sm.capsize::after, - .text-md .font-sans .leading-sm.capsize::after { - content: ''; - margin-top: -0.053em; - display: table; - } - - .font-sans.text-md.leading-md.capsize::before, - .font-sans .text-md.leading-md.capsize::before, - .font-sans .text-md .leading-md.capsize::before, - .text-md .font-sans.leading-md.capsize::before, - .text-md .font-sans .leading-md.capsize::before { - content: ''; - margin-bottom: -0.4697em; - display: table; - } - - .font-sans.text-md.leading-md.capsize::after, - .font-sans .text-md.leading-md.capsize::after, - .font-sans .text-md .leading-md.capsize::after, - .text-md .font-sans.leading-md.capsize::after, - .text-md .font-sans .leading-md.capsize::after { - content: ''; - margin-top: -0.4697em; - display: table; - } - `) - }) - }) - - it('generates utility classes with a custom root size', async () => { - return await postcss( - tailwindcss({ - theme: { - ...THEME_CONFIG, - fontSize: { - sm: '14px', - md: '1.5rem', - }, - lineHeight: { - sm: '20px', - md: '2.5rem', - }, - }, - corePlugins: false, - plugins: [capsizePlugin({ rootSize: 12, mode: 'classic' })], - }), - ) - .process('@tailwind utilities', { - from: undefined, - }) - .then((result) => { - expect(result.css).toMatchCss(` - .font-sans.text-sm.leading-sm.capsize::before, - .font-sans .text-sm.leading-sm.capsize::before, - .font-sans .text-sm .leading-sm.capsize::before, - .text-sm .font-sans.leading-sm.capsize::before, - .text-sm .font-sans .leading-sm.capsize::before { - content: ''; - margin-bottom: -0.3506em; - display: table; - } - - .font-sans.text-sm.leading-sm.capsize::after, - .font-sans .text-sm.leading-sm.capsize::after, - .font-sans .text-sm .leading-sm.capsize::after, - .text-sm .font-sans.leading-sm.capsize::after, - .text-sm .font-sans .leading-sm.capsize::after { - content: ''; - margin-top: -0.3506em; - display: table; - } - - .font-sans.text-sm.leading-md.capsize::before, - .font-sans .text-sm.leading-md.capsize::before, - .font-sans .text-sm .leading-md.capsize::before, - .text-sm .font-sans.leading-md.capsize::before, - .text-sm .font-sans .leading-md.capsize::before { - content: ''; - margin-bottom: -0.7078em; - display: table; - } - - .font-sans.text-sm.leading-md.capsize::after, - .font-sans .text-sm.leading-md.capsize::after, - .font-sans .text-sm .leading-md.capsize::after, - .text-sm .font-sans.leading-md.capsize::after, - .text-sm .font-sans .leading-md.capsize::after { - content: ''; - margin-top: -0.7078em; - display: table; - } - - .font-sans.text-md.leading-sm.capsize::before, - .font-sans .text-md.leading-sm.capsize::before, - .font-sans .text-md .leading-sm.capsize::before, - .text-md .font-sans.leading-sm.capsize::before, - .text-md .font-sans .leading-sm.capsize::before { - content: ''; - margin-bottom: -0.1919em; - display: table; - } - - .font-sans.text-md.leading-sm.capsize::after, - .font-sans .text-md.leading-sm.capsize::after, - .font-sans .text-md .leading-sm.capsize::after, - .text-md .font-sans.leading-sm.capsize::after, - .text-md .font-sans .leading-sm.capsize::after { - content: ''; - margin-top: -0.1919em; - display: table; - } - - .font-sans.text-md.leading-md.capsize::before, - .font-sans .text-md.leading-md.capsize::before, - .font-sans .text-md .leading-md.capsize::before, - .text-md .font-sans.leading-md.capsize::before, - .text-md .font-sans .leading-md.capsize::before { - content: ''; - margin-bottom: -0.4697em; - display: table; - } - - .font-sans.text-md.leading-md.capsize::after, - .font-sans .text-md.leading-md.capsize::after, - .font-sans .text-md .leading-md.capsize::after, - .text-md .font-sans.leading-md.capsize::after, - .text-md .font-sans .leading-md.capsize::after { - content: ''; - margin-top: -0.4697em; - display: table; - } - `) - }) - }) - - it('works with unitless or percentage line-height values', async () => { - return await postcss( - tailwindcss({ - theme: { - ...THEME_CONFIG, - fontSize: { - sm: '1rem', - }, - lineHeight: { - sm: '100%', - md: '1.5', - }, - }, - corePlugins: false, - plugins: [capsizePlugin({ mode: 'classic' })], - }), - ) - .process('@tailwind utilities', { - from: undefined, - }) - .then((result) => { - expect(result.css).toMatchCss(` - .font-sans.text-sm.leading-sm.capsize::before, - .font-sans .text-sm.leading-sm.capsize::before, - .font-sans .text-sm .leading-sm.capsize::before, - .text-sm .font-sans.leading-sm.capsize::before, - .text-sm .font-sans .leading-sm.capsize::before { - content: ''; - margin-bottom: -0.1364em; - display: table; - } - - .font-sans.text-sm.leading-sm.capsize::after, - .font-sans .text-sm.leading-sm.capsize::after, - .font-sans .text-sm .leading-sm.capsize::after, - .text-sm .font-sans.leading-sm.capsize::after, - .text-sm .font-sans .leading-sm.capsize::after { - content: ''; - margin-top: -0.1364em; - display: table; - } - - .font-sans.text-sm.leading-md.capsize::before, - .font-sans .text-sm.leading-md.capsize::before, - .font-sans .text-sm .leading-md.capsize::before, - .text-sm .font-sans.leading-md.capsize::before, - .text-sm .font-sans .leading-md.capsize::before { - content: ''; - margin-bottom: -0.3864em; - display: table; - } - - .font-sans.text-sm.leading-md.capsize::after, - .font-sans .text-sm.leading-md.capsize::after, - .font-sans .text-sm .leading-md.capsize::after, - .text-sm .font-sans.leading-md.capsize::after, - .text-sm .font-sans .leading-md.capsize::after { - content: ''; - margin-top: -0.3864em; - display: table; - } - `) - }) - }) - - it('generates utility classes with a custom activation class', async () => { - return await postcss( - tailwindcss({ - theme: { - ...THEME_CONFIG, - fontSize: { - sm: '1rem', - }, - lineHeight: { - md: '1.5', - }, - }, - corePlugins: false, - plugins: [ - capsizePlugin({ - className: 'leading-trim', - mode: 'classic', - }), - ], - }), - ) - .process('@tailwind utilities', { - from: undefined, - }) - .then((result) => { - expect(result.css).toMatchCss(` - .font-sans.text-sm.leading-md.leading-trim::before, - .font-sans .text-sm.leading-md.leading-trim::before, - .font-sans .text-sm .leading-md.leading-trim::before, - .text-sm .font-sans.leading-md.leading-trim::before, - .text-sm .font-sans .leading-md.leading-trim::before { - content: ''; - margin-bottom: -0.3864em; - display: table; - } - - .font-sans.text-sm.leading-md.leading-trim::after, - .font-sans .text-sm.leading-md.leading-trim::after, - .font-sans .text-sm .leading-md.leading-trim::after, - .text-sm .font-sans.leading-md.leading-trim::after, - .text-sm .font-sans .leading-md.leading-trim::after { - content: ''; - margin-top: -0.3864em; - display: table; - } - `) - }) - }) - }) + describe('in "modern" mode', () => { + it('generates utility classes with a default root size', async () => { + return await postcss( + tailwindcss({ + theme: { + ...THEME_CONFIG, + fontSize: { + sm: '14px', + md: '1.5rem', + }, + lineHeight: { + sm: '20px', + md: '2.5rem', + }, + }, + corePlugins: false, + plugins: [capsizePlugin], + }), + ) + .process('@tailwind utilities', { + from: undefined, + }) + .then((result) => { + expect(result.css).toMatchCss(` + .font-sans { + --ascent-scale: 0.9688; + --descent-scale: 0.2415; + --cap-height-scale: 0.7273; + --line-gap-scale: 0; + --line-height-scale: 1.2102; + font-family: Inter, sans-serif; + } + + .text-sm { + --font-size-px: 14; + font-size: 14px; + } + + .text-md { + --font-size-px: 24; + font-size: 1.5rem; + } + + .leading-sm { + --line-height-offset: calc( + (((var(--line-height-scale) * var(--font-size-px)) - 20) / 2) / + var(--font-size-px) + ); + line-height: 20px; + } + + .leading-md { + --line-height-offset: calc( + (((var(--line-height-scale) * var(--font-size-px)) - 40) / 2) / + var(--font-size-px) + ); + line-height: 2.5rem; + } + + .capsize::before { + display: table; + content: ""; + margin-bottom: calc( + ( + ( + var(--ascent-scale) - var(--cap-height-scale) + + var(--line-gap-scale) / 2 + ) - var(--line-height-offset) + ) * -1em + ); + } + + .capsize::after { + display: table; + content: ""; + margin-top: calc( + ( + (var(--descent-scale) + var(--line-gap-scale) / 2) - + var(--line-height-offset) + ) * -1em + ); + } + + @media (min-width: 640px) { + .sm\\:font-sans { + --ascent-scale: 0.9688; + --descent-scale: 0.2415; + --cap-height-scale: 0.7273; + --line-gap-scale: 0; + --line-height-scale: 1.2102; + font-family: Inter, sans-serif; + } + + .sm\\:text-sm { + --font-size-px: 14; + font-size: 14px; + } + + .sm\\:text-md { + --font-size-px: 24; + font-size: 1.5rem; + } + + .sm\\:leading-sm { + --line-height-offset: calc( + (((var(--line-height-scale) * var(--font-size-px)) - 20) / 2) / + var(--font-size-px) + ); + line-height: 20px; + } + + .sm\\:leading-md { + --line-height-offset: calc( + (((var(--line-height-scale) * var(--font-size-px)) - 40) / 2) / + var(--font-size-px) + ); + line-height: 2.5rem; + } + } + `) + }) + }) + + it('generates utility classes with a custom root size', async () => { + return await postcss( + tailwindcss({ + theme: { + ...THEME_CONFIG, + screens: {}, + fontSize: { + sm: '14px', + md: '1.5rem', + }, + lineHeight: { + sm: '20px', + md: '2.5rem', + }, + }, + corePlugins: false, + plugins: [capsizePlugin({ rootSize: 12 })], + }), + ) + .process('@tailwind utilities', { + from: undefined, + }) + .then((result) => { + expect(result.css).toMatchCss(` + .font-sans { + --ascent-scale: 0.9688; + --descent-scale: 0.2415; + --cap-height-scale: 0.7273; + --line-gap-scale: 0; + --line-height-scale: 1.2102; + font-family: Inter, sans-serif; + } + + .text-sm { + --font-size-px: 14; + font-size: 14px; + } + + .text-md { + --font-size-px: 18; + font-size: 1.5rem; + } + + .leading-sm { + --line-height-offset: calc( + (((var(--line-height-scale) * var(--font-size-px)) - 20) / 2) / + var(--font-size-px) + ); + line-height: 20px; + } + + .leading-md { + --line-height-offset: calc( + (((var(--line-height-scale) * var(--font-size-px)) - 30) / 2) / + var(--font-size-px) + ); + line-height: 2.5rem; + } + + .capsize::before { + display: table; + content: ""; + margin-bottom: calc( + ( + ( + var(--ascent-scale) - var(--cap-height-scale) + + var(--line-gap-scale) / 2 + ) - var(--line-height-offset) + ) * -1em + ); + } + + .capsize::after { + display: table; + content: ""; + margin-top: calc( + ( + (var(--descent-scale) + var(--line-gap-scale) / 2) - + var(--line-height-offset) + ) * -1em + ); + } + `) + }) + }) + + it('works with unitless or percentage line-height values', async () => { + return await postcss( + tailwindcss({ + theme: { + ...THEME_CONFIG, + screens: {}, + fontSize: { + sm: '1rem', + }, + lineHeight: { + sm: '100%', + md: '1.5', + }, + }, + corePlugins: false, + plugins: [capsizePlugin], + }), + ) + .process('@tailwind utilities', { + from: undefined, + }) + .then((result) => { + expect(result.css).toMatchCss(` + .font-sans { + --ascent-scale: 0.9688; + --descent-scale: 0.2415; + --cap-height-scale: 0.7273; + --line-gap-scale: 0; + --line-height-scale: 1.2102; + font-family: Inter, sans-serif; + } + + .text-sm { + --font-size-px: 16; + font-size: 1rem; + } + + .leading-sm { + --line-height-offset: calc( + ( + ( + (var(--line-height-scale) * var(--font-size-px)) - + calc(1 * var(--font-size-px)) + ) / 2 + ) / var(--font-size-px) + ); + line-height: 100%; + } + + .leading-md { + --line-height-offset: calc( + ( + ( + (var(--line-height-scale) * var(--font-size-px)) - + calc(1.5 * var(--font-size-px)) + ) / 2 + ) / var(--font-size-px) + ); + line-height: 1.5; + } + + .capsize::before { + display: table; + content: ""; + margin-bottom: calc( + ( + ( + var(--ascent-scale) - var(--cap-height-scale) + + var(--line-gap-scale) / 2 + ) - var(--line-height-offset) + ) * -1em + ); + } + + .capsize::after { + display: table; + content: ""; + margin-top: calc( + ( + (var(--descent-scale) + var(--line-gap-scale) / 2) - + var(--line-height-offset) + ) * -1em + ); + } + `) + }) + }) + + it('works with default line-height values', async () => { + return await postcss( + tailwindcss({ + theme: { + ...THEME_CONFIG, + screens: {}, + fontSize: { + base: ['1rem', '1.5rem'], + }, + lineHeight: {}, + }, + corePlugins: false, + plugins: [capsizePlugin], + }), + ) + .process('@tailwind utilities', { + from: undefined, + }) + .then((result) => { + expect(result.css).toMatchCss(` + .font-sans { + --ascent-scale: 0.9688; + --descent-scale: 0.2415; + --cap-height-scale: 0.7273; + --line-gap-scale: 0; + --line-height-scale: 1.2102; + font-family: Inter, sans-serif; + } + + .text-base { + --font-size-px: 16; + font-size: 1rem; + --line-height-offset: calc( + (((var(--line-height-scale) * var(--font-size-px)) - 24) / 2) / + var(--font-size-px) + ); + line-height: 1.5rem; + } + + .capsize::before { + display: table; + content: ""; + margin-bottom: calc( + ( + ( + var(--ascent-scale) - var(--cap-height-scale) + + var(--line-gap-scale) / 2 + ) - var(--line-height-offset) + ) * -1em + ); + } + + .capsize::after { + display: table; + content: ""; + margin-top: calc( + ( + (var(--descent-scale) + var(--line-gap-scale) / 2) - + var(--line-height-offset) + ) * -1em + ); + } + `) + }) + }) + + it('generates utility classes with a custom activation class', async () => { + return await postcss( + tailwindcss({ + theme: { + ...THEME_CONFIG, + screens: {}, + fontSize: { + sm: '1rem', + }, + lineHeight: { + md: '1.5', + }, + }, + corePlugins: false, + plugins: [capsizePlugin({ className: 'leading-trim' })], + }), + ) + .process('@tailwind utilities', { + from: undefined, + }) + .then((result) => { + expect(result.css).toMatchCss(` + .font-sans { + --ascent-scale: 0.9688; + --descent-scale: 0.2415; + --cap-height-scale: 0.7273; + --line-gap-scale: 0; + --line-height-scale: 1.2102; + font-family: Inter, sans-serif; + } + + .text-sm { + --font-size-px: 16; + font-size: 1rem; + } + + .leading-md { + --line-height-offset: calc( + ( + ( + (var(--line-height-scale) * var(--font-size-px)) - + calc(1.5 * var(--font-size-px)) + ) / 2 + ) / var(--font-size-px) + ); + line-height: 1.5; + } + + .leading-trim::before { + display: table; + content: ""; + margin-bottom: calc( + ( + ( + var(--ascent-scale) - var(--cap-height-scale) + + var(--line-gap-scale) / 2 + ) - var(--line-height-offset) + ) * -1em + ); + } + + .leading-trim::after { + display: table; + content: ""; + margin-top: calc( + ( + (var(--descent-scale) + var(--line-gap-scale) / 2) - + var(--line-height-offset) + ) * -1em + ); + } + `) + }) + }) + }) + + describe('in "classic" mode', () => { + it('generates utility classes with a default root size', async () => { + return await postcss( + tailwindcss({ + theme: { + ...THEME_CONFIG, + fontSize: { + sm: '14px', + md: '1.5rem', + }, + lineHeight: { + sm: '20px', + md: '2.5rem', + }, + }, + corePlugins: false, + plugins: [capsizePlugin({ mode: 'classic' })], + }), + ) + .process('@tailwind utilities', { + from: undefined, + }) + .then((result) => { + expect(result.css).toMatchCss(` + .font-sans.text-sm.leading-sm.capsize::before, + .font-sans .text-sm.leading-sm.capsize::before, + .font-sans .text-sm .leading-sm.capsize::before, + .text-sm .font-sans.leading-sm.capsize::before, + .text-sm .font-sans .leading-sm.capsize::before { + content: ''; + margin-bottom: -0.3506em; + display: table; + } + + .font-sans.text-sm.leading-sm.capsize::after, + .font-sans .text-sm.leading-sm.capsize::after, + .font-sans .text-sm .leading-sm.capsize::after, + .text-sm .font-sans.leading-sm.capsize::after, + .text-sm .font-sans .leading-sm.capsize::after { + content: ''; + margin-top: -0.3506em; + display: table; + } + + .font-sans.text-sm.leading-md.capsize::before, + .font-sans .text-sm.leading-md.capsize::before, + .font-sans .text-sm .leading-md.capsize::before, + .text-sm .font-sans.leading-md.capsize::before, + .text-sm .font-sans .leading-md.capsize::before { + content: ''; + margin-bottom: -1.0649em; + display: table; + } + + .font-sans.text-sm.leading-md.capsize::after, + .font-sans .text-sm.leading-md.capsize::after, + .font-sans .text-sm .leading-md.capsize::after, + .text-sm .font-sans.leading-md.capsize::after, + .text-sm .font-sans .leading-md.capsize::after { + content: ''; + margin-top: -1.0649em; + display: table; + } + + .font-sans.text-md.leading-sm.capsize::before, + .font-sans .text-md.leading-sm.capsize::before, + .font-sans .text-md .leading-sm.capsize::before, + .text-md .font-sans.leading-sm.capsize::before, + .text-md .font-sans .leading-sm.capsize::before { + content: ''; + margin-bottom: -0.053em; + display: table; + } + + .font-sans.text-md.leading-sm.capsize::after, + .font-sans .text-md.leading-sm.capsize::after, + .font-sans .text-md .leading-sm.capsize::after, + .text-md .font-sans.leading-sm.capsize::after, + .text-md .font-sans .leading-sm.capsize::after { + content: ''; + margin-top: -0.053em; + display: table; + } + + .font-sans.text-md.leading-md.capsize::before, + .font-sans .text-md.leading-md.capsize::before, + .font-sans .text-md .leading-md.capsize::before, + .text-md .font-sans.leading-md.capsize::before, + .text-md .font-sans .leading-md.capsize::before { + content: ''; + margin-bottom: -0.4697em; + display: table; + } + + .font-sans.text-md.leading-md.capsize::after, + .font-sans .text-md.leading-md.capsize::after, + .font-sans .text-md .leading-md.capsize::after, + .text-md .font-sans.leading-md.capsize::after, + .text-md .font-sans .leading-md.capsize::after { + content: ''; + margin-top: -0.4697em; + display: table; + } + `) + }) + }) + + it('generates utility classes with a custom root size', async () => { + return await postcss( + tailwindcss({ + theme: { + ...THEME_CONFIG, + fontSize: { + sm: '14px', + md: '1.5rem', + }, + lineHeight: { + sm: '20px', + md: '2.5rem', + }, + }, + corePlugins: false, + plugins: [capsizePlugin({ rootSize: 12, mode: 'classic' })], + }), + ) + .process('@tailwind utilities', { + from: undefined, + }) + .then((result) => { + expect(result.css).toMatchCss(` + .font-sans.text-sm.leading-sm.capsize::before, + .font-sans .text-sm.leading-sm.capsize::before, + .font-sans .text-sm .leading-sm.capsize::before, + .text-sm .font-sans.leading-sm.capsize::before, + .text-sm .font-sans .leading-sm.capsize::before { + content: ''; + margin-bottom: -0.3506em; + display: table; + } + + .font-sans.text-sm.leading-sm.capsize::after, + .font-sans .text-sm.leading-sm.capsize::after, + .font-sans .text-sm .leading-sm.capsize::after, + .text-sm .font-sans.leading-sm.capsize::after, + .text-sm .font-sans .leading-sm.capsize::after { + content: ''; + margin-top: -0.3506em; + display: table; + } + + .font-sans.text-sm.leading-md.capsize::before, + .font-sans .text-sm.leading-md.capsize::before, + .font-sans .text-sm .leading-md.capsize::before, + .text-sm .font-sans.leading-md.capsize::before, + .text-sm .font-sans .leading-md.capsize::before { + content: ''; + margin-bottom: -0.7078em; + display: table; + } + + .font-sans.text-sm.leading-md.capsize::after, + .font-sans .text-sm.leading-md.capsize::after, + .font-sans .text-sm .leading-md.capsize::after, + .text-sm .font-sans.leading-md.capsize::after, + .text-sm .font-sans .leading-md.capsize::after { + content: ''; + margin-top: -0.7078em; + display: table; + } + + .font-sans.text-md.leading-sm.capsize::before, + .font-sans .text-md.leading-sm.capsize::before, + .font-sans .text-md .leading-sm.capsize::before, + .text-md .font-sans.leading-sm.capsize::before, + .text-md .font-sans .leading-sm.capsize::before { + content: ''; + margin-bottom: -0.1919em; + display: table; + } + + .font-sans.text-md.leading-sm.capsize::after, + .font-sans .text-md.leading-sm.capsize::after, + .font-sans .text-md .leading-sm.capsize::after, + .text-md .font-sans.leading-sm.capsize::after, + .text-md .font-sans .leading-sm.capsize::after { + content: ''; + margin-top: -0.1919em; + display: table; + } + + .font-sans.text-md.leading-md.capsize::before, + .font-sans .text-md.leading-md.capsize::before, + .font-sans .text-md .leading-md.capsize::before, + .text-md .font-sans.leading-md.capsize::before, + .text-md .font-sans .leading-md.capsize::before { + content: ''; + margin-bottom: -0.4697em; + display: table; + } + + .font-sans.text-md.leading-md.capsize::after, + .font-sans .text-md.leading-md.capsize::after, + .font-sans .text-md .leading-md.capsize::after, + .text-md .font-sans.leading-md.capsize::after, + .text-md .font-sans .leading-md.capsize::after { + content: ''; + margin-top: -0.4697em; + display: table; + } + `) + }) + }) + + it('works with unitless or percentage line-height values', async () => { + return await postcss( + tailwindcss({ + theme: { + ...THEME_CONFIG, + fontSize: { + sm: '1rem', + }, + lineHeight: { + sm: '100%', + md: '1.5', + }, + }, + corePlugins: false, + plugins: [capsizePlugin({ mode: 'classic' })], + }), + ) + .process('@tailwind utilities', { + from: undefined, + }) + .then((result) => { + expect(result.css).toMatchCss(` + .font-sans.text-sm.leading-sm.capsize::before, + .font-sans .text-sm.leading-sm.capsize::before, + .font-sans .text-sm .leading-sm.capsize::before, + .text-sm .font-sans.leading-sm.capsize::before, + .text-sm .font-sans .leading-sm.capsize::before { + content: ''; + margin-bottom: -0.1364em; + display: table; + } + + .font-sans.text-sm.leading-sm.capsize::after, + .font-sans .text-sm.leading-sm.capsize::after, + .font-sans .text-sm .leading-sm.capsize::after, + .text-sm .font-sans.leading-sm.capsize::after, + .text-sm .font-sans .leading-sm.capsize::after { + content: ''; + margin-top: -0.1364em; + display: table; + } + + .font-sans.text-sm.leading-md.capsize::before, + .font-sans .text-sm.leading-md.capsize::before, + .font-sans .text-sm .leading-md.capsize::before, + .text-sm .font-sans.leading-md.capsize::before, + .text-sm .font-sans .leading-md.capsize::before { + content: ''; + margin-bottom: -0.3864em; + display: table; + } + + .font-sans.text-sm.leading-md.capsize::after, + .font-sans .text-sm.leading-md.capsize::after, + .font-sans .text-sm .leading-md.capsize::after, + .text-sm .font-sans.leading-md.capsize::after, + .text-sm .font-sans .leading-md.capsize::after { + content: ''; + margin-top: -0.3864em; + display: table; + } + `) + }) + }) + + it('generates utility classes with a custom activation class', async () => { + return await postcss( + tailwindcss({ + theme: { + ...THEME_CONFIG, + fontSize: { + sm: '1rem', + }, + lineHeight: { + md: '1.5', + }, + }, + corePlugins: false, + plugins: [ + capsizePlugin({ + className: 'leading-trim', + mode: 'classic', + }), + ], + }), + ) + .process('@tailwind utilities', { + from: undefined, + }) + .then((result) => { + expect(result.css).toMatchCss(` + .font-sans.text-sm.leading-md.leading-trim::before, + .font-sans .text-sm.leading-md.leading-trim::before, + .font-sans .text-sm .leading-md.leading-trim::before, + .text-sm .font-sans.leading-md.leading-trim::before, + .text-sm .font-sans .leading-md.leading-trim::before { + content: ''; + margin-bottom: -0.3864em; + display: table; + } + + .font-sans.text-sm.leading-md.leading-trim::after, + .font-sans .text-sm.leading-md.leading-trim::after, + .font-sans .text-sm .leading-md.leading-trim::after, + .text-sm .font-sans.leading-md.leading-trim::after, + .text-sm .font-sans .leading-md.leading-trim::after { + content: ''; + margin-top: -0.3864em; + display: table; + } + `) + }) + }) + }) }) diff --git a/src/plugin.ts b/src/plugin.ts index f4d4c46..9f5f0c5 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -4,267 +4,267 @@ import type { NestedObject } from '@navith/tailwindcss-plugin-author-types' import plugin from 'tailwindcss/plugin' import { - isPlainObject, - lineHeightProperties, - makeCssSelectors, - normalizeThemeValue, - normalizeValue, - round, + isPlainObject, + lineHeightProperties, + makeCssSelectors, + normalizeThemeValue, + normalizeValue, + round, } from './utils' export interface PluginOptions { - /** The root font-size, in pixels */ - rootSize?: number - /** Custom utility classname */ - className?: string - /** CSS Output strategy */ - mode?: 'modern' | 'classic' + /** The root font-size, in pixels */ + rootSize?: number + /** Custom utility classname */ + className?: string + /** CSS Output strategy */ + mode?: 'modern' | 'classic' } interface FontSizeOptions { - lineHeight: string - letterSpacing?: string + lineHeight: string + letterSpacing?: string } export default plugin.withOptions>( - ({ rootSize = 16, className = 'capsize', mode = 'modern' } = {}) => { - if (mode === 'classic') { - return function ({ addUtilities, theme }) { - let fontMetrics = theme('fontMetrics', {}) as Record< - string, - FontMetrics - > - let lineHeight = theme('lineHeight', {}) as Record< - string, - string - > - let fontSize = theme('fontSize', {}) as Record - let utilities: NestedObject = {} + ({ rootSize = 16, className = 'capsize', mode = 'modern' } = {}) => { + if (mode === 'classic') { + return function ({ addUtilities, theme }) { + let fontMetrics = theme('fontMetrics', {}) as Record< + string, + FontMetrics + > + let lineHeight = theme('lineHeight', {}) as Record< + string, + string + > + let fontSize = theme('fontSize', {}) as Record + let utilities: NestedObject = {} - Object.keys(fontMetrics).forEach((fontFamily) => { - let fontConfig = fontMetrics[fontFamily] + Object.keys(fontMetrics).forEach((fontFamily) => { + let fontConfig = fontMetrics[fontFamily] - Object.keys(fontSize).forEach((sizeName) => { - Object.keys(lineHeight).forEach((leading) => { - let fs = normalizeValue( - fontSize[sizeName], - rootSize, - ) - let lh = normalizeValue( - lineHeight[leading], - rootSize, - fs, - ) + Object.keys(fontSize).forEach((sizeName) => { + Object.keys(lineHeight).forEach((leading) => { + let fs = normalizeValue( + fontSize[sizeName], + rootSize, + ) + let lh = normalizeValue( + lineHeight[leading], + rootSize, + fs, + ) - let { - '::after': after, - '::before': before, - } = createStyleObject({ - fontMetrics: fontConfig, - fontSize: fs, - leading: lh, - }) + let { + '::after': after, + '::before': before, + } = createStyleObject({ + fontMetrics: fontConfig, + fontSize: fs, + leading: lh, + }) - utilities[ - makeCssSelectors( - fontFamily, - sizeName, - leading, - className, - ) - ] = { - '&::before': before, - '&::after': after, - } - }) - }) - }) + utilities[ + makeCssSelectors( + fontFamily, + sizeName, + leading, + className, + ) + ] = { + '&::before': before, + '&::after': after, + } + }) + }) + }) - addUtilities(utilities, {}) - } - } else { - return function ({ - addUtilities, - // @ts-expect-error -- `matchUtilities` exists. - matchUtilities, - e, - theme, - variants, - }) { - let fontMetrics = theme('fontMetrics', {}) as Record< - string, - FontMetrics - > - let fontFamily = - (theme('fontFamily', {}) as { [k: string]: unknown }) ?? {} + addUtilities(utilities, {}) + } + } else { + return function ({ + addUtilities, + // @ts-expect-error -- `matchUtilities` exists. + matchUtilities, + e, + theme, + variants, + }) { + let fontMetrics = theme('fontMetrics', {}) as Record< + string, + FontMetrics + > + let fontFamily = + (theme('fontFamily', {}) as { [k: string]: unknown }) ?? {} - // font-family - matchUtilities( - { - font: (value: string | string[]) => { - function fallback(val: string | string[]) { - return { - 'font-family': val, - } - } + // font-family + matchUtilities( + { + font: (value: string | string[]) => { + function fallback(val: string | string[]) { + return { + 'font-family': val, + } + } - let family = normalizeThemeValue( - 'fontFamily', - value, - ) + let family = normalizeThemeValue( + 'fontFamily', + value, + ) - let familyKey = Object.keys(fontFamily).find( - (key) => fontFamily[key] === value, - ) + let familyKey = Object.keys(fontFamily).find( + (key) => fontFamily[key] === value, + ) - if (familyKey === undefined) return fallback(family) + if (familyKey === undefined) return fallback(family) - let metrics = fontMetrics[familyKey] + let metrics = fontMetrics[familyKey] - if (metrics === undefined) return fallback(family) + if (metrics === undefined) return fallback(family) - let { - ascent, - descent, - lineGap, - unitsPerEm, - capHeight, - } = metrics - let ascentScale = ascent / unitsPerEm - let descentScale = Math.abs(descent) / unitsPerEm - let capHeightScale = capHeight / unitsPerEm - let lineGapScale = lineGap / unitsPerEm - let lineHeightScale = - (ascent + lineGap + Math.abs(descent)) / - unitsPerEm + let { + ascent, + descent, + lineGap, + unitsPerEm, + capHeight, + } = metrics + let ascentScale = ascent / unitsPerEm + let descentScale = Math.abs(descent) / unitsPerEm + let capHeightScale = capHeight / unitsPerEm + let lineGapScale = lineGap / unitsPerEm + let lineHeightScale = + (ascent + lineGap + Math.abs(descent)) / + unitsPerEm - return { - '--ascent-scale': round(ascentScale), - '--descent-scale': round(descentScale), - '--cap-height-scale': round(capHeightScale), - '--line-gap-scale': round(lineGapScale), - '--line-height-scale': round(lineHeightScale), - 'font-family': family, - } - }, - }, - { - // @ts-expect-error -- `defaultValue` should be optional. - values: theme('fontFamily'), - type: ['lookup', 'generic-name', 'family-name'], - // @ts-expect-error -- `defaultValue` should be optional. - variants: variants('fontFamily'), - }, - ) + return { + '--ascent-scale': round(ascentScale), + '--descent-scale': round(descentScale), + '--cap-height-scale': round(capHeightScale), + '--line-gap-scale': round(lineGapScale), + '--line-height-scale': round(lineHeightScale), + 'font-family': family, + } + }, + }, + { + // @ts-expect-error -- `defaultValue` should be optional. + values: theme('fontFamily'), + type: ['lookup', 'generic-name', 'family-name'], + // @ts-expect-error -- `defaultValue` should be optional. + variants: variants('fontFamily'), + }, + ) - // font-size - matchUtilities( - { - text: ( - value: string | [string, string | FontSizeOptions], - ) => { - let [fontSize, options] = Array.isArray(value) - ? value - : [value] - let fontSizeActual = normalizeValue( - fontSize, - rootSize, - ) - let { - lineHeight, - letterSpacing, - /** - * @todo TS error here is probably due to the - * outdated version of Tailwind used. - */ - // @ts-expect-error -- See above. - fontWeight, - } = (isPlainObject(options) - ? options - : { - lineHeight: options, - letterSpacing: undefined, - }) as FontSizeOptions + // font-size + matchUtilities( + { + text: ( + value: string | [string, string | FontSizeOptions], + ) => { + let [fontSize, options] = Array.isArray(value) + ? value + : [value] + let fontSizeActual = normalizeValue( + fontSize, + rootSize, + ) + let { + lineHeight, + letterSpacing, + /** + * @todo TS error here is probably due to the + * outdated version of Tailwind used. + */ + // @ts-expect-error -- See above. + fontWeight, + } = (isPlainObject(options) + ? options + : { + lineHeight: options, + letterSpacing: undefined, + }) as FontSizeOptions - return { - '--font-size-px': String(fontSizeActual), - 'font-size': fontSize, - ...lineHeightProperties(lineHeight, rootSize), - ...(letterSpacing === undefined - ? {} - : { 'letter-spacing': letterSpacing }), - ...(fontWeight === undefined - ? {} - : { 'font-weight': fontWeight }), - } - }, - }, - { - // @ts-expect-error -- `defaultValue` should be optional. - values: theme('fontSize'), - type: [ - 'absolute-size', - 'relative-size', - 'length', - 'percentage', - ], - // @ts-expect-error -- `defaultValue` should be optional. - variants: variants('fontSize'), - }, - ) + return { + '--font-size-px': String(fontSizeActual), + 'font-size': fontSize, + ...lineHeightProperties(lineHeight, rootSize), + ...(letterSpacing === undefined + ? {} + : { 'letter-spacing': letterSpacing }), + ...(fontWeight === undefined + ? {} + : { 'font-weight': fontWeight }), + } + }, + }, + { + // @ts-expect-error -- `defaultValue` should be optional. + values: theme('fontSize'), + type: [ + 'absolute-size', + 'relative-size', + 'length', + 'percentage', + ], + // @ts-expect-error -- `defaultValue` should be optional. + variants: variants('fontSize'), + }, + ) - // line-height - matchUtilities( - { - leading: (value: string | string[]) => { - let lineHeight = normalizeThemeValue( - 'lineHeight', - value, - ) as string + // line-height + matchUtilities( + { + leading: (value: string | string[]) => { + let lineHeight = normalizeThemeValue( + 'lineHeight', + value, + ) as string - return lineHeightProperties(lineHeight, rootSize) - }, - }, - { - // @ts-expect-error -- `defaultValue` should be optional. - values: theme('lineHeight'), - // @ts-expect-error -- `defaultValue` should be optional. - variants: variants('lineHeight'), - }, - ) + return lineHeightProperties(lineHeight, rootSize) + }, + }, + { + // @ts-expect-error -- `defaultValue` should be optional. + values: theme('lineHeight'), + // @ts-expect-error -- `defaultValue` should be optional. + variants: variants('lineHeight'), + }, + ) - // leading-trim - addUtilities( - { - [`.${e(className)}`]: { - '&::before': { - display: 'table', - content: '""', - 'margin-bottom': - 'calc(((var(--ascent-scale) - var(--cap-height-scale) + var(--line-gap-scale) / 2) - var(--line-height-offset)) * -1em)', - }, - '&::after': { - display: 'table', - content: '""', - 'margin-top': - 'calc(((var(--descent-scale) + var(--line-gap-scale) / 2) - var(--line-height-offset)) * -1em)', - }, - }, - }, - {}, - ) - } - } - }, - ({ mode = 'modern' } = {}) => { - if (mode === 'classic') return {} + // leading-trim + addUtilities( + { + [`.${e(className)}`]: { + '&::before': { + display: 'table', + content: '""', + 'margin-bottom': + 'calc(((var(--ascent-scale) - var(--cap-height-scale) + var(--line-gap-scale) / 2) - var(--line-height-offset)) * -1em)', + }, + '&::after': { + display: 'table', + content: '""', + 'margin-top': + 'calc(((var(--descent-scale) + var(--line-gap-scale) / 2) - var(--line-height-offset)) * -1em)', + }, + }, + }, + {}, + ) + } + } + }, + ({ mode = 'modern' } = {}) => { + if (mode === 'classic') return {} - return { - corePlugins: { - fontFamily: false, - fontSize: false, - lineHeight: false, - }, - } - }, + return { + corePlugins: { + fontFamily: false, + fontSize: false, + lineHeight: false, + }, + } + }, ) diff --git a/src/utils.ts b/src/utils.ts index 1169d3f..a043164 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,69 +1,69 @@ type FontSizeValue = [string, Record<'lineHeight', string>] export function isRelativeValue(value: string) { - let isPercentValue = value.endsWith('%') - let isUnitlessValue = /[0-9]$/.test(value) + let isPercentValue = value.endsWith('%') + let isUnitlessValue = /[0-9]$/.test(value) - return isPercentValue || isUnitlessValue + return isPercentValue || isUnitlessValue } export function getRelativeValue(value: string) { - let isPercentValue = value.endsWith('%') + let isPercentValue = value.endsWith('%') - return isPercentValue - ? parseInt(value.replace('%', '')) / 100 - : parseFloat(value) + return isPercentValue + ? parseInt(value.replace('%', '')) / 100 + : parseFloat(value) } export function normalizeValue( - value: string | FontSizeValue, - root: number, - fs?: number, + value: string | FontSizeValue, + root: number, + fs?: number, ): number { - value = Array.isArray(value) ? value[0] : value + value = Array.isArray(value) ? value[0] : value - if (value.endsWith('px')) return parseFloat(value.replace('px', '')) - if (value.endsWith('rem')) - return root * parseFloat(value.replace('rem', '')) + if (value.endsWith('px')) return parseFloat(value.replace('px', '')) + if (value.endsWith('rem')) + return root * parseFloat(value.replace('rem', '')) - if (isRelativeValue(value) && fs !== undefined) { - return fs * getRelativeValue(value) - } + if (isRelativeValue(value) && fs !== undefined) { + return fs * getRelativeValue(value) + } - return parseInt(value) + return parseInt(value) } const cssRegex = /^([+-]?(?:\d+|\d*\.\d+))([a-z]*|%)$/ export function getValueAndUnit(value: string): [number, string | undefined] { - if (typeof value !== 'string') return [value, ''] - let matchedValue = value.match(cssRegex) - if (matchedValue != null) return [parseFloat(value), matchedValue[2]] - return [parseInt(value), undefined] + if (typeof value !== 'string') return [value, ''] + let matchedValue = value.match(cssRegex) + if (matchedValue != null) return [parseFloat(value), matchedValue[2]] + return [parseInt(value), undefined] } export function makeCssSelectors( - fontFamily: string, - sizeName: string, - leading: string, - className: string, + fontFamily: string, + sizeName: string, + leading: string, + className: string, ): string { - return ( - `.font-${fontFamily}.text-${sizeName}.leading-${leading}.${className},` + - `.font-${fontFamily} .text-${sizeName}.leading-${leading}.${className},` + - `.font-${fontFamily} .text-${sizeName} .leading-${leading}.${className},` + - `.text-${sizeName} .font-${fontFamily}.leading-${leading}.${className},` + - `.text-${sizeName} .font-${fontFamily} .leading-${leading}.${className}` - ) + return ( + `.font-${fontFamily}.text-${sizeName}.leading-${leading}.${className},` + + `.font-${fontFamily} .text-${sizeName}.leading-${leading}.${className},` + + `.font-${fontFamily} .text-${sizeName} .leading-${leading}.${className},` + + `.text-${sizeName} .font-${fontFamily}.leading-${leading}.${className},` + + `.text-${sizeName} .font-${fontFamily} .leading-${leading}.${className}` + ) } export function isPlainObject(value: unknown) { - if (Object.prototype.toString.call(value) !== '[object Object]') { - return false - } + if (Object.prototype.toString.call(value) !== '[object Object]') { + return false + } - let prototype = Object.getPrototypeOf(value) - return prototype === null || prototype === Object.prototype + let prototype = Object.getPrototypeOf(value) + return prototype === null || prototype === Object.prototype } type ThemeValue = string | string[] | ((p?: any) => string) @@ -75,29 +75,29 @@ type ThemeValue = string | string[] | ((p?: any) => string) * @see https://github.com/tailwindlabs/tailwindcss/blob/30ea5b14a631b6f68e56740c3d09bb54fcbad08a/src/util/transformThemeValue.js */ export function normalizeThemeValue(key: string, value: ThemeValue) { - switch (key) { - case 'fontSize': - case 'lineHeight': - if (typeof value === 'function') value = value({}) - if (Array.isArray(value)) value = value[0] + switch (key) { + case 'fontSize': + case 'lineHeight': + if (typeof value === 'function') value = value({}) + if (Array.isArray(value)) value = value[0] - return value + return value - case 'fontFamily': - if (typeof value === 'function') value = value({}) - if (Array.isArray(value)) value = value.join(', ') + case 'fontFamily': + if (typeof value === 'function') value = value({}) + if (Array.isArray(value)) value = value.join(', ') - return value + return value - default: - if (typeof value === 'function') value = value({}) + default: + if (typeof value === 'function') value = value({}) - return value - } + return value + } } export function round(value: number) { - return parseFloat(value.toFixed(4)).toString() + return parseFloat(value.toFixed(4)).toString() } /** @@ -105,14 +105,14 @@ export function round(value: number) { * as well as a `--line-height-offset` custom property. */ export function lineHeightProperties(lineHeight: string, rootSize: number) { - if (lineHeight === undefined) return {} + if (lineHeight === undefined) return {} - let lineHeightActual = isRelativeValue(lineHeight) - ? `calc(${getRelativeValue(lineHeight)} * var(--font-size-px))` - : normalizeValue(lineHeight, rootSize) + let lineHeightActual = isRelativeValue(lineHeight) + ? `calc(${getRelativeValue(lineHeight)} * var(--font-size-px))` + : normalizeValue(lineHeight, rootSize) - return { - '--line-height-offset': `calc((((var(--line-height-scale) * var(--font-size-px)) - ${lineHeightActual}) / 2) / var(--font-size-px))`, - 'line-height': lineHeight, - } + return { + '--line-height-offset': `calc((((var(--line-height-scale) * var(--font-size-px)) - ${lineHeightActual}) / 2) / var(--font-size-px))`, + 'line-height': lineHeight, + } } diff --git a/types/jest-matcher-css.d.ts b/types/jest-matcher-css.d.ts index 0d66c1f..9c807bf 100644 --- a/types/jest-matcher-css.d.ts +++ b/types/jest-matcher-css.d.ts @@ -1,5 +1,5 @@ declare namespace jest { - interface Matchers { - toMatchCss: (expected: string) => R - } + interface Matchers { + toMatchCss: (expected: string) => R + } } diff --git a/types/tailwindcss.d.ts b/types/tailwindcss.d.ts index 9371dbd..e533ac0 100644 --- a/types/tailwindcss.d.ts +++ b/types/tailwindcss.d.ts @@ -1,8 +1,8 @@ declare module 'tailwindcss' declare module 'tailwindcss/plugin' { - import { CreatePlugin } from '@navith/tailwindcss-plugin-author-types' + import { CreatePlugin } from '@navith/tailwindcss-plugin-author-types' - let createPlugin: CreatePlugin - export default createPlugin + let createPlugin: CreatePlugin + export default createPlugin }