Skip to content

Commit cf2d716

Browse files
chore: wip
1 parent 9d4fc6f commit cf2d716

7 files changed

Lines changed: 217 additions & 18 deletions

File tree

bunpress.config.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import type { BunPressOptions } from '@stacksjs/bunpress'
22

3-
export default {
3+
const config: BunPressOptions = {
44
verbose: false,
5-
docsDir: './docs',
6-
outDir: './dist',
75

86
nav: [
97
{ text: 'Home', link: '/' },
@@ -133,9 +131,6 @@ export default {
133131
],
134132
},
135133

136-
fathom: {
137-
enabled: true,
138-
siteId: 'HEADWIND',
139-
honorDNT: true,
140-
},
141-
} satisfies BunPressOptions
134+
}
135+
136+
export default config

packages/crosswind/bin/crosswind

2.26 MB
Binary file not shown.

packages/crosswind/src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,7 @@ export const defaultConfig: CrosswindConfig = {
509509
blocklist: [],
510510
preflights: [tailwindPreflight],
511511
presets: [],
512+
cssVariables: false,
512513
}
513514

514515
// Lazy-loaded config to avoid top-level await (enables bun --compile)

packages/crosswind/src/generator.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1770,9 +1770,9 @@ export class CSSGenerator {
17701770
for (let i = 0; i < rulesLength; i++) {
17711771
const result = builtInRules[i](parsed, this.config)
17721772
if (result) {
1773-
// Handle both old format (just properties) and new format (object with properties and childSelector)
1773+
// Handle both old format (just properties) and new format (object with properties and childSelector/pseudoElement)
17741774
if ('properties' in result && typeof result.properties === 'object') {
1775-
this.addRule(parsed, result.properties, result.childSelector)
1775+
this.addRule(parsed, result.properties, result.childSelector, result.pseudoElement)
17761776
}
17771777
else {
17781778
this.addRule(parsed, result as Record<string, string>)
@@ -1788,12 +1788,16 @@ export class CSSGenerator {
17881788
/**
17891789
* Add a CSS rule with variants applied
17901790
*/
1791-
private addRule(parsed: ParsedClass, properties: Record<string, string>, childSelector?: string): void {
1791+
private addRule(parsed: ParsedClass, properties: Record<string, string>, childSelector?: string, pseudoElement?: string): void {
17921792
// Use cached selector if available
1793-
const cacheKey = `${parsed.raw}${childSelector || ''}`
1793+
const cacheKey = `${parsed.raw}${childSelector || ''}${pseudoElement || ''}`
17941794
let selector = this.selectorCache.get(cacheKey)
17951795
if (!selector) {
17961796
selector = this.buildSelector(parsed)
1797+
// Append pseudo-element directly (no space)
1798+
if (pseudoElement) {
1799+
selector += pseudoElement
1800+
}
17971801
// Append child selector if provided
17981802
if (childSelector) {
17991803
selector += ` ${childSelector}`
@@ -2020,6 +2024,14 @@ export class CSSGenerator {
20202024
}
20212025
}
20222026

2027+
// Generate CSS variables from theme colors if enabled
2028+
if (this.config.cssVariables) {
2029+
const vars = this.generateCSSVariables()
2030+
if (vars) {
2031+
parts.push(minify ? vars.replace(/\s+/g, ' ').trim() : vars)
2032+
}
2033+
}
2034+
20232035
// Base rules (no media query)
20242036
const baseRules = this.rules.get('base') || []
20252037
if (baseRules.length > 0) {
@@ -2080,6 +2092,32 @@ export class CSSGenerator {
20802092
return grouped
20812093
}
20822094

2095+
/**
2096+
* Generate :root CSS variables from theme colors
2097+
* Flattens nested color objects: { monokai: { bg: '#2d2a2e' } } -> --monokai-bg: #2d2a2e
2098+
*/
2099+
private generateCSSVariables(): string | null {
2100+
const colors = this.config.theme.colors
2101+
if (!colors || Object.keys(colors).length === 0) return null
2102+
2103+
const vars: string[] = []
2104+
for (const [name, value] of Object.entries(colors)) {
2105+
if (typeof value === 'string') {
2106+
vars.push(` --${name}: ${value};`)
2107+
}
2108+
else if (typeof value === 'object' && value !== null) {
2109+
for (const [shade, shadeValue] of Object.entries(value)) {
2110+
if (typeof shadeValue === 'string') {
2111+
vars.push(` --${name}-${shade}: ${shadeValue};`)
2112+
}
2113+
}
2114+
}
2115+
}
2116+
2117+
if (vars.length === 0) return null
2118+
return `:root {\n${vars.join('\n')}\n}`
2119+
}
2120+
20832121
/**
20842122
* Reset the generator state
20852123
*/

packages/crosswind/src/rules-effects.ts

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,52 @@
11
import type { UtilityRule } from './rules'
22

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+
// =============================================================================
348
// Backgrounds, Borders, Effects utilities
49+
// =============================================================================
450

551
// Background utilities
652
export const backgroundAttachmentRule: UtilityRule = (parsed) => {
@@ -175,13 +221,73 @@ export const outlineRule: UtilityRule = (parsed, config) => {
175221

176222
// Effect utilities
177223
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
181261
}
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
184278
}
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>
185291
}
186292

187293
export const textShadowRule: UtilityRule = (parsed) => {
@@ -342,6 +448,7 @@ export const effectsRules: UtilityRule[] = [
342448
borderStyleRule,
343449
outlineRule,
344450
boxShadowThemeRule,
451+
shadowColorRule,
345452
textShadowRule,
346453
opacityRule,
347454
mixBlendModeRule,

packages/crosswind/src/rules.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,58 @@ function applyOpacity(color: string, opacity: number): string {
454454
return cleanColor
455455
}
456456

457+
// Placeholder color utilities (placeholder-{color})
458+
export const placeholderColorRule: UtilityRule = (parsed, config) => {
459+
if (parsed.utility !== 'placeholder' || !parsed.value)
460+
return undefined
461+
462+
// Build/update flat color cache if needed
463+
if (flatColorCache === null || flatColorCacheConfig !== config.theme.colors) {
464+
flatColorCache = buildFlatColorCache(config.theme.colors)
465+
flatColorCacheConfig = config.theme.colors
466+
}
467+
468+
const value = parsed.value
469+
const slashIdx = value.indexOf('/')
470+
471+
if (slashIdx === -1) {
472+
// No opacity
473+
const colorVal = flatColorCache.get(value)
474+
if (colorVal) {
475+
return {
476+
properties: { color: colorVal },
477+
pseudoElement: '::placeholder',
478+
}
479+
}
480+
}
481+
else {
482+
// With opacity modifier
483+
const colorValue = value.slice(0, slashIdx)
484+
const opacityValue = Number.parseInt(value.slice(slashIdx + 1), 10)
485+
if (Number.isNaN(opacityValue) || opacityValue < 0 || opacityValue > 100)
486+
return undefined
487+
const opacity = opacityValue / 100
488+
const baseColor = flatColorCache.get(colorValue)
489+
if (baseColor) {
490+
return {
491+
properties: { color: applyOpacity(baseColor, opacity) },
492+
pseudoElement: '::placeholder',
493+
}
494+
}
495+
}
496+
497+
// Special colors
498+
const specialColor = SPECIAL_COLORS[parsed.value]
499+
if (specialColor) {
500+
return {
501+
properties: { color: specialColor },
502+
pseudoElement: '::placeholder',
503+
}
504+
}
505+
506+
return undefined
507+
}
508+
457509
// Typography utilities
458510
export const fontSizeRule: UtilityRule = (parsed, config) => {
459511
if (parsed.utility === 'text' && parsed.value) {
@@ -701,6 +753,9 @@ export const builtInRules: UtilityRule[] = [
701753
// Effects rules that use 'bg' utility (bg-gradient-*, bg-fixed, bg-clip-*, etc.)
702754
...effectsRules,
703755

756+
// Placeholder color rule (placeholder-{color} -> ::placeholder { color })
757+
placeholderColorRule,
758+
704759
// Color rule (bg, text, border are very common)
705760
// IMPORTANT: This must come AFTER all specific text-*, bg-*, border-* rules
706761
// because it will match ANY text-*, bg-*, border-* class

packages/crosswind/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ export interface CrosswindConfig {
7979
compileClass?: CompileClassConfig
8080
attributify?: AttributifyConfig
8181
bracketSyntax?: BracketSyntaxConfig
82+
/** Generate :root CSS variables from theme colors (e.g., --monokai-bg: #2d2a2e) */
83+
cssVariables?: boolean
8284
}
8385

8486
export interface Theme {
@@ -170,6 +172,7 @@ export interface CSSRule {
170172
export interface UtilityRuleResult {
171173
properties: Record<string, string>
172174
childSelector?: string
175+
pseudoElement?: string // e.g., '::placeholder' — appended to selector without space
173176
}
174177

175178
export type CustomRule = [RegExp, (match: RegExpMatchArray) => Record<string, string> | undefined]

0 commit comments

Comments
 (0)