Tailwind 与文本和文本阴影上的颜色合并

问题描述 投票:0回答:1

tl;dr 我的

text-shadow
中有一个
tailwind.config.ts
实用程序类,它允许我更改阴影的尺寸和颜色。我正在使用 Tailwind Merge,我不知道如何阻止
text-shadow-{size/color}
text-{color}
发生冲突。

问题

通常在 CSS 中,使用文本阴影对于在文本周围添加很酷的设计甚至添加文本对比度非常有帮助,而不是使用投影。不久前,我为我的 Tailwind Config 创建了一个

text-shadow
实用程序类,它工作得很好,直到我在利用 Tailwind Merge 的组件上使用它。 Tailwind Merge 是一个很棒的包,但是当使用自定义实用程序类时,它可能会感到困惑。

解决方案

当然,我的目标是使用

extendTailwindMerge
来纠正这个问题。关于配置 Tailwind Merge 的文档非常详细,但由于它只提供了
foo
bar
baz
作为示例,我有点困惑如何做特定的事情。

询问

请查看我的

tailwind.config.ts
和自定义
twMerge()
功能,如果您有任何想法,请告诉我。谢谢!

代码

// tailwind.config.ts

// * Types
import type { Config } from 'tailwindcss'
import type { CSSRuleObject, ThemeConfig } from 'tailwindcss/types/config'

/**
 * ### Decimal Alpha to HEX
 * - Converts an RGB decimal alpha value to hexidecimal alpha format
 * @param decimalAlpha
 * @returns
 */
export function decimalAlphaToHex(decimalAlpha: number): string {
  // Ensure the input is within the valid range
  if (decimalAlpha < 0 || decimalAlpha > 1)
    throw new Error('Decimal alpha value must be between 0 and 1')

  // Convert decimal alpha to a hexadecimal value
  const alphaHex = Math.floor(decimalAlpha * 255)
    .toString(16)
    .toUpperCase()

  // Ensure the hexadecimal value is two digits long (e.g., 0A instead of A)
  if (alphaHex.length < 2) {
    return '0' + alphaHex
  } else {
    return alphaHex
  }
}

type GetTheme = <
  TDefaultValue =
    | Partial<
        ThemeConfig & {
          extend: Partial<ThemeConfig>
        }
      >
    | undefined,
>(
  path?: string | undefined,
  defaultValue?: TDefaultValue | undefined,
) => TDefaultValue

// * Plugins
import plugin from 'tailwindcss/plugin'
import headlessui from '@headlessui/tailwindcss'

// @ts-ignore
import { default as flattenColorPalette } from 'tailwindcss/lib/util/flattenColorPalette'

const config: Config = {
  content: [
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    textShadow: {
      sm: '0 0 0.125rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))',
      DEFAULT: '0 0 0.25rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))',
      md: '0 0 0.5rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))',
      lg: '0 0 0.75rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))',
      xl: '0 0 1rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))',
      '2xl': '0 0 2rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))',
      '3xl': '0 0 3rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))',
      none: 'none',
    },
  },
  plugins: [
    plugin(function ({ matchUtilities, theme }) {
      const colors: { [key: string]: string } = {},
        opacities: { [key: string]: string } = flattenColorPalette(
          theme('opacity'),
        ),
        opacityEntries = Object.entries(opacities)

      Object.entries(flattenColorPalette(theme('colors'))).forEach((color) => {
        const [key, value] = color

        if (typeof key !== 'string' || typeof value !== 'string') return null

        colors[key] = value.replace(' / <alpha-value>', '')

        if (value.startsWith('#') && value.length === 7)
          opacityEntries.forEach(([opacityKey, opacityValue]) => {
            colors[`${key}/${opacityKey}`] = `${value}${decimalAlphaToHex(
              Number(opacityValue),
            )}`
          })

        if (value.startsWith('#') && value.length === 4)
          opacityEntries.forEach(([opacityKey, opacityValue]) => {
            colors[`${key}/${opacityKey}`] = `${value}${value.slice(
              1,
            )}${decimalAlphaToHex(Number(opacityValue))}`
          })

        if (value.startsWith('rgb') || value.startsWith('hsl'))
          opacityEntries.forEach(([opacityKey, opacityValue]) => {
            colors[`${key}/${opacityKey}`] = `${value.slice(
              0,
              -1,
            )} / ${opacityValue})`.replace(' / <alpha-value>', '')
          })
      })

      matchUtilities(
        {
          'text-shadow': (value) => {
            const cssProperties: CSSRuleObject = {}

            if (
              typeof value === 'string' &&
              (value.startsWith('#') ||
                value.startsWith('rgb') ||
                value.startsWith('hsl'))
            ) {
              cssProperties['--tw-text-shadow-color'] = value
            } else {
              cssProperties['text-shadow'] = value
            }

            return cssProperties
          },
        },
        { values: { ...theme('textShadow'), ...colors } },
      )
    }),
  ],
}

