-
-
Notifications
You must be signed in to change notification settings - Fork 5.1k
fix: preserve opacity values in multiple shadows using color-mix #19819
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,77 @@ import { segment } from './segment' | |||||||||||||||||||
| const KEYWORDS = new Set(['inset', 'inherit', 'initial', 'revert', 'unset']) | ||||||||||||||||||||
| const LENGTH = /^-?(\d+|\.\d+)(.*?)$/g | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * Extract the alpha channel from a color value. | ||||||||||||||||||||
| * Returns the alpha as a percentage string (e.g., "12%") or null if no alpha is found. | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| function extractAlpha(color: string): string | null { | ||||||||||||||||||||
| // Modern rgba/hsla syntax with slash: rgba(0 0 0 / 0.12) or rgba(0 0 0 / 12%) | ||||||||||||||||||||
| const slashAlphaMatch = color.match(/\/\s*([\d.]+)%?\s*\)/) | ||||||||||||||||||||
| if (slashAlphaMatch) { | ||||||||||||||||||||
| let alpha = slashAlphaMatch[1] | ||||||||||||||||||||
| // If it's already a percentage, return it | ||||||||||||||||||||
| if (slashAlphaMatch[0].includes('%')) { | ||||||||||||||||||||
| return `${alpha}%` | ||||||||||||||||||||
| } | ||||||||||||||||||||
| // Convert decimal to percentage | ||||||||||||||||||||
| const alphaNum = parseFloat(alpha) | ||||||||||||||||||||
| if (!isNaN(alphaNum)) { | ||||||||||||||||||||
| // Round to avoid floating point precision issues | ||||||||||||||||||||
| return `${Math.round(alphaNum * 100)}%` | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return null | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Legacy rgba/hsla syntax with comma: rgba(0, 0, 0, 0.12) | ||||||||||||||||||||
| // Must be rgba/hsla (with 'a') to have 4 values where the last is alpha`n const commaAlphaMatch = color.match(/^(?:rgba|hsla)\([^,]+,[^,]+,[^,]+,\s*([\d.]+)\s*\)$/i) | ||||||||||||||||||||
| if (commaAlphaMatch) { | ||||||||||||||||||||
|
Comment on lines
+28
to
+30
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n packages/tailwindcss/src/utils/replace-shadow-colors.ts | sed -n '25,35p'Repository: tailwindlabs/tailwindcss Length of output: 627 🏁 Script executed: rg "commaAlphaMatch" packages/tailwindcss/src/utils/replace-shadow-colors.tsRepository: tailwindlabs/tailwindcss Length of output: 299 🏁 Script executed: cd packages/tailwindcss && [ -f tsconfig.json ] && cat tsconfig.json || echo "No tsconfig.json found"Repository: tailwindlabs/tailwindcss Length of output: 109 Restore the The 🐛 Proposed fix // Legacy rgba/hsla syntax with comma: rgba(0, 0, 0, 0.12)
- // Must be rgba/hsla (with 'a') to have 4 values where the last is alpha`n const commaAlphaMatch = color.match(/^(?:rgba|hsla)\([^,]+,[^,]+,[^,]+,\s*([\d.]+)\s*\)$/i)
+ // Must be rgba/hsla (with 'a') to have 4 values where the last is alpha.
+ const commaAlphaMatch = color.match(
+ /^(?:rgba|hsla)\([^,]+,[^,]+,[^,]+,\s*([\d.]+)\s*\)$/i,
+ )
if (commaAlphaMatch) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| const alpha = commaAlphaMatch[1] | ||||||||||||||||||||
| const alphaNum = parseFloat(alpha) | ||||||||||||||||||||
| if (!isNaN(alphaNum)) { | ||||||||||||||||||||
| // Round to avoid floating point precision issues | ||||||||||||||||||||
| return `${Math.round(alphaNum * 100)}%` | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return null | ||||||||||||||||||||
| } | ||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| // No alpha found | ||||||||||||||||||||
| return null | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * Extract the base color without the alpha channel. | ||||||||||||||||||||
| * For rgba/hsla, returns rgb/hsl with the same values but without alpha. | ||||||||||||||||||||
| * For other colors, returns as-is. | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| function stripAlpha(color: string): string { | ||||||||||||||||||||
| // Modern rgba/hsla syntax with slash: rgba(0 0 0 / 0.12) or hsla(0 0% 0% / 0.3) | ||||||||||||||||||||
| const slashMatch = color.match(/^(rgba?|hsla?)\(([\d\s.%]+)\s*\/\s*[\d.]+%?\s*\)$/i) | ||||||||||||||||||||
| if (slashMatch) { | ||||||||||||||||||||
| const type = slashMatch[1].toLowerCase().replace('a', '') | ||||||||||||||||||||
| const values = slashMatch[2].trim() | ||||||||||||||||||||
| return `${type}(${values})` | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Legacy rgba/hsla syntax with comma: rgba(0, 0, 0, 0.12) or hsla(0, 0%, 0%, 0.3) | ||||||||||||||||||||
| const commaMatch = color.match(/^(rgba?|hsla?)\(([\d\s.,%]+),\s*[\d.]+\s*\)$/i) | ||||||||||||||||||||
| if (commaMatch) { | ||||||||||||||||||||
| const type = commaMatch[1].toLowerCase().replace('a', '') | ||||||||||||||||||||
| const values = commaMatch[2].trim() | ||||||||||||||||||||
| return `${type}(${values})` | ||||||||||||||||||||
| } | ||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| // No alpha to strip | ||||||||||||||||||||
| return color | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * Check if a string already contains alpha handling (oklab, color-mix, etc.) | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| function hasAlphaHandling(value: string): boolean { | ||||||||||||||||||||
| return value.includes('oklab(') || value.includes('color-mix(') || value.includes('oklch(') | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export function replaceShadowColors(input: string, replacement: (color: string) => string) { | ||||||||||||||||||||
| let shadows = segment(input, ',').map((shadow) => { | ||||||||||||||||||||
| shadow = shadow.trim() | ||||||||||||||||||||
|
|
@@ -33,7 +104,25 @@ export function replaceShadowColors(input: string, replacement: (color: string) | |||||||||||||||||||
| // we can't know what to replace. | ||||||||||||||||||||
| if (offsetX === null || offsetY === null) return shadow | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let replacementColor = replacement(color ?? 'currentcolor') | ||||||||||||||||||||
| // Extract alpha from the original color if present | ||||||||||||||||||||
| let alpha: string | null = null | ||||||||||||||||||||
| let baseColor: string = color ?? 'currentcolor' | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (color) { | ||||||||||||||||||||
| alpha = extractAlpha(color) | ||||||||||||||||||||
| if (alpha) { | ||||||||||||||||||||
| baseColor = stripAlpha(color) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let replacementColor = replacement(baseColor) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Only apply color-mix wrapping if: | ||||||||||||||||||||
| // 1. The original color had an alpha channel | ||||||||||||||||||||
| // 2. The replacement doesn't already have alpha handling (oklab, color-mix, etc.) | ||||||||||||||||||||
| if (alpha && !hasAlphaHandling(replacementColor)) { | ||||||||||||||||||||
| replacementColor = `color-mix(in srgb, transparent, ${replacementColor} ${alpha})` | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (color !== null) { | ||||||||||||||||||||
| // If a color was found, replace the color. | ||||||||||||||||||||
|
|
||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: tailwindlabs/tailwindcss
Length of output: 125
🏁 Script executed:
Repository: tailwindlabs/tailwindcss
Length of output: 5588
🏁 Script executed:
Repository: tailwindlabs/tailwindcss
Length of output: 531
🏁 Script executed:
Repository: tailwindlabs/tailwindcss
Length of output: 2425
Fix regex mismatch between
extractAlpha()andstripAlpha()for consistent color/alpha handling.Current code has misaligned patterns that create color parsing failures:
extractAlpha()matcheshsl(180deg 50% 50% / 0.3)butstripAlpha()rejects it (character class[\d\s.%]+excludes unit letters likedeg), leaving/ 0.3in the base color passed toreplacement().rgba(0, 0, 0, 12%)andhsla(..., 30%)are never extracted, so custom--tw-shadow-coloroverrides lose per-shadow opacity.The proposed fix patterns are correct, but the diff contains a group index error: the slash pattern
/^(rgba?|hsla?)\((.+)\s*\/\s*([\d.]+%?)\s*\)$/icaptures alpha in group [3], not [2] as shown in the diff. Correct the assignment toslashAlphaMatch[3]and apply the corresponding fixes tostripAlpha()to handle both modern (slash) and legacy (comma) syntax consistently.🤖 Prompt for AI Agents