@@ -157,6 +157,25 @@ const ANIMATION_MAP: Record<string, Record<string, string>> = {
157157 'animate-bounce' : { animation : 'bounce 1s infinite' } ,
158158}
159159
160+ // Keyframe definitions for built-in animations
161+ const KEYFRAMES : Record < string , string > = {
162+ spin : `@keyframes spin {
163+ from { transform: rotate(0deg); }
164+ to { transform: rotate(360deg); }
165+ }` ,
166+ ping : `@keyframes ping {
167+ 75%, 100% { transform: scale(2); opacity: 0; }
168+ }` ,
169+ pulse : `@keyframes pulse {
170+ 0%, 100% { opacity: 1; }
171+ 50% { opacity: .5; }
172+ }` ,
173+ bounce : `@keyframes bounce {
174+ 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8, 0, 1, 1); }
175+ 50% { transform: translateY(0); animation-timing-function: cubic-bezier(0, 0, 0.2, 1); }
176+ }` ,
177+ }
178+
160179// Transform origin - utility="origin", value lookup
161180const TRANSFORM_ORIGIN_VALUES : Record < string , string > = {
162181 'center' : 'center' ,
@@ -1116,16 +1135,16 @@ const BLUR_MAP: Record<string, Record<string, string>> = {
11161135 'blur-3xl' : { filter : 'blur(64px)' } ,
11171136}
11181137
1119- // Backdrop blur - direct raw class to CSS
1138+ // Backdrop blur - direct raw class to CSS (with -webkit- prefix for Safari)
11201139const BACKDROP_BLUR_MAP : Record < string , Record < string , string > > = {
1121- 'backdrop-blur-none' : { 'backdrop-filter' : 'blur(0)' } ,
1122- 'backdrop-blur-sm' : { 'backdrop-filter' : 'blur(4px)' } ,
1123- 'backdrop-blur' : { 'backdrop-filter' : 'blur(8px)' } ,
1124- 'backdrop-blur-md' : { 'backdrop-filter' : 'blur(12px)' } ,
1125- 'backdrop-blur-lg' : { 'backdrop-filter' : 'blur(16px)' } ,
1126- 'backdrop-blur-xl' : { 'backdrop-filter' : 'blur(24px)' } ,
1127- 'backdrop-blur-2xl' : { 'backdrop-filter' : 'blur(40px)' } ,
1128- 'backdrop-blur-3xl' : { 'backdrop-filter' : 'blur(64px)' } ,
1140+ 'backdrop-blur-none' : { '-webkit-backdrop-filter' : 'blur(0)' , ' backdrop-filter' : 'blur(0)' } ,
1141+ 'backdrop-blur-sm' : { '-webkit-backdrop-filter' : 'blur(4px)' , ' backdrop-filter' : 'blur(4px)' } ,
1142+ 'backdrop-blur' : { '-webkit-backdrop-filter' : 'blur(8px)' , ' backdrop-filter' : 'blur(8px)' } ,
1143+ 'backdrop-blur-md' : { '-webkit-backdrop-filter' : 'blur(12px)' , ' backdrop-filter' : 'blur(12px)' } ,
1144+ 'backdrop-blur-lg' : { '-webkit-backdrop-filter' : 'blur(16px)' , ' backdrop-filter' : 'blur(16px)' } ,
1145+ 'backdrop-blur-xl' : { '-webkit-backdrop-filter' : 'blur(24px)' , ' backdrop-filter' : 'blur(24px)' } ,
1146+ 'backdrop-blur-2xl' : { '-webkit-backdrop-filter' : 'blur(40px)' , ' backdrop-filter' : 'blur(40px)' } ,
1147+ 'backdrop-blur-3xl' : { '-webkit-backdrop-filter' : 'blur(64px)' , ' backdrop-filter' : 'blur(64px)' } ,
11291148}
11301149
11311150// Grayscale/invert/sepia - direct raw class to CSS
@@ -1138,14 +1157,14 @@ const FILTER_TOGGLE_MAP: Record<string, Record<string, string>> = {
11381157 'sepia' : { filter : 'sepia(100%)' } ,
11391158}
11401159
1141- // Backdrop grayscale/invert/sepia - direct raw class to CSS
1160+ // Backdrop grayscale/invert/sepia - direct raw class to CSS (with -webkit- prefix)
11421161const BACKDROP_FILTER_TOGGLE_MAP : Record < string , Record < string , string > > = {
1143- 'backdrop-grayscale-0' : { 'backdrop-filter' : 'grayscale(0)' } ,
1144- 'backdrop-grayscale' : { 'backdrop-filter' : 'grayscale(100%)' } ,
1145- 'backdrop-invert-0' : { 'backdrop-filter' : 'invert(0)' } ,
1146- 'backdrop-invert' : { 'backdrop-filter' : 'invert(100%)' } ,
1147- 'backdrop-sepia-0' : { 'backdrop-filter' : 'sepia(0)' } ,
1148- 'backdrop-sepia' : { 'backdrop-filter' : 'sepia(100%)' } ,
1162+ 'backdrop-grayscale-0' : { '-webkit-backdrop-filter' : 'grayscale(0)' , ' backdrop-filter' : 'grayscale(0)' } ,
1163+ 'backdrop-grayscale' : { '-webkit-backdrop-filter' : 'grayscale(100%)' , ' backdrop-filter' : 'grayscale(100%)' } ,
1164+ 'backdrop-invert-0' : { '-webkit-backdrop-filter' : 'invert(0)' , ' backdrop-filter' : 'invert(0)' } ,
1165+ 'backdrop-invert' : { '-webkit-backdrop-filter' : 'invert(100%)' , ' backdrop-filter' : 'invert(100%)' } ,
1166+ 'backdrop-sepia-0' : { '-webkit-backdrop-filter' : 'sepia(0)' , ' backdrop-filter' : 'sepia(0)' } ,
1167+ 'backdrop-sepia' : { '-webkit-backdrop-filter' : 'sepia(100%)' , ' backdrop-filter' : 'sepia(100%)' } ,
11491168}
11501169
11511170// Drop shadow - direct raw class to CSS
@@ -1369,6 +1388,8 @@ export class CSSGenerator {
13691388 private screenBreakpoints : Map < string , string >
13701389 // Cache for utility+value combinations that don't match any rule (negative cache)
13711390 private noMatchCache : Set < string > = new Set ( )
1391+ // Track which animation keyframes are used (for @keyframes injection)
1392+ private usedKeyframes : Set < string > = new Set ( )
13721393 // Preserve extend colors for CSS variable generation (only custom colors, not defaults)
13731394 private extendColors : Record < string , string | Record < string , string > > | null = null
13741395
@@ -1472,6 +1493,13 @@ export class CSSGenerator {
14721493 const staticResult = STATIC_UTILITY_MAP [ parsed . raw ]
14731494 if ( staticResult ) {
14741495 this . addRule ( parsed , staticResult )
1496+ // Track animation keyframe usage
1497+ if ( staticResult . animation ) {
1498+ const animName = staticResult . animation . split ( ' ' ) [ 0 ]
1499+ if ( animName && animName !== 'none' ) {
1500+ this . usedKeyframes . add ( animName )
1501+ }
1502+ }
14751503 return
14761504 }
14771505
@@ -2245,16 +2273,18 @@ export class CSSGenerator {
22452273 // Check for special CSS selector characters:
22462274 // : (58), . (46), / (47), @ (64), space (32), [ (91), ] (93)
22472275 // ( (40), ) (41), % (37), # (35), , (44), > (62), + (43), ~ (126)
2276+ // ! (33), ' (39), " (34), * (42), = (61)
22482277 if ( c === 58 || c === 46 || c === 47 || c === 64 || c === 32 || c === 91 || c === 93 ||
2249- c === 40 || c === 41 || c === 37 || c === 35 || c === 44 || c === 62 || c === 43 || c === 126 ) {
2278+ c === 40 || c === 41 || c === 37 || c === 35 || c === 44 || c === 62 || c === 43 || c === 126 ||
2279+ c === 33 || c === 39 || c === 34 || c === 42 || c === 61 ) {
22502280 needsEscape = true
22512281 break
22522282 }
22532283 }
22542284 if ( ! needsEscape ) {
22552285 return className
22562286 }
2257- return className . replace ( / [: ./ @ \[ \] ( ) % # , > + ~ ] / g, '\\$&' )
2287+ return className . replace ( / [: ./ @ \[ \] ( ) % # , > + ~ ! ' " * = ] / g, '\\$&' )
22582288 }
22592289
22602290 /**
@@ -2295,6 +2325,16 @@ export class CSSGenerator {
22952325 parts . push ( this . rulesToCSS ( baseRules , minify ) )
22962326 }
22972327
2328+ // Inject @keyframes for used animations
2329+ if ( this . usedKeyframes . size > 0 ) {
2330+ for ( const name of this . usedKeyframes ) {
2331+ const kf = KEYFRAMES [ name ]
2332+ if ( kf ) {
2333+ parts . push ( minify ? kf . replace ( / \s + / g, ' ' ) . replace ( / \s * \{ \s * / g, '{' ) . replace ( / \s * \} \s * / g, '}' ) . replace ( / ; \s * / g, ';' ) . trim ( ) : kf )
2334+ }
2335+ }
2336+ }
2337+
22982338 // Media query rules
22992339 for ( const [ key , rules ] of this . rules . entries ( ) ) {
23002340 if ( key !== 'base' && rules . length > 0 ) {
0 commit comments