Skip to content

Commit a75b2cd

Browse files
committed
chore: wip
1 parent 57b7141 commit a75b2cd

10 files changed

Lines changed: 1670 additions & 31 deletions

File tree

packages/headwind/src/build.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,8 @@ export async function build(config: HeadwindConfig): Promise<BuildResult> {
3030
generator.generate(className)
3131
}
3232

33-
// Add preflights (reset CSS)
34-
let preflightCSS = ''
35-
for (const preflight of config.preflights) {
36-
preflightCSS += preflight.getCSS() + '\n\n'
37-
}
38-
39-
const css = preflightCSS + generator.toCSS(config.minify)
33+
// Preflight CSS is now added by generator.toCSS()
34+
const css = generator.toCSS(config.minify)
4035
const duration = performance.now() - startTime
4136

4237
return {

packages/headwind/src/config.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { HeadwindConfig } from './types'
22
import { loadConfig } from 'bunfig'
3+
import { tailwindPreflight } from './preflight'
34

45
export const defaultConfig: HeadwindConfig = {
56
content: ['./src/**/*.{html,js,ts,jsx,tsx}'],
@@ -88,10 +89,31 @@ export const defaultConfig: HeadwindConfig = {
8889
active: true,
8990
disabled: true,
9091
dark: true,
92+
group: true,
93+
peer: true,
94+
before: true,
95+
after: true,
96+
first: true,
97+
last: true,
98+
odd: true,
99+
even: true,
100+
'first-of-type': true,
101+
'last-of-type': true,
102+
visited: true,
103+
checked: true,
104+
'focus-within': true,
105+
'focus-visible': true,
106+
print: true,
107+
rtl: true,
108+
ltr: true,
109+
'motion-safe': true,
110+
'motion-reduce': true,
111+
'contrast-more': true,
112+
'contrast-less': true,
91113
},
92114
safelist: [],
93115
blocklist: [],
94-
preflights: [],
116+
preflights: [tailwindPreflight],
95117
presets: [],
96118
}
97119

packages/headwind/src/generator.ts

Lines changed: 113 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,15 @@ export class CSSGenerator {
2323

2424
// Try built-in rules
2525
for (const rule of builtInRules) {
26-
const properties = rule(parsed, this.config)
27-
if (properties) {
28-
this.addRule(parsed, properties)
26+
const result = rule(parsed, this.config)
27+
if (result) {
28+
// Handle both old format (just properties) and new format (object with properties and childSelector)
29+
if ('properties' in result && typeof result.properties === 'object') {
30+
this.addRule(parsed, result.properties, result.childSelector)
31+
}
32+
else {
33+
this.addRule(parsed, result as Record<string, string>)
34+
}
2935
return
3036
}
3137
}
@@ -55,8 +61,14 @@ export class CSSGenerator {
5561
/**
5662
* Add a CSS rule with variants applied
5763
*/
58-
private addRule(parsed: ParsedClass, properties: Record<string, string>): void {
59-
const selector = this.buildSelector(parsed)
64+
private addRule(parsed: ParsedClass, properties: Record<string, string>, childSelector?: string): void {
65+
let selector = this.buildSelector(parsed)
66+
67+
// Append child selector if provided
68+
if (childSelector) {
69+
selector += ` ${childSelector}`
70+
}
71+
6072
const mediaQuery = this.getMediaQuery(parsed)
6173

6274
// Apply !important modifier
@@ -75,17 +87,20 @@ export class CSSGenerator {
7587
selector,
7688
properties,
7789
mediaQuery,
90+
childSelector,
7891
})
7992
}
8093

8194
/**
82-
* Build CSS selector with pseudo-classes
95+
* Build CSS selector with pseudo-classes and variants
8396
*/
8497
private buildSelector(parsed: ParsedClass): string {
8598
let selector = `.${this.escapeSelector(parsed.raw)}`
99+
let prefix = ''
86100

87-
// Apply pseudo-class variants (non-responsive)
101+
// Apply variants
88102
for (const variant of parsed.variants) {
103+
// Pseudo-class variants
89104
if (variant === 'hover' && this.config.variants.hover) {
90105
selector += ':hover'
91106
}
@@ -98,27 +113,101 @@ export class CSSGenerator {
98113
else if (variant === 'disabled' && this.config.variants.disabled) {
99114
selector += ':disabled'
100115
}
116+
else if (variant === 'visited' && this.config.variants.visited) {
117+
selector += ':visited'
118+
}
119+
else if (variant === 'checked' && this.config.variants.checked) {
120+
selector += ':checked'
121+
}
122+
else if (variant === 'focus-within' && this.config.variants['focus-within']) {
123+
selector += ':focus-within'
124+
}
125+
else if (variant === 'focus-visible' && this.config.variants['focus-visible']) {
126+
selector += ':focus-visible'
127+
}
128+
// Positional variants
129+
else if (variant === 'first' && this.config.variants.first) {
130+
selector += ':first-child'
131+
}
132+
else if (variant === 'last' && this.config.variants.last) {
133+
selector += ':last-child'
134+
}
135+
else if (variant === 'odd' && this.config.variants.odd) {
136+
selector += ':nth-child(odd)'
137+
}
138+
else if (variant === 'even' && this.config.variants.even) {
139+
selector += ':nth-child(even)'
140+
}
141+
else if (variant === 'first-of-type' && this.config.variants['first-of-type']) {
142+
selector += ':first-of-type'
143+
}
144+
else if (variant === 'last-of-type' && this.config.variants['last-of-type']) {
145+
selector += ':last-of-type'
146+
}
147+
// Pseudo-elements
148+
else if (variant === 'before' && this.config.variants.before) {
149+
selector += '::before'
150+
}
151+
else if (variant === 'after' && this.config.variants.after) {
152+
selector += '::after'
153+
}
154+
// Group/Peer variants
155+
else if (variant.startsWith('group-') && this.config.variants.group) {
156+
const groupVariant = variant.slice(6) // Remove 'group-'
157+
prefix = `.group:${groupVariant} `
158+
}
159+
else if (variant.startsWith('peer-') && this.config.variants.peer) {
160+
const peerVariant = variant.slice(5) // Remove 'peer-'
161+
prefix = `.peer:${peerVariant} ~ `
162+
}
163+
// Dark mode
101164
else if (variant === 'dark' && this.config.variants.dark) {
102-
// Dark mode variant prepends a selector
103-
selector = `.dark ${selector}`
165+
prefix = '.dark '
166+
}
167+
// Direction variants
168+
else if (variant === 'rtl' && this.config.variants.rtl) {
169+
prefix = '[dir="rtl"] '
170+
}
171+
else if (variant === 'ltr' && this.config.variants.ltr) {
172+
prefix = '[dir="ltr"] '
104173
}
105174
}
106175

107-
return selector
176+
return prefix + selector
108177
}
109178

110179
/**
111-
* Get media query for responsive variants
180+
* Get media query for responsive and media variants
112181
*/
113182
private getMediaQuery(parsed: ParsedClass): string | undefined {
114-
if (!this.config.variants.responsive) {
115-
return undefined
116-
}
117-
118183
for (const variant of parsed.variants) {
119-
const breakpoint = this.config.theme.screens[variant]
120-
if (breakpoint) {
121-
return `@media (min-width: ${breakpoint})`
184+
// Responsive breakpoints
185+
if (this.config.variants.responsive) {
186+
const breakpoint = this.config.theme.screens[variant]
187+
if (breakpoint) {
188+
return `@media (min-width: ${breakpoint})`
189+
}
190+
}
191+
192+
// Print media
193+
if (variant === 'print' && this.config.variants.print) {
194+
return '@media print'
195+
}
196+
197+
// Motion preferences
198+
if (variant === 'motion-safe' && this.config.variants['motion-safe']) {
199+
return '@media (prefers-reduced-motion: no-preference)'
200+
}
201+
if (variant === 'motion-reduce' && this.config.variants['motion-reduce']) {
202+
return '@media (prefers-reduced-motion: reduce)'
203+
}
204+
205+
// Contrast preferences
206+
if (variant === 'contrast-more' && this.config.variants['contrast-more']) {
207+
return '@media (prefers-contrast: more)'
208+
}
209+
if (variant === 'contrast-less' && this.config.variants['contrast-less']) {
210+
return '@media (prefers-contrast: less)'
122211
}
123212
}
124213

@@ -138,6 +227,12 @@ export class CSSGenerator {
138227
toCSS(minify = false): string {
139228
const parts: string[] = []
140229

230+
// Add preflight CSS first
231+
for (const preflight of this.config.preflights) {
232+
const preflightCSS = preflight.getCSS()
233+
parts.push(minify ? preflightCSS.replace(/\s+/g, ' ').trim() : preflightCSS)
234+
}
235+
141236
// Base rules (no media query)
142237
const baseRules = this.rules.get('base') || []
143238
if (baseRules.length > 0) {

packages/headwind/src/parser.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ export function parseClass(className: string): ParsedClass {
1616
cleanClassName = className.slice(1)
1717
}
1818

19+
// Check for arbitrary properties BEFORE splitting on colons: [color:red], [mask-type:luminance]
20+
const arbitraryPropMatch = cleanClassName.match(/^\[([a-z-]+):(.+)\]$/)
21+
if (arbitraryPropMatch) {
22+
return {
23+
raw: className,
24+
variants: [],
25+
utility: arbitraryPropMatch[1],
26+
value: arbitraryPropMatch[2],
27+
important,
28+
arbitrary: true,
29+
}
30+
}
31+
1932
const parts = cleanClassName.split(':')
2033
const utility = parts[parts.length - 1]
2134
const variants = parts.slice(0, -1)
@@ -69,8 +82,51 @@ export function parseClass(className: string): ParsedClass {
6982
'gap-y',
7083
'overflow-x',
7184
'overflow-y',
85+
'min-w',
86+
'max-w',
87+
'min-h',
88+
'max-h',
89+
'space-x',
90+
'space-y',
91+
'ring-offset',
92+
'backdrop-blur',
93+
'backdrop-brightness',
94+
'backdrop-contrast',
95+
'backdrop-grayscale',
96+
'backdrop-invert',
97+
'backdrop-saturate',
98+
'backdrop-sepia',
99+
'hue-rotate',
100+
'justify-self',
72101
]
73102

103+
// Special case for divide-x and divide-y (without values, they should be treated as compound)
104+
// divide-x -> utility: "divide-x", value: undefined
105+
// divide-x-2 -> utility: "divide-x", value: "2"
106+
if (utility === 'divide-x' || utility === 'divide-y') {
107+
return {
108+
raw: className,
109+
variants,
110+
utility,
111+
value: undefined,
112+
important,
113+
arbitrary: false,
114+
}
115+
}
116+
117+
// Check for divide-x-{width} and divide-y-{width}
118+
const divideMatch = utility.match(/^(divide-[xy])-(.+)$/)
119+
if (divideMatch) {
120+
return {
121+
raw: className,
122+
variants,
123+
utility: divideMatch[1],
124+
value: divideMatch[2],
125+
important,
126+
arbitrary: false,
127+
}
128+
}
129+
74130
for (const prefix of compoundPrefixes) {
75131
if (utility.startsWith(prefix + '-')) {
76132
return {
@@ -84,6 +140,51 @@ export function parseClass(className: string): ParsedClass {
84140
}
85141
}
86142

143+
// Check for negative values: -m-4, -top-4, -translate-x-4
144+
if (utility.startsWith('-')) {
145+
const positiveUtility = utility.slice(1)
146+
147+
// Try compound prefixes first
148+
for (const prefix of compoundPrefixes) {
149+
if (positiveUtility.startsWith(prefix + '-')) {
150+
return {
151+
raw: className,
152+
variants,
153+
utility: prefix,
154+
value: '-' + positiveUtility.slice(prefix.length + 1),
155+
important,
156+
arbitrary: false,
157+
}
158+
}
159+
}
160+
161+
// Regular negative value
162+
const match = positiveUtility.match(/^([a-z-]+?)(?:-(.+))?$/)
163+
if (match) {
164+
return {
165+
raw: className,
166+
variants,
167+
utility: match[1],
168+
value: match[2] ? '-' + match[2] : undefined,
169+
important,
170+
arbitrary: false,
171+
}
172+
}
173+
}
174+
175+
// Check for fractional values: w-1/2, h-3/4
176+
const fractionMatch = utility.match(/^([a-z-]+?)-(\d+)\/(\d+)$/)
177+
if (fractionMatch) {
178+
return {
179+
raw: className,
180+
variants,
181+
utility: fractionMatch[1],
182+
value: `${fractionMatch[2]}/${fractionMatch[3]}`,
183+
important,
184+
arbitrary: false,
185+
}
186+
}
187+
87188
// Regular parsing - split on last dash
88189
const match = utility.match(/^([a-z-]+?)(?:-(.+))?$/)
89190
if (!match) {

0 commit comments

Comments
 (0)