Skip to content

Commit af6f0f3

Browse files
committed
feat: handle arbitrary bracket opacity on named colors
1 parent 8876156 commit af6f0f3

4 files changed

Lines changed: 81 additions & 15 deletions

File tree

packages/crosswind/src/parser.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,9 +1279,9 @@ function parseClassImpl(className: string): ParsedClass {
12791279
}
12801280
}
12811281

1282-
// Check for color opacity modifiers: bg-blue-500/50, text-red-500/75
1282+
// Check for color opacity modifiers: bg-blue-500/50, text-red-500/75, bg-white/[0.04]
12831283
// Must come before fractional values to avoid conflict
1284-
const opacityMatch = utility.match(/^([a-z]+(?:-[a-z]+)*?)-(.+?)\/(\d+)$/)
1284+
const opacityMatch = utility.match(/^([a-z]+(?:-[a-z]+)*?)-(.+?)\/(\d+|\[\d*\.?\d+\])$/)
12851285
if (opacityMatch && ['bg', 'text', 'border', 'ring', 'placeholder', 'divide'].includes(opacityMatch[1])) {
12861286
return {
12871287
raw: className,

packages/crosswind/src/rules-effects.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -272,10 +272,21 @@ export const shadowColorRule: UtilityRule = (parsed, config) => {
272272

273273
if (slashIdx !== -1) {
274274
colorName = value.slice(0, slashIdx)
275-
const opacityValue = Number.parseInt(value.slice(slashIdx + 1), 10)
276-
if (Number.isNaN(opacityValue) || opacityValue < 0 || opacityValue > 100)
277-
return undefined
278-
opacity = opacityValue / 100
275+
const opacityStr = value.slice(slashIdx + 1)
276+
277+
// Handle arbitrary opacity: /[0.04], /[0.5]
278+
if (opacityStr.charCodeAt(0) === 91 && opacityStr.charCodeAt(opacityStr.length - 1) === 93) {
279+
const arbitraryOpacity = Number.parseFloat(opacityStr.slice(1, -1))
280+
if (Number.isNaN(arbitraryOpacity) || arbitraryOpacity < 0 || arbitraryOpacity > 1)
281+
return undefined
282+
opacity = arbitraryOpacity
283+
}
284+
else {
285+
const opacityValue = Number.parseInt(opacityStr, 10)
286+
if (Number.isNaN(opacityValue) || opacityValue < 0 || opacityValue > 100)
287+
return undefined
288+
opacity = opacityValue / 100
289+
}
279290
}
280291
else {
281292
colorName = value

packages/crosswind/src/rules.ts

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -378,13 +378,24 @@ export const colorRule: UtilityRule = (parsed, config) => {
378378

379379
if (slashIdx !== -1) {
380380
colorValue = value.slice(0, slashIdx)
381-
const opacityValue = Number.parseInt(value.slice(slashIdx + 1), 10)
381+
const opacityStr = value.slice(slashIdx + 1)
382382

383-
// Validate opacity is in 0-100 range
384-
if (Number.isNaN(opacityValue) || opacityValue < 0 || opacityValue > 100) {
385-
return undefined
383+
// Handle arbitrary opacity: /[0.04], /[0.5], /[.15]
384+
if (opacityStr.charCodeAt(0) === 91 && opacityStr.charCodeAt(opacityStr.length - 1) === 93) { // '[' and ']'
385+
const arbitraryOpacity = Number.parseFloat(opacityStr.slice(1, -1))
386+
if (Number.isNaN(arbitraryOpacity) || arbitraryOpacity < 0 || arbitraryOpacity > 1) {
387+
return undefined
388+
}
389+
opacity = arbitraryOpacity
390+
}
391+
else {
392+
// Standard integer opacity: /50, /75 (0-100 scale)
393+
const opacityValue = Number.parseInt(opacityStr, 10)
394+
if (Number.isNaN(opacityValue) || opacityValue < 0 || opacityValue > 100) {
395+
return undefined
396+
}
397+
opacity = opacityValue / 100
386398
}
387-
opacity = opacityValue / 100
388399

389400
// Try flat cache with base color value
390401
const baseColor = flatColorCache!.get(colorValue)
@@ -485,10 +496,23 @@ export const placeholderColorRule: UtilityRule = (parsed, config) => {
485496
else {
486497
// With opacity modifier
487498
const colorValue = value.slice(0, slashIdx)
488-
const opacityValue = Number.parseInt(value.slice(slashIdx + 1), 10)
489-
if (Number.isNaN(opacityValue) || opacityValue < 0 || opacityValue > 100)
490-
return undefined
491-
const opacity = opacityValue / 100
499+
const opacityStr = value.slice(slashIdx + 1)
500+
let opacity: number
501+
502+
// Handle arbitrary opacity: /[0.04], /[0.5]
503+
if (opacityStr.charCodeAt(0) === 91 && opacityStr.charCodeAt(opacityStr.length - 1) === 93) {
504+
const arbitraryOpacity = Number.parseFloat(opacityStr.slice(1, -1))
505+
if (Number.isNaN(arbitraryOpacity) || arbitraryOpacity < 0 || arbitraryOpacity > 1)
506+
return undefined
507+
opacity = arbitraryOpacity
508+
}
509+
else {
510+
const opacityValue = Number.parseInt(opacityStr, 10)
511+
if (Number.isNaN(opacityValue) || opacityValue < 0 || opacityValue > 100)
512+
return undefined
513+
opacity = opacityValue / 100
514+
}
515+
492516
const baseColor = flatColorCache.get(colorValue)
493517
if (baseColor) {
494518
return {

packages/crosswind/test/colors.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,37 @@ describe('Edge Cases', () => {
236236
const css = gen.toCSS(false)
237237
expect(css).toContain('background-color: rgb(0 255 0 / 0.75);')
238238
})
239+
240+
it('should handle arbitrary bracket opacity on named colors: bg-white/[0.04]', () => {
241+
const gen = new CSSGenerator(defaultConfig)
242+
gen.generate('bg-white/[0.04]')
243+
const css = gen.toCSS(false)
244+
expect(css).toContain('background-color:')
245+
expect(css).toContain('0.04')
246+
})
247+
248+
it('should handle arbitrary bracket opacity on shade colors: text-white/[0.5]', () => {
249+
const gen = new CSSGenerator(defaultConfig)
250+
gen.generate('text-white/[0.5]')
251+
const css = gen.toCSS(false)
252+
expect(css).toContain('color:')
253+
expect(css).toContain('0.5')
254+
})
255+
256+
it('should handle arbitrary bracket opacity on border colors: border-white/[0.06]', () => {
257+
const gen = new CSSGenerator(defaultConfig)
258+
gen.generate('border-white/[0.06]')
259+
const css = gen.toCSS(false)
260+
expect(css).toContain('border-color:')
261+
expect(css).toContain('0.06')
262+
})
263+
264+
it('should reject arbitrary bracket opacity > 1', () => {
265+
const gen = new CSSGenerator(defaultConfig)
266+
gen.generate('bg-white/[1.5]')
267+
const css = gen.toCSS(false)
268+
expect(css).not.toContain('background-color:')
269+
})
239270
})
240271

241272
describe('Color with variants', () => {

0 commit comments

Comments
 (0)