@@ -53,9 +53,18 @@ pub fn sort_class_list(root: &TwRoot) -> String {
5353 . collect ( ) ;
5454
5555 // `Vec::sort_by` is stable, so Unknown-vs-Unknown comparisons returning
56- // `Equal` keep input order, and Known entries with identical keys
57- // also keep input order.
58- keyed. sort_by ( |a, b| compare ( & a. 0 , & b. 0 ) ) ;
56+ // `Equal` keep input order. Known entries whose keys tie — possible
57+ // when variants share a rank (`@sm/main:flex` with `@sm:flex`) —
58+ // break the tie on full candidate text, mirroring Tailwind's final
59+ // candidate-string comparison.
60+ keyed. sort_by ( |a, b| {
61+ compare ( & a. 0 , & b. 0 ) . then_with ( || match ( & a. 0 , & b. 0 ) {
62+ ( SortKey :: Known { .. } , SortKey :: Known { .. } ) => {
63+ TwNameCollator . cmp ( a. 1 . chars ( ) , b. 1 . chars ( ) )
64+ }
65+ _ => Ordering :: Equal ,
66+ } )
67+ } ) ;
5968
6069 // Sort is in-place; total text length is unchanged. Pre-size the output
6170 // so chunked emission never re-allocates.
@@ -286,7 +295,26 @@ impl PendingSortKey {
286295 return Self :: Unknown ;
287296 } ;
288297 let name = name. text_trimmed ( ) ;
289- if let Some ( entry) = STATIC_UTILITIES
298+ if let Some ( modifier) = s. modifier ( ) {
299+ // Static registrations take no modifier; the few
300+ // valid bare-with-modifier forms (`@container/main`
301+ // names the container, `shadow/50` sets the shadow
302+ // color opacity) compile through a functional root's
303+ // bare placement, picked by modifier shape.
304+ if is_negative {
305+ None
306+ } else {
307+ FUNCTIONAL_UTILITIES . get ( name) . and_then ( |entry| {
308+ let placement = if modifier_accepted ( ModifierKind :: Opacity , & modifier)
309+ {
310+ entry. bare_opacity
311+ } else {
312+ entry. bare_name
313+ } ;
314+ placement. map ( |( sig, count) | ( pool_signature ( sig) , count) )
315+ } )
316+ }
317+ } else if let Some ( entry) = STATIC_UTILITIES
290318 . get ( name)
291319 // Tailwind registers negative statics individually
292320 // (`-m-px` exists, `-flex` does not).
@@ -1190,6 +1218,56 @@ mod tests {
11901218 assert_eq ! ( compare( & keys[ 0 ] , & keys[ 1 ] ) , Ordering :: Less ) ;
11911219 }
11921220
1221+ #[ test]
1222+ fn group_and_peer_scopes_sort_bare_first_then_by_modifier_text ( ) {
1223+ let keys = classify_all ( "group-hover:flex group-hover/a:flex group-hover/b:flex" ) ;
1224+ assert_eq ! ( compare( & keys[ 0 ] , & keys[ 1 ] ) , Ordering :: Less ) ;
1225+ assert_eq ! ( compare( & keys[ 1 ] , & keys[ 2 ] ) , Ordering :: Less ) ;
1226+ // A bracketed modifier compares by its inner value (`.5` < `menu`).
1227+ let keys = classify_all ( "group-hover/[.5]:flex group-hover/menu:flex" ) ;
1228+ assert_eq ! ( compare( & keys[ 0 ] , & keys[ 1 ] ) , Ordering :: Less ) ;
1229+ }
1230+
1231+ #[ test]
1232+ fn a_modifier_outside_group_peer_and_container_variants_is_unknown ( ) {
1233+ assert_eq ! ( classify( "hover/foo:flex" ) , SortKey :: Unknown ) ;
1234+ assert_eq ! ( classify( "has-hover/5:flex" ) , SortKey :: Unknown ) ;
1235+ assert_eq ! ( classify( "min-[600px]/5:flex" ) , SortKey :: Unknown ) ;
1236+ }
1237+
1238+ #[ test]
1239+ fn an_unresolvable_breakpoint_or_container_size_is_unknown ( ) {
1240+ assert_eq ! ( classify( "min-abc:flex" ) , SortKey :: Unknown ) ;
1241+ assert_eq ! ( classify( "@max-abc:flex" ) , SortKey :: Unknown ) ;
1242+ assert_eq ! ( classify( "min-[var(--w)]:flex" ) , SortKey :: Unknown ) ;
1243+ }
1244+
1245+ #[ test]
1246+ fn variants_resolving_equal_lengths_share_a_rank ( ) {
1247+ // `min-[40rem]` and `sm` both resolve 40rem, so their keys tie;
1248+ // `sort_class_list` breaks the tie on candidate text.
1249+ let keys = classify_all ( "min-[40rem]:flex sm:flex" ) ;
1250+ assert_eq ! ( compare( & keys[ 0 ] , & keys[ 1 ] ) , Ordering :: Equal ) ;
1251+ // A container modifier does not participate in ordering.
1252+ let keys = classify_all ( "@sm/main:flex @sm:flex" ) ;
1253+ assert_eq ! ( compare( & keys[ 0 ] , & keys[ 1 ] ) , Ordering :: Equal ) ;
1254+ }
1255+
1256+ #[ test]
1257+ fn container_sizes_compare_by_unit_text_before_magnitude ( ) {
1258+ // 400px > 384px (= 24rem), but `px` < `rem` textually, so the
1259+ // arbitrary pixel size groups before every named (rem) size.
1260+ let keys = classify_all ( "@min-[400px]:flex @sm:flex" ) ;
1261+ assert_eq ! ( compare( & keys[ 0 ] , & keys[ 1 ] ) , Ordering :: Less ) ;
1262+ }
1263+
1264+ #[ test]
1265+ fn arbitrary_selectors_compare_with_underscores_decoded ( ) {
1266+ // `_` decodes to a space, which sorts below `>`.
1267+ let keys = classify_all ( "[&_p]:flex [&>p]:flex" ) ;
1268+ assert_eq ! ( compare( & keys[ 0 ] , & keys[ 1 ] ) , Ordering :: Less ) ;
1269+ }
1270+
11931271 #[ test]
11941272 fn unparseable_arbitrary_breakpoints_keep_the_comparator_total ( ) {
11951273 // Parseable and unparseable arbitrary breakpoint values sharing one
0 commit comments