export default config

我最好的尝试

// utils/custom-tailwind-merge.ts

import { extendTailwindMerge } from 'tailwind-merge'
import colors from 'tailwindcss/colors'

const colorList: { [key: string]: string[] }[] = []
const excludedColors = [
  'black',
  'blueGray',
  'coolGray',
  'inherit',
  'transparent',
  'trueGray',
  'warmGray',
  'white',
]

Object.entries(colors).forEach(([colorName, valueList]) => {
  if (excludedColors.includes(colorName)) return

  colorList.push({ [colorName]: Object.keys(valueList) })
})

type AdditionalClassGroupIds = 'text-shadow'

export const twMerge = extendTailwindMerge<AdditionalClassGroupIds>({
  extend: {
    classGroups: {
      'text-shadow': [
        'sm',
        'DEFAULT',
        'md',
        'lg',
        'xl',
        '2xl',
        '3xl',
        'none',
        ...colorList,
        'transparent',
        'white',
        'black',
      ],
    },
  },
})
// components/link.tsx

import type { LinkProps } from '@/typings/components'
import { twMerge } from '@/utils/custom-tailwind-merge'

export default function Link({ children, className, href }: LinkProps) {
  const defaultClasses = 'text-blue-500'

  return (
    <a href={href} className={twMerge(defaultClasses, className)}>{children}</a>
  )
}

示例

输入

import Link from '@/components/link'

export default function Page() {
  return (
    <Link href='https://andrilla.net' className='text-shadow-lg text-shadow-red-500'>Website</Link>
  )
}

预期输出

<a href='https://andrilla.net' class='text-blue-500 text-shadow-lg text-shadow-red-500'>Website</a>

实际产量

<a href='https://andrilla.net' class='text-shadow-lg'>Website</a>

javascript css reactjs next.js tailwind-css
1个回答
0
投票

错误识别

根据有关班级组配置的文档

该库使用“类组”的概念,它是一组 Tailwind 类,它们都修改相同的 CSS 属性。例如。这是职位类别组。 const positionClassGroup = ['static', 'fixed', 'absolute', 'relative', 'sticky']

tailwind-merge 解决类组中的类之间的冲突,并且仅保留传递给合并函数调用的最后一个。

这意味着您的
text-shadow-<size>

text-shadow-<color>
类将取消相互覆盖,更不用说
text-<color>
类了。因此,为什么只渲染
text-shadow-*
类(根据我的测试,它是
text-shadow-red-500
类,因为它是
twMerge()
调用中的最后一个)。
无论如何,您的类组配置无论如何都是不正确的,因为第一个键是类组“ID”,它应该是一个对象数组,其中

那些

