Skip to content

Commit 0a2924a

Browse files
committed
chore: minor improvements
1 parent 7de7dc3 commit 0a2924a

9 files changed

Lines changed: 503 additions & 399 deletions

File tree

README.md

Lines changed: 133 additions & 233 deletions
Large diffs are not rendered by default.

packages/crosswind/src/generator.ts

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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
161180
const 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)
11201139
const 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)
11421161
const 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) {

packages/crosswind/src/parser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1321,7 +1321,7 @@ function parseClassImpl(className: string): ParsedClass {
13211321
// Check for color opacity modifiers: bg-blue-500/50, text-red-500/75, bg-white/[0.04]
13221322
// Must come before fractional values to avoid conflict
13231323
const opacityMatch = utility.match(/^([a-z]+(?:-[a-z]+)*?)-(.+?)\/(\d+|\[\d*\.?\d+\])$/)
1324-
if (opacityMatch && ['bg', 'text', 'border', 'ring', 'placeholder', 'divide'].includes(opacityMatch[1])) {
1324+
if (opacityMatch && ['bg', 'text', 'border', 'ring', 'placeholder', 'divide', 'accent', 'caret', 'fill', 'stroke', 'outline', 'decoration', 'shadow', 'ring-offset'].includes(opacityMatch[1])) {
13251325
return {
13261326
raw: className,
13271327
variants,

packages/crosswind/src/rules-advanced.ts

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { UtilityRule } from './rules'
2+
import { resolveColorValue } from './rules'
23

34
// Advanced utilities
45

@@ -53,20 +54,11 @@ export const ringRule: UtilityRule = (parsed, config) => {
5354
return { '--hw-ring-inset': 'inset' } as Record<string, string>
5455
}
5556

56-
// Check if this is a ring color (e.g., ring-sky-500)
57+
// Check if this is a ring color (e.g., ring-sky-500, ring-white/50)
5758
if (parsed.value) {
58-
const parts = parsed.value.split('-')
59-
if (parts.length >= 2) {
60-
const colorName = parts.slice(0, -1).join('-')
61-
const shade = parts[parts.length - 1]
62-
const colorValue = config.theme.colors[colorName]
63-
if (typeof colorValue === 'object' && colorValue[shade]) {
64-
return { '--hw-ring-color': colorValue[shade] } as Record<string, string>
65-
}
66-
// Also check if it's a direct color value (like custom colors)
67-
if (config.theme.colors[parsed.value]) {
68-
return { '--hw-ring-color': config.theme.colors[parsed.value] } as Record<string, string>
69-
}
59+
const color = resolveColorValue(parsed.value, config)
60+
if (color) {
61+
return { '--hw-ring-color': color } as Record<string, string>
7062
}
7163
}
7264

@@ -100,20 +92,10 @@ export const ringRule: UtilityRule = (parsed, config) => {
10092
return { '--hw-ring-offset-width': widths[parsed.value] } as Record<string, string>
10193
}
10294

103-
// Otherwise, treat as a color (e.g., ring-offset-ocean-blue)
104-
const parts = parsed.value.split('-')
105-
if (parts.length >= 2) {
106-
const colorName = parts.slice(0, -1).join('-')
107-
const shade = parts[parts.length - 1]
108-
const colorValue = config.theme.colors[colorName]
109-
if (typeof colorValue === 'object' && colorValue[shade]) {
110-
return { '--hw-ring-offset-color': colorValue[shade] } as Record<string, string>
111-
}
112-
}
113-
// Check for direct color (e.g., ring-offset-black)
114-
const directColor = config.theme.colors[parsed.value]
115-
if (typeof directColor === 'string') {
116-
return { '--hw-ring-offset-color': directColor } as Record<string, string>
95+
// Otherwise, treat as a color (e.g., ring-offset-white, ring-offset-blue-500/50)
96+
const color = resolveColorValue(parsed.value, config)
97+
if (color) {
98+
return { '--hw-ring-offset-color': color } as Record<string, string>
11799
}
118100
}
119101

@@ -143,6 +125,14 @@ export const borderOpacityRule: UtilityRule = (parsed) => {
143125
// Space utilities (child spacing)
144126
export const spaceRule: UtilityRule = (parsed, config) => {
145127
if (parsed.utility === 'space-x' && parsed.value) {
128+
// space-x-reverse toggles the CSS variable
129+
if (parsed.value === 'reverse') {
130+
return {
131+
properties: { '--hw-space-x-reverse': '1' } as Record<string, string>,
132+
childSelector: '> :not([hidden]) ~ :not([hidden])',
133+
}
134+
}
135+
146136
let spacing: string
147137
if (parsed.value.startsWith('-')) {
148138
const positiveValue = parsed.value.slice(1)
@@ -164,6 +154,14 @@ export const spaceRule: UtilityRule = (parsed, config) => {
164154
}
165155

166156
if (parsed.utility === 'space-y' && parsed.value) {
157+
// space-y-reverse toggles the CSS variable
158+
if (parsed.value === 'reverse') {
159+
return {
160+
properties: { '--hw-space-y-reverse': '1' } as Record<string, string>,
161+
childSelector: '> :not([hidden]) ~ :not([hidden])',
162+
}
163+
}
164+
167165
let spacing: string
168166
if (parsed.value.startsWith('-')) {
169167
const positiveValue = parsed.value.slice(1)
@@ -224,6 +222,14 @@ export const divideRule: UtilityRule = (parsed, config) => {
224222
}
225223

226224
if (parsed.utility === 'divide-x') {
225+
// divide-x-reverse toggles the CSS variable
226+
if (parsed.value === 'reverse') {
227+
return {
228+
properties: { '--hw-divide-x-reverse': '1' } as Record<string, string>,
229+
childSelector: '> :not([hidden]) ~ :not([hidden])',
230+
}
231+
}
232+
227233
const widths: Record<string, string> = {
228234
0: '0',
229235
2: '2px',
@@ -244,6 +250,14 @@ export const divideRule: UtilityRule = (parsed, config) => {
244250
}
245251

246252
if (parsed.utility === 'divide-y') {
253+
// divide-y-reverse toggles the CSS variable
254+
if (parsed.value === 'reverse') {
255+
return {
256+
properties: { '--hw-divide-y-reverse': '1' } as Record<string, string>,
257+
childSelector: '> :not([hidden]) ~ :not([hidden])',
258+
}
259+
}
260+
247261
const widths: Record<string, string> = {
248262
0: '0',
249263
2: '2px',

packages/crosswind/src/rules-effects.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { UtilityRule } from './rules'
2+
import { resolveColorValue } from './rules'
23

34
// =============================================================================
45
// Shadow color helpers
@@ -190,20 +191,10 @@ export const outlineRule: UtilityRule = (parsed, config) => {
190191
return { 'outline-width': '1px' } as Record<string, string>
191192
}
192193

193-
// Check for colors first (e.g., outline-blue-500)
194-
const parts = parsed.value.split('-')
195-
if (parts.length === 2) {
196-
const [colorName, shade] = parts
197-
const colorValue = config.theme.colors[colorName]
198-
if (typeof colorValue === 'object' && colorValue[shade]) {
199-
return { 'outline-color': colorValue[shade] } as Record<string, string>
200-
}
201-
}
202-
203-
// Direct color (e.g., outline-black)
204-
const directColor = config.theme.colors[parsed.value]
205-
if (typeof directColor === 'string') {
206-
return { 'outline-color': directColor } as Record<string, string>
194+
// Check for colors (e.g., outline-blue-500, outline-white/50)
195+
const color = resolveColorValue(parsed.value, config)
196+
if (color) {
197+
return { 'outline-color': color } as Record<string, string>
207198
}
208199

209200
// Check for width values

0 commit comments

Comments
 (0)