@@ -856,6 +856,40 @@ const GRADIENT_MAP: Record<string, Record<string, string>> = {
856856 'bg-gradient-to-bl' : { 'background-image' : 'linear-gradient(to bottom left, var(--hw-gradient-stops))' } ,
857857 'bg-gradient-to-l' : { 'background-image' : 'linear-gradient(to left, var(--hw-gradient-stops))' } ,
858858 'bg-gradient-to-tl' : { 'background-image' : 'linear-gradient(to top left, var(--hw-gradient-stops))' } ,
859+ // Radial gradients
860+ 'bg-radial' : { 'background-image' : 'radial-gradient(var(--hw-gradient-stops))' } ,
861+ 'bg-radial-at-t' : { 'background-image' : 'radial-gradient(at top, var(--hw-gradient-stops))' } ,
862+ 'bg-radial-at-tr' : { 'background-image' : 'radial-gradient(at top right, var(--hw-gradient-stops))' } ,
863+ 'bg-radial-at-r' : { 'background-image' : 'radial-gradient(at right, var(--hw-gradient-stops))' } ,
864+ 'bg-radial-at-br' : { 'background-image' : 'radial-gradient(at bottom right, var(--hw-gradient-stops))' } ,
865+ 'bg-radial-at-b' : { 'background-image' : 'radial-gradient(at bottom, var(--hw-gradient-stops))' } ,
866+ 'bg-radial-at-bl' : { 'background-image' : 'radial-gradient(at bottom left, var(--hw-gradient-stops))' } ,
867+ 'bg-radial-at-l' : { 'background-image' : 'radial-gradient(at left, var(--hw-gradient-stops))' } ,
868+ 'bg-radial-at-tl' : { 'background-image' : 'radial-gradient(at top left, var(--hw-gradient-stops))' } ,
869+ 'bg-radial-at-c' : { 'background-image' : 'radial-gradient(at center, var(--hw-gradient-stops))' } ,
870+ // Conic gradients
871+ 'bg-conic' : { 'background-image' : 'conic-gradient(var(--hw-gradient-stops))' } ,
872+ 'bg-conic-from-t' : { 'background-image' : 'conic-gradient(from 0deg at center, var(--hw-gradient-stops))' } ,
873+ 'bg-conic-from-tr' : { 'background-image' : 'conic-gradient(from 45deg at center, var(--hw-gradient-stops))' } ,
874+ 'bg-conic-from-r' : { 'background-image' : 'conic-gradient(from 90deg at center, var(--hw-gradient-stops))' } ,
875+ 'bg-conic-from-br' : { 'background-image' : 'conic-gradient(from 135deg at center, var(--hw-gradient-stops))' } ,
876+ 'bg-conic-from-b' : { 'background-image' : 'conic-gradient(from 180deg at center, var(--hw-gradient-stops))' } ,
877+ 'bg-conic-from-bl' : { 'background-image' : 'conic-gradient(from 225deg at center, var(--hw-gradient-stops))' } ,
878+ 'bg-conic-from-l' : { 'background-image' : 'conic-gradient(from 270deg at center, var(--hw-gradient-stops))' } ,
879+ 'bg-conic-from-tl' : { 'background-image' : 'conic-gradient(from 315deg at center, var(--hw-gradient-stops))' } ,
880+ }
881+
882+ // Content utility - direct raw class to CSS
883+ const CONTENT_MAP : Record < string , Record < string , string > > = {
884+ 'content-none' : { content : 'none' } ,
885+ 'content-empty' : { content : '""' } ,
886+ }
887+
888+ // Scrollbar utilities - direct raw class to CSS
889+ const SCROLLBAR_MAP : Record < string , Record < string , string > > = {
890+ 'scrollbar-auto' : { 'scrollbar-width' : 'auto' } ,
891+ 'scrollbar-thin' : { 'scrollbar-width' : 'thin' } ,
892+ 'scrollbar-none' : { 'scrollbar-width' : 'none' } ,
859893}
860894
861895// =============================================================================
@@ -1251,6 +1285,10 @@ const STATIC_UTILITY_MAP: Record<string, Record<string, string>> = {
12511285 ...DROP_SHADOW_MAP ,
12521286 ...MIX_BLEND_MAP ,
12531287 ...BG_BLEND_MAP ,
1288+ // Content
1289+ ...CONTENT_MAP ,
1290+ // Scrollbar
1291+ ...SCROLLBAR_MAP ,
12541292}
12551293
12561294// Pre-computed variant selector map for O(1) lookup (shared across all instances)
@@ -1296,9 +1334,22 @@ const VARIANT_SELECTORS: Record<string, string> = {
12961334 'optional' : ':optional' ,
12971335}
12981336
1337+ // Not-* variants (negated pseudo-classes)
1338+ const NOT_VARIANT_SELECTORS : Record < string , string > = {
1339+ 'not-first' : ':not(:first-child)' ,
1340+ 'not-last' : ':not(:last-child)' ,
1341+ 'not-only' : ':not(:only-child)' ,
1342+ 'not-empty' : ':not(:empty)' ,
1343+ 'not-disabled' : ':not(:disabled)' ,
1344+ 'not-checked' : ':not(:checked)' ,
1345+ 'not-first-of-type' : ':not(:first-of-type)' ,
1346+ 'not-last-of-type' : ':not(:last-of-type)' ,
1347+ }
1348+
12991349// Pre-computed prefix variants (these modify the selector prefix, not suffix)
13001350const PREFIX_VARIANTS : Record < string , string > = {
13011351 'dark' : '.dark ' ,
1352+ 'light' : '.light ' ,
13021353 'rtl' : '[dir="rtl"] ' ,
13031354 'ltr' : '[dir="ltr"] ' ,
13041355}
@@ -1640,12 +1691,18 @@ export class CSSGenerator {
16401691 }
16411692
16421693 // Align content: content-{normal|center|start|end|between|around|evenly|baseline|stretch}
1694+ // Also handles CSS content property for arbitrary values: content-['hello'], content-[attr(data-label)]
16431695 if ( utility === 'content' && value ) {
16441696 const contentValue = ALIGN_CONTENT_VALUES [ value ]
16451697 if ( contentValue ) {
16461698 this . addRule ( parsed , { 'align-content' : contentValue } )
16471699 return
16481700 }
1701+ // Arbitrary content property: content-['hello'], content-[attr(data-label)]
1702+ if ( parsed . arbitrary ) {
1703+ this . addRule ( parsed , { content : value } )
1704+ return
1705+ }
16491706 }
16501707
16511708 // Align self: self-{auto|start|end|center|stretch|baseline}
@@ -1938,21 +1995,100 @@ export class CSSGenerator {
19381995 continue
19391996 }
19401997
1941- // Handle group-* variants
1998+ // Handle not-* variants: not-first, not-last, etc.
1999+ if ( variant . charCodeAt ( 0 ) === 110 && variant . startsWith ( 'not-' ) ) { // 'n' = 110
2000+ if ( this . variantEnabled . not ) {
2001+ const notSelector = NOT_VARIANT_SELECTORS [ variant ]
2002+ if ( notSelector ) {
2003+ selector += notSelector
2004+ }
2005+ }
2006+ continue
2007+ }
2008+
2009+ // Handle group-* variants (with optional named group: group/name-hover)
19422010 if ( variant . charCodeAt ( 0 ) === 103 && variant . startsWith ( 'group-' ) ) { // 'g' = 103
19432011 if ( this . variantEnabled . group ) {
19442012 const groupVariant = variant . slice ( 6 )
19452013 prefix = `.group:${ groupVariant } `
19462014 }
19472015 continue
19482016 }
2017+ // Named group: group/name (for nested groups)
2018+ if ( variant . charCodeAt ( 0 ) === 103 && variant . startsWith ( 'group/' ) ) { // 'g' = 103
2019+ if ( this . variantEnabled . group ) {
2020+ const groupName = variant . slice ( 6 )
2021+ prefix = `.group\\/${ groupName } `
2022+ }
2023+ continue
2024+ }
19492025
1950- // Handle peer-* variants
2026+ // Handle peer-* variants (with optional named peer)
19512027 if ( variant . charCodeAt ( 0 ) === 112 && variant . startsWith ( 'peer-' ) ) { // 'p' = 112
19522028 if ( this . variantEnabled . peer ) {
19532029 const peerVariant = variant . slice ( 5 )
19542030 prefix = `.peer:${ peerVariant } ~ `
19552031 }
2032+ continue
2033+ }
2034+ // Named peer: peer/name
2035+ if ( variant . charCodeAt ( 0 ) === 112 && variant . startsWith ( 'peer/' ) ) { // 'p' = 112
2036+ if ( this . variantEnabled . peer ) {
2037+ const peerName = variant . slice ( 5 )
2038+ prefix = `.peer\\/${ peerName } ~ `
2039+ }
2040+ continue
2041+ }
2042+
2043+ // Handle has-* variants: has-[input:checked], has-[:focus]
2044+ if ( variant . charCodeAt ( 0 ) === 104 && variant . startsWith ( 'has-' ) ) { // 'h' = 104
2045+ if ( this . variantEnabled . has ) {
2046+ const hasValue = variant . slice ( 4 )
2047+ // Arbitrary value: has-[selector]
2048+ if ( hasValue . charCodeAt ( 0 ) === 91 && hasValue . charCodeAt ( hasValue . length - 1 ) === 93 ) {
2049+ const inner = hasValue . slice ( 1 , - 1 )
2050+ selector += `:has(${ inner } )`
2051+ }
2052+ else {
2053+ // Named pseudo: has-checked -> :has(:checked)
2054+ selector += `:has(:${ hasValue } )`
2055+ }
2056+ }
2057+ continue
2058+ }
2059+
2060+ // Handle aria-* variants: aria-disabled, aria-[sort=ascending]
2061+ if ( variant . charCodeAt ( 0 ) === 97 && variant . startsWith ( 'aria-' ) ) { // 'a' = 97
2062+ if ( this . variantEnabled . aria ) {
2063+ const ariaValue = variant . slice ( 5 )
2064+ // Arbitrary value: aria-[sort=ascending]
2065+ if ( ariaValue . charCodeAt ( 0 ) === 91 && ariaValue . charCodeAt ( ariaValue . length - 1 ) === 93 ) {
2066+ const inner = ariaValue . slice ( 1 , - 1 )
2067+ selector += `[aria-${ inner } ]`
2068+ }
2069+ else {
2070+ // Boolean attribute: aria-disabled -> [aria-disabled="true"]
2071+ selector += `[aria-${ ariaValue } ="true"]`
2072+ }
2073+ }
2074+ continue
2075+ }
2076+
2077+ // Handle data-* variants: data-[state=active], data-loading
2078+ if ( variant . charCodeAt ( 0 ) === 100 && variant . startsWith ( 'data-' ) ) { // 'd' = 100
2079+ if ( this . variantEnabled . data ) {
2080+ const dataValue = variant . slice ( 5 )
2081+ // Arbitrary value: data-[state=active]
2082+ if ( dataValue . charCodeAt ( 0 ) === 91 && dataValue . charCodeAt ( dataValue . length - 1 ) === 93 ) {
2083+ const inner = dataValue . slice ( 1 , - 1 )
2084+ selector += `[data-${ inner } ]`
2085+ }
2086+ else {
2087+ // Boolean attribute: data-loading -> [data-loading]
2088+ selector += `[data-${ dataValue } ]`
2089+ }
2090+ }
2091+ continue
19562092 }
19572093 }
19582094
@@ -1979,7 +2115,8 @@ export class CSSGenerator {
19792115 return cached || undefined // Convert empty string to undefined
19802116 }
19812117
1982- let result : string | undefined
2118+ // Collect all media conditions — multiple media variants can stack
2119+ const mediaConditions : string [ ] = [ ]
19832120
19842121 for ( let i = 0 ; i < variantsLen ; i ++ ) {
19852122 const variant = variants [ i ]
@@ -1990,7 +2127,7 @@ export class CSSGenerator {
19902127 const breakpointKey = variant . slice ( 1 )
19912128 const breakpoint = this . screenBreakpoints . get ( breakpointKey )
19922129 if ( breakpoint ) {
1993- result = `@container (min-width: ${ breakpoint } )`
2130+ const result = `@container (min-width: ${ breakpoint } )`
19942131 this . mediaQueryCache . set ( cacheKey , result )
19952132 return result
19962133 }
@@ -2001,50 +2138,71 @@ export class CSSGenerator {
20012138 if ( this . variantEnabled . responsive ) {
20022139 const breakpoint = this . screenBreakpoints . get ( variant )
20032140 if ( breakpoint ) {
2004- result = `@media (min-width: ${ breakpoint } )`
2005- this . mediaQueryCache . set ( cacheKey , result )
2006- return result
2141+ mediaConditions . push ( `(min-width: ${ breakpoint } )` )
2142+ continue
20072143 }
20082144 }
20092145
2010- // Media preference variants - use switch for common cases
2146+ // Media preference variants
20112147 switch ( variant ) {
20122148 case 'print' :
2013- if ( this . variantEnabled . print ) {
2014- result = '@media print'
2015- this . mediaQueryCache . set ( cacheKey , result )
2016- return result
2017- }
2149+ if ( this . variantEnabled . print ) mediaConditions . push ( 'print' )
20182150 break
20192151 case 'motion-safe' :
2020- if ( this . variantEnabled [ 'motion-safe' ] ) {
2021- result = '@media (prefers-reduced-motion: no-preference)'
2022- this . mediaQueryCache . set ( cacheKey , result )
2023- return result
2024- }
2152+ if ( this . variantEnabled [ 'motion-safe' ] ) mediaConditions . push ( '(prefers-reduced-motion: no-preference)' )
20252153 break
20262154 case 'motion-reduce' :
2027- if ( this . variantEnabled [ 'motion-reduce' ] ) {
2028- result = '@media (prefers-reduced-motion: reduce)'
2029- this . mediaQueryCache . set ( cacheKey , result )
2030- return result
2031- }
2155+ if ( this . variantEnabled [ 'motion-reduce' ] ) mediaConditions . push ( '(prefers-reduced-motion: reduce)' )
20322156 break
20332157 case 'contrast-more' :
2034- if ( this . variantEnabled [ 'contrast-more' ] ) {
2035- result = '@media (prefers-contrast: more)'
2036- this . mediaQueryCache . set ( cacheKey , result )
2037- return result
2038- }
2158+ if ( this . variantEnabled [ 'contrast-more' ] ) mediaConditions . push ( '(prefers-contrast: more)' )
20392159 break
20402160 case 'contrast-less' :
2041- if ( this . variantEnabled [ 'contrast-less' ] ) {
2042- result = '@media (prefers-contrast: less)'
2043- this . mediaQueryCache . set ( cacheKey , result )
2044- return result
2045- }
2161+ if ( this . variantEnabled [ 'contrast-less' ] ) mediaConditions . push ( '(prefers-contrast: less)' )
2162+ break
2163+ case 'landscape' :
2164+ if ( this . variantEnabled . landscape ) mediaConditions . push ( '(orientation: landscape)' )
2165+ break
2166+ case 'portrait' :
2167+ if ( this . variantEnabled . portrait ) mediaConditions . push ( '(orientation: portrait)' )
2168+ break
2169+ case 'forced-colors' :
2170+ if ( this . variantEnabled [ 'forced-colors' ] ) mediaConditions . push ( '(forced-colors: active)' )
20462171 break
20472172 }
2173+
2174+ // Handle supports-* variant: supports-[display:grid] -> @supports (display: grid)
2175+ if ( variant . charCodeAt ( 0 ) === 115 && variant . startsWith ( 'supports-' ) ) { // 's' = 115
2176+ if ( this . variantEnabled . supports ) {
2177+ const supportsValue = variant . slice ( 9 )
2178+ let supportsQuery : string
2179+ if ( supportsValue . charCodeAt ( 0 ) === 91 && supportsValue . charCodeAt ( supportsValue . length - 1 ) === 93 ) {
2180+ const inner = supportsValue . slice ( 1 , - 1 ) . replace ( / _ / g, ' ' )
2181+ const colonIdx = inner . indexOf ( ':' )
2182+ if ( colonIdx !== - 1 ) {
2183+ const prop = inner . slice ( 0 , colonIdx ) . trim ( )
2184+ const val = inner . slice ( colonIdx + 1 ) . trim ( )
2185+ supportsQuery = `@supports (${ prop } : ${ val } )`
2186+ }
2187+ else {
2188+ supportsQuery = `@supports (${ inner } )`
2189+ }
2190+ }
2191+ else {
2192+ supportsQuery = `@supports (${ supportsValue } )`
2193+ }
2194+ // Supports queries don't combine with @media — return directly
2195+ this . mediaQueryCache . set ( cacheKey , supportsQuery )
2196+ return supportsQuery
2197+ }
2198+ }
2199+ }
2200+
2201+ if ( mediaConditions . length > 0 ) {
2202+ // Combine conditions: @media (min-width: 1024px) and (orientation: landscape)
2203+ const result = `@media ${ mediaConditions . join ( ' and ' ) } `
2204+ this . mediaQueryCache . set ( cacheKey , result )
2205+ return result
20482206 }
20492207
20502208 this . mediaQueryCache . set ( cacheKey , '' ) // Use empty string as "no result" marker
0 commit comments