Skip to content

Commit cc6ffe4

Browse files
committed
chore: several minor improvements
1 parent c17bb5b commit cc6ffe4

14 files changed

Lines changed: 883 additions & 46 deletions

packages/crosswind/src/config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,7 @@ export const defaultConfig: CrosswindConfig = {
465465
'active': true,
466466
'disabled': true,
467467
'dark': true,
468+
'light': true,
468469
'group': true,
469470
'peer': true,
470471
'before': true,
@@ -497,7 +498,15 @@ export const defaultConfig: CrosswindConfig = {
497498
'indeterminate': true,
498499
'default': true,
499500
'optional': true,
501+
'aria': true,
502+
'data': true,
503+
'has': true,
504+
'not': true,
505+
'supports': true,
500506
'print': true,
507+
'landscape': true,
508+
'portrait': true,
509+
'forced-colors': true,
501510
'rtl': true,
502511
'ltr': true,
503512
'motion-safe': true,

packages/crosswind/src/generator.ts

Lines changed: 191 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,40 @@ const GRADIENT_MAP: Record<string, Record<string, string>> = {
856856
'bg-gradient-to-bl': { 'background-image': 'linear-gradient(to bottom left, var(--hw-gradient-stops))' },
857857
'bg-gradient-to-l': { 'background-image': 'linear-gradient(to left, var(--hw-gradient-stops))' },
858858
'bg-gradient-to-tl': { 'background-image': 'linear-gradient(to top left, var(--hw-gradient-stops))' },
859+
// Radial gradients
860+
'bg-radial': { 'background-image': 'radial-gradient(var(--hw-gradient-stops))' },
861+
'bg-radial-at-t': { 'background-image': 'radial-gradient(at top, var(--hw-gradient-stops))' },
862+
'bg-radial-at-tr': { 'background-image': 'radial-gradient(at top right, var(--hw-gradient-stops))' },
863+
'bg-radial-at-r': { 'background-image': 'radial-gradient(at right, var(--hw-gradient-stops))' },
864+
'bg-radial-at-br': { 'background-image': 'radial-gradient(at bottom right, var(--hw-gradient-stops))' },
865+
'bg-radial-at-b': { 'background-image': 'radial-gradient(at bottom, var(--hw-gradient-stops))' },
866+
'bg-radial-at-bl': { 'background-image': 'radial-gradient(at bottom left, var(--hw-gradient-stops))' },
867+
'bg-radial-at-l': { 'background-image': 'radial-gradient(at left, var(--hw-gradient-stops))' },
868+
'bg-radial-at-tl': { 'background-image': 'radial-gradient(at top left, var(--hw-gradient-stops))' },
869+
'bg-radial-at-c': { 'background-image': 'radial-gradient(at center, var(--hw-gradient-stops))' },
870+
// Conic gradients
871+
'bg-conic': { 'background-image': 'conic-gradient(var(--hw-gradient-stops))' },
872+
'bg-conic-from-t': { 'background-image': 'conic-gradient(from 0deg at center, var(--hw-gradient-stops))' },
873+
'bg-conic-from-tr': { 'background-image': 'conic-gradient(from 45deg at center, var(--hw-gradient-stops))' },
874+
'bg-conic-from-r': { 'background-image': 'conic-gradient(from 90deg at center, var(--hw-gradient-stops))' },
875+
'bg-conic-from-br': { 'background-image': 'conic-gradient(from 135deg at center, var(--hw-gradient-stops))' },
876+
'bg-conic-from-b': { 'background-image': 'conic-gradient(from 180deg at center, var(--hw-gradient-stops))' },
877+
'bg-conic-from-bl': { 'background-image': 'conic-gradient(from 225deg at center, var(--hw-gradient-stops))' },
878+
'bg-conic-from-l': { 'background-image': 'conic-gradient(from 270deg at center, var(--hw-gradient-stops))' },
879+
'bg-conic-from-tl': { 'background-image': 'conic-gradient(from 315deg at center, var(--hw-gradient-stops))' },
880+
}
881+
882+
// Content utility - direct raw class to CSS
883+
const CONTENT_MAP: Record<string, Record<string, string>> = {
884+
'content-none': { content: 'none' },
885+
'content-empty': { content: '""' },
886+
}
887+
888+
// Scrollbar utilities - direct raw class to CSS
889+
const SCROLLBAR_MAP: Record<string, Record<string, string>> = {
890+
'scrollbar-auto': { 'scrollbar-width': 'auto' },
891+
'scrollbar-thin': { 'scrollbar-width': 'thin' },
892+
'scrollbar-none': { 'scrollbar-width': 'none' },
859893
}
860894

861895
// =============================================================================
@@ -1251,6 +1285,10 @@ const STATIC_UTILITY_MAP: Record<string, Record<string, string>> = {
12511285
...DROP_SHADOW_MAP,
12521286
...MIX_BLEND_MAP,
12531287
...BG_BLEND_MAP,
1288+
// Content
1289+
...CONTENT_MAP,
1290+
// Scrollbar
1291+
...SCROLLBAR_MAP,
12541292
}
12551293

12561294
// Pre-computed variant selector map for O(1) lookup (shared across all instances)
@@ -1296,9 +1334,22 @@ const VARIANT_SELECTORS: Record<string, string> = {
12961334
'optional': ':optional',
12971335
}
12981336

1337+
// Not-* variants (negated pseudo-classes)
1338+
const NOT_VARIANT_SELECTORS: Record<string, string> = {
1339+
'not-first': ':not(:first-child)',
1340+
'not-last': ':not(:last-child)',
1341+
'not-only': ':not(:only-child)',
1342+
'not-empty': ':not(:empty)',
1343+
'not-disabled': ':not(:disabled)',
1344+
'not-checked': ':not(:checked)',
1345+
'not-first-of-type': ':not(:first-of-type)',
1346+
'not-last-of-type': ':not(:last-of-type)',
1347+
}
1348+
12991349
// Pre-computed prefix variants (these modify the selector prefix, not suffix)
13001350
const PREFIX_VARIANTS: Record<string, string> = {
13011351
'dark': '.dark ',
1352+
'light': '.light ',
13021353
'rtl': '[dir="rtl"] ',
13031354
'ltr': '[dir="ltr"] ',
13041355
}
@@ -1640,12 +1691,18 @@ export class CSSGenerator {
16401691
}
16411692

16421693
// Align content: content-{normal|center|start|end|between|around|evenly|baseline|stretch}
1694+
// Also handles CSS content property for arbitrary values: content-['hello'], content-[attr(data-label)]
16431695
if (utility === 'content' && value) {
16441696
const contentValue = ALIGN_CONTENT_VALUES[value]
16451697
if (contentValue) {
16461698
this.addRule(parsed, { 'align-content': contentValue })
16471699
return
16481700
}
1701+
// Arbitrary content property: content-['hello'], content-[attr(data-label)]
1702+
if (parsed.arbitrary) {
1703+
this.addRule(parsed, { content: value })
1704+
return
1705+
}
16491706
}
16501707

16511708
// Align self: self-{auto|start|end|center|stretch|baseline}
@@ -1938,21 +1995,100 @@ export class CSSGenerator {
19381995
continue
19391996
}
19401997

1941-
// Handle group-* variants
1998+
// Handle not-* variants: not-first, not-last, etc.
1999+
if (variant.charCodeAt(0) === 110 && variant.startsWith('not-')) { // 'n' = 110
2000+
if (this.variantEnabled.not) {
2001+
const notSelector = NOT_VARIANT_SELECTORS[variant]
2002+
if (notSelector) {
2003+
selector += notSelector
2004+
}
2005+
}
2006+
continue
2007+
}
2008+
2009+
// Handle group-* variants (with optional named group: group/name-hover)
19422010
if (variant.charCodeAt(0) === 103 && variant.startsWith('group-')) { // 'g' = 103
19432011
if (this.variantEnabled.group) {
19442012
const groupVariant = variant.slice(6)
19452013
prefix = `.group:${groupVariant} `
19462014
}
19472015
continue
19482016
}
2017+
// Named group: group/name (for nested groups)
2018+
if (variant.charCodeAt(0) === 103 && variant.startsWith('group/')) { // 'g' = 103
2019+
if (this.variantEnabled.group) {
2020+
const groupName = variant.slice(6)
2021+
prefix = `.group\\/${groupName} `
2022+
}
2023+
continue
2024+
}
19492025

1950-
// Handle peer-* variants
2026+
// Handle peer-* variants (with optional named peer)
19512027
if (variant.charCodeAt(0) === 112 && variant.startsWith('peer-')) { // 'p' = 112
19522028
if (this.variantEnabled.peer) {
19532029
const peerVariant = variant.slice(5)
19542030
prefix = `.peer:${peerVariant} ~ `
19552031
}
2032+
continue
2033+
}
2034+
// Named peer: peer/name
2035+
if (variant.charCodeAt(0) === 112 && variant.startsWith('peer/')) { // 'p' = 112
2036+
if (this.variantEnabled.peer) {
2037+
const peerName = variant.slice(5)
2038+
prefix = `.peer\\/${peerName} ~ `
2039+
}
2040+
continue
2041+
}
2042+
2043+
// Handle has-* variants: has-[input:checked], has-[:focus]
2044+
if (variant.charCodeAt(0) === 104 && variant.startsWith('has-')) { // 'h' = 104
2045+
if (this.variantEnabled.has) {
2046+
const hasValue = variant.slice(4)
2047+
// Arbitrary value: has-[selector]
2048+
if (hasValue.charCodeAt(0) === 91 && hasValue.charCodeAt(hasValue.length - 1) === 93) {
2049+
const inner = hasValue.slice(1, -1)
2050+
selector += `:has(${inner})`
2051+
}
2052+
else {
2053+
// Named pseudo: has-checked -> :has(:checked)
2054+
selector += `:has(:${hasValue})`
2055+
}
2056+
}
2057+
continue
2058+
}
2059+
2060+
// Handle aria-* variants: aria-disabled, aria-[sort=ascending]
2061+
if (variant.charCodeAt(0) === 97 && variant.startsWith('aria-')) { // 'a' = 97
2062+
if (this.variantEnabled.aria) {
2063+
const ariaValue = variant.slice(5)
2064+
// Arbitrary value: aria-[sort=ascending]
2065+
if (ariaValue.charCodeAt(0) === 91 && ariaValue.charCodeAt(ariaValue.length - 1) === 93) {
2066+
const inner = ariaValue.slice(1, -1)
2067+
selector += `[aria-${inner}]`
2068+
}
2069+
else {
2070+
// Boolean attribute: aria-disabled -> [aria-disabled="true"]
2071+
selector += `[aria-${ariaValue}="true"]`
2072+
}
2073+
}
2074+
continue
2075+
}
2076+
2077+
// Handle data-* variants: data-[state=active], data-loading
2078+
if (variant.charCodeAt(0) === 100 && variant.startsWith('data-')) { // 'd' = 100
2079+
if (this.variantEnabled.data) {
2080+
const dataValue = variant.slice(5)
2081+
// Arbitrary value: data-[state=active]
2082+
if (dataValue.charCodeAt(0) === 91 && dataValue.charCodeAt(dataValue.length - 1) === 93) {
2083+
const inner = dataValue.slice(1, -1)
2084+
selector += `[data-${inner}]`
2085+
}
2086+
else {
2087+
// Boolean attribute: data-loading -> [data-loading]
2088+
selector += `[data-${dataValue}]`
2089+
}
2090+
}
2091+
continue
19562092
}
19572093
}
19582094

@@ -1979,7 +2115,8 @@ export class CSSGenerator {
19792115
return cached || undefined // Convert empty string to undefined
19802116
}
19812117

1982-
let result: string | undefined
2118+
// Collect all media conditions — multiple media variants can stack
2119+
const mediaConditions: string[] = []
19832120

19842121
for (let i = 0; i < variantsLen; i++) {
19852122
const variant = variants[i]
@@ -1990,7 +2127,7 @@ export class CSSGenerator {
19902127
const breakpointKey = variant.slice(1)
19912128
const breakpoint = this.screenBreakpoints.get(breakpointKey)
19922129
if (breakpoint) {
1993-
result = `@container (min-width: ${breakpoint})`
2130+
const result = `@container (min-width: ${breakpoint})`
19942131
this.mediaQueryCache.set(cacheKey, result)
19952132
return result
19962133
}
@@ -2001,50 +2138,71 @@ export class CSSGenerator {
20012138
if (this.variantEnabled.responsive) {
20022139
const breakpoint = this.screenBreakpoints.get(variant)
20032140
if (breakpoint) {
2004-
result = `@media (min-width: ${breakpoint})`
2005-
this.mediaQueryCache.set(cacheKey, result)
2006-
return result
2141+
mediaConditions.push(`(min-width: ${breakpoint})`)
2142+
continue
20072143
}
20082144
}
20092145

2010-
// Media preference variants - use switch for common cases
2146+
// Media preference variants
20112147
switch (variant) {
20122148
case 'print':
2013-
if (this.variantEnabled.print) {
2014-
result = '@media print'
2015-
this.mediaQueryCache.set(cacheKey, result)
2016-
return result
2017-
}
2149+
if (this.variantEnabled.print) mediaConditions.push('print')
20182150
break
20192151
case 'motion-safe':
2020-
if (this.variantEnabled['motion-safe']) {
2021-
result = '@media (prefers-reduced-motion: no-preference)'
2022-
this.mediaQueryCache.set(cacheKey, result)
2023-
return result
2024-
}
2152+
if (this.variantEnabled['motion-safe']) mediaConditions.push('(prefers-reduced-motion: no-preference)')
20252153
break
20262154
case 'motion-reduce':
2027-
if (this.variantEnabled['motion-reduce']) {
2028-
result = '@media (prefers-reduced-motion: reduce)'
2029-
this.mediaQueryCache.set(cacheKey, result)
2030-
return result
2031-
}
2155+
if (this.variantEnabled['motion-reduce']) mediaConditions.push('(prefers-reduced-motion: reduce)')
20322156
break
20332157
case 'contrast-more':
2034-
if (this.variantEnabled['contrast-more']) {
2035-
result = '@media (prefers-contrast: more)'
2036-
this.mediaQueryCache.set(cacheKey, result)
2037-
return result
2038-
}
2158+
if (this.variantEnabled['contrast-more']) mediaConditions.push('(prefers-contrast: more)')
20392159
break
20402160
case 'contrast-less':
2041-
if (this.variantEnabled['contrast-less']) {
2042-
result = '@media (prefers-contrast: less)'
2043-
this.mediaQueryCache.set(cacheKey, result)
2044-
return result
2045-
}
2161+
if (this.variantEnabled['contrast-less']) mediaConditions.push('(prefers-contrast: less)')
2162+
break
2163+
case 'landscape':
2164+
if (this.variantEnabled.landscape) mediaConditions.push('(orientation: landscape)')
2165+
break
2166+
case 'portrait':
2167+
if (this.variantEnabled.portrait) mediaConditions.push('(orientation: portrait)')
2168+
break
2169+
case 'forced-colors':
2170+
if (this.variantEnabled['forced-colors']) mediaConditions.push('(forced-colors: active)')
20462171
break
20472172
}
2173+
2174+
// Handle supports-* variant: supports-[display:grid] -> @supports (display: grid)
2175+
if (variant.charCodeAt(0) === 115 && variant.startsWith('supports-')) { // 's' = 115
2176+
if (this.variantEnabled.supports) {
2177+
const supportsValue = variant.slice(9)
2178+
let supportsQuery: string
2179+
if (supportsValue.charCodeAt(0) === 91 && supportsValue.charCodeAt(supportsValue.length - 1) === 93) {
2180+
const inner = supportsValue.slice(1, -1).replace(/_/g, ' ')
2181+
const colonIdx = inner.indexOf(':')
2182+
if (colonIdx !== -1) {
2183+
const prop = inner.slice(0, colonIdx).trim()
2184+
const val = inner.slice(colonIdx + 1).trim()
2185+
supportsQuery = `@supports (${prop}: ${val})`
2186+
}
2187+
else {
2188+
supportsQuery = `@supports (${inner})`
2189+
}
2190+
}
2191+
else {
2192+
supportsQuery = `@supports (${supportsValue})`
2193+
}
2194+
// Supports queries don't combine with @media — return directly
2195+
this.mediaQueryCache.set(cacheKey, supportsQuery)
2196+
return supportsQuery
2197+
}
2198+
}
2199+
}
2200+
2201+
if (mediaConditions.length > 0) {
2202+
// Combine conditions: @media (min-width: 1024px) and (orientation: landscape)
2203+
const result = `@media ${mediaConditions.join(' and ')}`
2204+
this.mediaQueryCache.set(cacheKey, result)
2205+
return result
20482206
}
20492207

20502208
this.mediaQueryCache.set(cacheKey, '') // Use empty string as "no result" marker

packages/crosswind/src/parser.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1042,7 +1042,24 @@ function parseClassImpl(className: string): ParsedClass {
10421042
}
10431043
}
10441044

1045-
const parts = cleanClassName.split(':')
1045+
// Split on colons, but preserve colons inside brackets [...]
1046+
const parts: string[] = []
1047+
let current = ''
1048+
let bracketDepth = 0
1049+
for (let i = 0; i < cleanClassName.length; i++) {
1050+
const ch = cleanClassName[i]
1051+
if (ch === '[') bracketDepth++
1052+
else if (ch === ']') bracketDepth--
1053+
if (ch === ':' && bracketDepth === 0) {
1054+
parts.push(current)
1055+
current = ''
1056+
}
1057+
else {
1058+
current += ch
1059+
}
1060+
}
1061+
parts.push(current)
1062+
10461063
const utility = parts[parts.length - 1]
10471064
const variants = parts.slice(0, -1)
10481065

0 commit comments

Comments
 (0)