@@ -157,20 +157,27 @@ function hasConflictingAttributeSelectors(
157157 mods : string [ ] ,
158158 parsedMods ?: Map < string , ParsedAttributeSelector | null > ,
159159) : boolean {
160- const attributeMap = new Map < string , string [ ] > ( ) ;
160+ const attributeValues = new Map < string , string [ ] > ( ) ;
161+ const attributeBooleans = new Set < string > ( ) ;
161162
162163 for ( const mod of mods ) {
163164 const parsed = parsedMods ?. get ( mod ) ?? parseAttributeSelector ( mod ) ;
164- if ( parsed && parsed . value !== 'true' ) {
165- if ( ! attributeMap . has ( parsed . attribute ) ) {
166- attributeMap . set ( parsed . attribute , [ ] ) ;
165+ if ( ! parsed ) continue ;
166+
167+ if ( parsed . value === 'true' ) {
168+ // Boolean attribute
169+ attributeBooleans . add ( parsed . attribute ) ;
170+ } else {
171+ // Value attribute
172+ if ( ! attributeValues . has ( parsed . attribute ) ) {
173+ attributeValues . set ( parsed . attribute , [ ] ) ;
167174 }
168- attributeMap . get ( parsed . attribute ) ! . push ( parsed . value ) ;
175+ attributeValues . get ( parsed . attribute ) ! . push ( parsed . value ) ;
169176 }
170177 }
171178
172- // Check if any attribute has multiple values
173- for ( const values of attributeMap . values ( ) ) {
179+ // Check for multiple different values for the same attribute
180+ for ( const values of attributeValues . values ( ) ) {
174181 if ( values . length > 1 ) return true ;
175182 }
176183
@@ -270,7 +277,10 @@ function hasContradiction(
270277 if ( parsed . value === 'true' ) {
271278 // Negative boolean: !([data-theme])
272279 // Case 6: Value positive + attribute negative = CONTRADICTION
273- if ( positiveAttributes . has ( parsed . attribute ) ) {
280+ if (
281+ positiveAttributes . has ( parsed . attribute ) ||
282+ positiveBooleans . has ( parsed . attribute )
283+ ) {
274284 return true ; // INVALID: can't have value without attribute
275285 }
276286 } else {
@@ -323,6 +333,14 @@ function optimizeNotSelectors(
323333 }
324334 }
325335
336+ // If we have a positive value for this attribute, skip the negative boolean
337+ // This avoids producing selectors like [data-attr="x"]:not([data-attr])
338+ if ( parsed && parsed . value === 'true' ) {
339+ if ( maps . currentAttributes . has ( parsed . attribute ) ) {
340+ continue ;
341+ }
342+ }
343+
326344 // Case 4 subsumption: If we have a value positive and boolean positive for same attribute
327345 // The value implies the boolean, so we can skip the boolean from positive mods
328346 // (This is handled elsewhere - the value selector is more specific)
@@ -354,6 +372,103 @@ function optimizeNotSelectors(
354372 return optimizedNotMods ;
355373}
356374
375+ /**
376+ * Filter mods based on priority order for same-attribute conflicts.
377+ * If a boolean selector has higher priority than value selectors for the same attribute,
378+ * remove the value selectors (they would be shadowed by the boolean).
379+ *
380+ * Priority is determined by order in the styleStates (after reversal - earlier = higher priority)
381+ */
382+ function filterModsByPriority (
383+ allMods : string [ ] ,
384+ styleStates : Record < string , any > ,
385+ lookupStyles : string [ ] ,
386+ parsedModsCache ?: Map < string , ParsedAttributeSelector | null > ,
387+ ) : string [ ] {
388+ // Parse all mods once
389+ const parsedMods =
390+ parsedModsCache || new Map < string , ParsedAttributeSelector | null > ( ) ;
391+ if ( ! parsedModsCache ) {
392+ for ( const mod of allMods ) {
393+ if ( ! parsedMods . has ( mod ) ) {
394+ parsedMods . set ( mod , parseAttributeSelector ( getModSelector ( mod ) ) ) ;
395+ }
396+ }
397+ }
398+
399+ // Build priority map: for each mod, find its earliest appearance in any state list
400+ const modPriorities = new Map < string , number > ( ) ;
401+
402+ for ( const style of lookupStyles ) {
403+ const states = styleStates [ style ] ;
404+ if ( ! states ) continue ;
405+
406+ // states are already reversed (higher priority = lower index)
407+ states . forEach ( ( state : any , index : number ) => {
408+ if ( ! state . mods ) return ;
409+
410+ state . mods . forEach ( ( mod : string ) => {
411+ const currentPriority = modPriorities . get ( mod ) ;
412+ if ( currentPriority === undefined || index < currentPriority ) {
413+ modPriorities . set ( mod , index ) ;
414+ }
415+ } ) ;
416+ } ) ;
417+ }
418+
419+ // Group mods by attribute
420+ const attributeGroups = new Map <
421+ string ,
422+ Array < {
423+ mod : string ;
424+ isBool : boolean ;
425+ priority : number ;
426+ } >
427+ > ( ) ;
428+
429+ for ( const mod of allMods ) {
430+ const parsed = parsedMods . get ( mod ) ;
431+ if ( ! parsed ) continue ;
432+
433+ const priority = modPriorities . get ( mod ) ;
434+ if ( priority === undefined ) continue ;
435+
436+ const isBool = parsed . value === 'true' ;
437+
438+ if ( ! attributeGroups . has ( parsed . attribute ) ) {
439+ attributeGroups . set ( parsed . attribute , [ ] ) ;
440+ }
441+
442+ attributeGroups . get ( parsed . attribute ) ! . push ( {
443+ mod,
444+ isBool,
445+ priority,
446+ } ) ;
447+ }
448+
449+ // Filter: for each attribute, if boolean has higher priority than any value, remove values
450+ const modsToRemove = new Set < string > ( ) ;
451+
452+ for ( const [ attribute , group ] of attributeGroups . entries ( ) ) {
453+ const boolMods = group . filter ( ( m ) => m . isBool ) ;
454+ const valueMods = group . filter ( ( m ) => ! m . isBool ) ;
455+
456+ // Check if any boolean has higher priority (lower index) than all values
457+ for ( const boolMod of boolMods ) {
458+ const hasHigherPriorityThanAllValues = valueMods . every (
459+ ( valueMod ) => boolMod . priority < valueMod . priority ,
460+ ) ;
461+
462+ if ( hasHigherPriorityThanAllValues && valueMods . length > 0 ) {
463+ // This boolean shadows all value mods for this attribute
464+ valueMods . forEach ( ( valueMod ) => modsToRemove . add ( valueMod . mod ) ) ;
465+ }
466+ }
467+ }
468+
469+ return allMods . filter ( ( mod ) => ! modsToRemove . has ( mod ) ) ;
470+ }
471+
357472/**
358473 * Explode a style handler result into logical rules with proper mapping
359474 * Phase 1: Handler fan-out ($ selectors, arrays)
@@ -847,15 +962,22 @@ function generateLogicalRules(
847962
848963 const allModsArray = Array . from ( allMods ) ;
849964
965+ // Apply priority-based filtering for same-attribute boolean vs value conflicts
966+ const filteredMods = filterModsByPriority (
967+ allModsArray ,
968+ styleStates ,
969+ lookupStyles ,
970+ ) ;
971+
850972 // Precompute attribute maps once for all combinations
851- const attributeMaps = buildAttributeMaps ( [ ] , allModsArray ) ;
973+ const attributeMaps = buildAttributeMaps ( [ ] , filteredMods ) ;
852974
853975 // Generate combinations with conflict-aware pruning
854976 const conflictChecker = createAttributeConflictChecker (
855977 attributeMaps . parsedMods ,
856978 ) ;
857979 const combinations = getModCombinationsIterative (
858- allModsArray ,
980+ filteredMods ,
859981 true ,
860982 conflictChecker ,
861983 ) ;
@@ -878,25 +1000,31 @@ function generateLogicalRules(
8781000 // Use precomputed maps for efficient not selector optimization
8791001 const currentMaps = buildAttributeMaps (
8801002 modCombination ,
881- allModsArray ,
1003+ filteredMods ,
8821004 ) ;
883- const optimizedNotMods = optimizeNotSelectors (
884- modCombination ,
885- allModsArray ,
886- currentMaps ,
1005+ // Compute raw NOTs for contradiction check (before optimization)
1006+ const rawNotMods = filteredMods . filter (
1007+ ( mod ) => ! modCombination . includes ( mod ) ,
8871008 ) ;
8881009
8891010 // Check for contradictions between positive and negative selectors
8901011 if (
8911012 hasContradiction (
8921013 modCombination ,
893- optimizedNotMods ,
1014+ rawNotMods ,
8941015 currentMaps . parsedMods ,
8951016 )
8961017 ) {
8971018 return ; // Skip this invalid combination
8981019 }
8991020
1021+ // Optimize NOT selectors afterwards (pure simplification)
1022+ const optimizedNotMods = optimizeNotSelectors (
1023+ modCombination ,
1024+ filteredMods ,
1025+ currentMaps ,
1026+ ) ;
1027+
9001028 const modsSelectors = `${ modCombination
9011029 . map ( getModSelector )
9021030 . join ( '' ) } ${ optimizedNotMods
@@ -961,15 +1089,22 @@ function generateLogicalRules(
9611089 // Generate all possible mod combinations
9621090 const allModsArray = Array . from ( allMods ) ;
9631091
1092+ // Apply priority-based filtering for same-attribute boolean vs value conflicts
1093+ const filteredMods = filterModsByPriority (
1094+ allModsArray ,
1095+ styleStates ,
1096+ lookupStyles ,
1097+ ) ;
1098+
9641099 // Precompute attribute maps once for all combinations
965- const attributeMaps = buildAttributeMaps ( [ ] , allModsArray ) ;
1100+ const attributeMaps = buildAttributeMaps ( [ ] , filteredMods ) ;
9661101
9671102 // Generate combinations with conflict-aware pruning
9681103 const conflictChecker = createAttributeConflictChecker (
9691104 attributeMaps . parsedMods ,
9701105 ) ;
9711106 const combinations = getModCombinationsIterative (
972- allModsArray ,
1107+ filteredMods ,
9731108 true ,
9741109 conflictChecker ,
9751110 ) ;
@@ -993,25 +1128,31 @@ function generateLogicalRules(
9931128 // Use precomputed maps for efficient not selector optimization
9941129 const currentMaps = buildAttributeMaps (
9951130 modCombination ,
996- allModsArray ,
1131+ filteredMods ,
9971132 ) ;
998- const optimizedNotMods = optimizeNotSelectors (
999- modCombination ,
1000- allModsArray ,
1001- currentMaps ,
1133+ // Compute raw NOTs for contradiction check (before optimization)
1134+ const rawNotMods = filteredMods . filter (
1135+ ( mod ) => ! modCombination . includes ( mod ) ,
10021136 ) ;
10031137
10041138 // Check for contradictions between positive and negative selectors
10051139 if (
10061140 hasContradiction (
10071141 modCombination ,
1008- optimizedNotMods ,
1142+ rawNotMods ,
10091143 currentMaps . parsedMods ,
10101144 )
10111145 ) {
10121146 return ; // Skip this invalid combination
10131147 }
10141148
1149+ // Optimize NOT selectors afterwards (pure simplification)
1150+ const optimizedNotMods = optimizeNotSelectors (
1151+ modCombination ,
1152+ filteredMods ,
1153+ currentMaps ,
1154+ ) ;
1155+
10151156 const modsSelectors = `${ modCombination
10161157 . map ( getModSelector )
10171158 . join ( '' ) } ${ optimizedNotMods
0 commit comments