键可以是带有值数组的类名前缀: extend: { classGroups: { 'text-shadow': [{ 'text-shadow': […] }]

解决方案

同一个班级组文档中的进一步内容:

Tailwind 类通常共享类名的开头,因此类组中的元素也可以是具有与类组相同形状的值的对象(是的,形状是递归的)。在对象中,每个键与相应数组中的所有元素相连,中间有破折号 (
-

)。

例如这是溢出类组,导致类 

overflow-auto

overflow-hidden
overflow-visible
overflow-scroll
const overflowClassGroup = [{ overflow: ['auto', 'hidden', 'visible', 'scroll'] }]

以及来自
默认配置

的一些示例,它们共享相同的前缀: 'font-size': [{ text: ['base', isTshirtSize, isArbitraryLength] }], // … 'text-alignment': [{ text: ['left', 'center', 'right', 'justify', 'start', 'end'] }], // … 'text-color': [{ text: [colors] }],

'font-weight': [
  {
    font: [
      'thin',
      'extralight',
// …

'font-family': [{ font: [isAny] }],
我们可以在自己的配置中遵循相同的模式,其中第一个键是某个组 ID,该值包含我们的类名称规范并分为两个单独的类组:

extend: { classGroups: { 'text-shadow-size': [ { 'text-shadow': [ 'sm', 'DEFAULT', 'md', 'lg', 'xl', '2xl', '3xl', 'none', ], }, ], 'text-shadow-color': [ { 'text-shadow': [ ...colorList, 'transparent', 'white', 'black', ], }, ], }, },

//// tailwind.config.ts const flattenColorPalette = (colors)=>Object.assign({}, ...Object.entries(colors !== null && colors !== void 0 ? colors : {}).flatMap(([color, values])=>typeof values == "object" ? Object.entries(flattenColorPalette(values)).map(([number, hex])=>({ [color + (number === "DEFAULT" ? "" : `-${number}`)]: hex })) : [ { [`${color}`]: values } ])); /** * ### Decimal Alpha to HEX * - Converts an RGB decimal alpha value to hexidecimal alpha format * @param decimalAlpha * @returns */ function decimalAlphaToHex(decimalAlpha) { // Ensure the input is within the valid range if (decimalAlpha < 0 || decimalAlpha > 1) throw new Error('Decimal alpha value must be between 0 and 1') // Convert decimal alpha to a hexadecimal value const alphaHex = Math.floor(decimalAlpha * 255) .toString(16) .toUpperCase() // Ensure the hexadecimal value is two digits long (e.g., 0A instead of A) if (alphaHex.length < 2) { return '0' + alphaHex } else { return alphaHex } } tailwind.config = { theme: { textShadow: { sm: '0 0 0.125rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))', DEFAULT: '0 0 0.25rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))', md: '0 0 0.5rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))', lg: '0 0 0.75rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))', xl: '0 0 1rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))', '2xl': '0 0 2rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))', '3xl': '0 0 3rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))', none: 'none', }, }, plugins: [ tailwind.plugin(function ({ matchUtilities, theme }) { const colors = {}, opacities = flattenColorPalette( theme('opacity') ), opacityEntries = Object.entries(opacities) Object.entries(flattenColorPalette(theme('colors'))).forEach((color) => { const [key, value] = color if (typeof key !== 'string' || typeof value !== 'string') return null colors[key] = value.replace(' / <alpha-value>', '') if (value.startsWith('#') && value.length === 7) opacityEntries.forEach(([opacityKey, opacityValue]) => { colors[`${key}/${opacityKey}`] = `${value}${decimalAlphaToHex( Number(opacityValue) )}` }) if (value.startsWith('#') && value.length === 4) opacityEntries.forEach(([opacityKey, opacityValue]) => { colors[`${key}/${opacityKey}`] = `${value}${value.slice( 1 )}${decimalAlphaToHex(Number(opacityValue))}` }) if (value.startsWith('rgb') || value.startsWith('hsl')) opacityEntries.forEach(([opacityKey, opacityValue]) => { colors[`${key}/${opacityKey}`] = `${value.slice( 0, -1 )} / ${opacityValue})`.replace(' / <alpha-value>', '') }) }) matchUtilities( { 'text-shadow': (value) => { const cssProperties = {} if ( typeof value === 'string' && (value.startsWith('#') || value.startsWith('rgb') || value.startsWith('hsl')) ) { cssProperties['--tw-text-shadow-color'] = value } else { cssProperties['text-shadow'] = value } return cssProperties }, }, { values: { ...theme('textShadow'), ...colors } } ) }), ], } //// utils/custom-tailwind-merge.ts const { colors } = tailwind; const { extendTailwindMerge } = tailwindMerge; const colorList = []; const excludedColors = [ 'black', 'blueGray', 'coolGray', 'inherit', 'transparent', 'trueGray', 'warmGray', 'white', ] Object.entries(colors).forEach(([colorName, valueList]) => { if (excludedColors.includes(colorName)) return colorList.push({ [colorName]: Object.keys(valueList) }) }) const twMerge_ = extendTailwindMerge({ extend: { classGroups: { 'text-shadow-size': [ { 'text-shadow': [ 'sm', 'DEFAULT', 'md', 'lg', 'xl', '2xl', '3xl', 'none', ], }, ], 'text-shadow-color': [ { 'text-shadow': [ ...colorList, 'transparent', 'white', 'black', ], }, ], }, }, }) //// Link.ts function Link({ children, className, href }) { const defaultClasses = 'text-blue-500' return ( <a href={href} className={twMerge_(defaultClasses, className)}>{children}</a> ) } //// Input ReactDOM.createRoot(document.getElementById('app')).render( <React.Fragment> <Link href='https://andrilla.net' className='text-shadow-lg text-shadow-red-500'>Website</Link> <Link href='' className='text-shadow-lg text-shadow-red-500 text-shadow-blue-200 text-shadow-md text-red-700'>Website</Link> </React.Fragment> );
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js" integrity="sha512-QVs8Lo43F9lSuBykadDb0oSXDL/BbZ588urWVCRwSIoewQv/Ewg1f84mK3U790bZ0FfhFa1YSQUmIhG+pIRKeg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js" integrity="sha512-6a1107rTlA4gYpgHAqbwLAtxmWipBdJFcq8y5S/aTge3Bp+VAklABm2LO+Kg51vOWR9JMZq1Ovjl5tpluNpTeQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.tailwindcss.com/3.4.3"></script>
<script>window.exports = {}</script>
<script src="https://unpkg.com/[email protected]/dist/bundle-cjs.js"></script>
<script>window.tailwindMerge = window.exports; window.exports = {}</script>


<div id="app"></div>

© www.soinside.com 2019 - 2024. All rights reserved.