|
1 | 1 | import type { UtilityRule } from './rules' |
2 | 2 |
|
| 3 | +// ============================================================================= |
| 4 | +// Shadow color helpers |
| 5 | +// ============================================================================= |
| 6 | + |
| 7 | +// Flat color cache for shadow color lookups |
| 8 | +let shadowColorCache: Map<string, string> | null = null |
| 9 | +let shadowColorCacheConfig: any = null |
| 10 | + |
| 11 | +function buildShadowColorCache(colors: Record<string, any>): Map<string, string> { |
| 12 | + const cache = new Map<string, string>() |
| 13 | + for (const [name, value] of Object.entries(colors)) { |
| 14 | + if (typeof value === 'string') { |
| 15 | + cache.set(name, value) |
| 16 | + } |
| 17 | + else if (typeof value === 'object' && value !== null) { |
| 18 | + for (const [shade, shadeValue] of Object.entries(value)) { |
| 19 | + if (typeof shadeValue === 'string') { |
| 20 | + cache.set(`${name}-${shade}`, shadeValue) |
| 21 | + } |
| 22 | + } |
| 23 | + } |
| 24 | + } |
| 25 | + return cache |
| 26 | +} |
| 27 | + |
| 28 | +function applyShadowOpacity(color: string, opacity: number): string { |
| 29 | + if (color.charCodeAt(0) === 35) { // '#' |
| 30 | + const hex = color.slice(1) |
| 31 | + const r = Number.parseInt(hex.slice(0, 2), 16) |
| 32 | + const g = Number.parseInt(hex.slice(2, 4), 16) |
| 33 | + const b = Number.parseInt(hex.slice(4, 6), 16) |
| 34 | + return `rgb(${r} ${g} ${b} / ${opacity})` |
| 35 | + } |
| 36 | + return color |
| 37 | +} |
| 38 | + |
| 39 | +/** |
| 40 | + * Replace color values in a shadow string with var(--hw-shadow-color) |
| 41 | + * e.g., '0 10px 15px -3px rgb(0 0 0 / 0.1)' -> '0 10px 15px -3px var(--hw-shadow-color)' |
| 42 | + */ |
| 43 | +function createColoredShadow(shadow: string): string { |
| 44 | + return shadow.replace(/rgba?\([^)]+\)/g, 'var(--hw-shadow-color)') |
| 45 | +} |
| 46 | + |
| 47 | +// ============================================================================= |
3 | 48 | // Backgrounds, Borders, Effects utilities |
| 49 | +// ============================================================================= |
4 | 50 |
|
5 | 51 | // Background utilities |
6 | 52 | export const backgroundAttachmentRule: UtilityRule = (parsed) => { |
@@ -175,13 +221,73 @@ export const outlineRule: UtilityRule = (parsed, config) => { |
175 | 221 |
|
176 | 222 | // Effect utilities |
177 | 223 | export const boxShadowThemeRule: UtilityRule = (parsed, config) => { |
178 | | - if (parsed.utility === 'shadow' && parsed.value) { |
179 | | - const shadow = config.theme.boxShadow[parsed.value] |
180 | | - return shadow ? { 'box-shadow': shadow } : undefined |
| 224 | + if (parsed.utility === 'shadow') { |
| 225 | + const shadow = parsed.value |
| 226 | + ? config.theme.boxShadow[parsed.value] |
| 227 | + : config.theme.boxShadow.DEFAULT |
| 228 | + if (!shadow) return undefined |
| 229 | + |
| 230 | + // shadow-none is a simple reset — no CSS variables needed |
| 231 | + if (shadow === 'none') { |
| 232 | + return { |
| 233 | + '--hw-shadow': '0 0 #0000', |
| 234 | + 'box-shadow': 'var(--hw-ring-offset-shadow, 0 0 #0000), var(--hw-ring-shadow, 0 0 #0000), var(--hw-shadow)', |
| 235 | + } as Record<string, string> |
| 236 | + } |
| 237 | + |
| 238 | + // Generate CSS variable-based shadow for color support |
| 239 | + const colored = createColoredShadow(shadow) |
| 240 | + return { |
| 241 | + '--hw-shadow': shadow, |
| 242 | + '--hw-shadow-colored': colored, |
| 243 | + 'box-shadow': 'var(--hw-ring-offset-shadow, 0 0 #0000), var(--hw-ring-shadow, 0 0 #0000), var(--hw-shadow)', |
| 244 | + } as Record<string, string> |
| 245 | + } |
| 246 | +} |
| 247 | + |
| 248 | +// Shadow color utilities (shadow-{color}, shadow-{color}/{opacity}) |
| 249 | +export const shadowColorRule: UtilityRule = (parsed, config) => { |
| 250 | + if (parsed.utility !== 'shadow' || !parsed.value) |
| 251 | + return undefined |
| 252 | + |
| 253 | + // Skip if it matches a theme shadow size (sm, md, lg, xl, none, DEFAULT) |
| 254 | + if (config.theme.boxShadow[parsed.value]) |
| 255 | + return undefined |
| 256 | + |
| 257 | + // Build/update color cache if needed |
| 258 | + if (shadowColorCache === null || shadowColorCacheConfig !== config.theme.colors) { |
| 259 | + shadowColorCache = buildShadowColorCache(config.theme.colors) |
| 260 | + shadowColorCacheConfig = config.theme.colors |
181 | 261 | } |
182 | | - if (parsed.utility === 'shadow' && !parsed.value) { |
183 | | - return { 'box-shadow': config.theme.boxShadow.DEFAULT } |
| 262 | + |
| 263 | + const value = parsed.value |
| 264 | + const slashIdx = value.indexOf('/') |
| 265 | + |
| 266 | + let colorName: string |
| 267 | + let opacity: number | undefined |
| 268 | + |
| 269 | + if (slashIdx !== -1) { |
| 270 | + colorName = value.slice(0, slashIdx) |
| 271 | + const opacityValue = Number.parseInt(value.slice(slashIdx + 1), 10) |
| 272 | + if (Number.isNaN(opacityValue) || opacityValue < 0 || opacityValue > 100) |
| 273 | + return undefined |
| 274 | + opacity = opacityValue / 100 |
| 275 | + } |
| 276 | + else { |
| 277 | + colorName = value |
184 | 278 | } |
| 279 | + |
| 280 | + const resolvedColor = shadowColorCache.get(colorName) |
| 281 | + if (!resolvedColor) return undefined |
| 282 | + |
| 283 | + const finalColor = opacity !== undefined |
| 284 | + ? applyShadowOpacity(resolvedColor, opacity) |
| 285 | + : resolvedColor |
| 286 | + |
| 287 | + return { |
| 288 | + '--hw-shadow-color': finalColor, |
| 289 | + '--hw-shadow': 'var(--hw-shadow-colored)', |
| 290 | + } as Record<string, string> |
185 | 291 | } |
186 | 292 |
|
187 | 293 | export const textShadowRule: UtilityRule = (parsed) => { |
@@ -342,6 +448,7 @@ export const effectsRules: UtilityRule[] = [ |
342 | 448 | borderStyleRule, |
343 | 449 | outlineRule, |
344 | 450 | boxShadowThemeRule, |
| 451 | + shadowColorRule, |
345 | 452 | textShadowRule, |
346 | 453 | opacityRule, |
347 | 454 | mixBlendModeRule, |
|
0 commit comments