@wordpress/global-styles-engine 1.12.0 → 1.12.1-next.v.202605131032.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -138,6 +138,44 @@ export type BlockSelectors = Record<
138
138
  }
139
139
  >;
140
140
 
141
+ /**
142
+ * Style node metadata used to render one selector's style rules.
143
+ *
144
+ * - `styles`: theme.json style object for this node.
145
+ * - `selector`: CSS selector used for the node's base declarations.
146
+ * - `selectorSuffix`: optional suffix used to append additional selectors,
147
+ * such as pseudo selectors, to base and feature selectors.
148
+ * - `mediaQuery`: optional media query wrapping this node's rules.
149
+ * - `skipSelectorWrapper`: omits the `:root :where()` specificity wrapper.
150
+ * - `duotoneSelector`: alternate selector for duotone filter declarations.
151
+ * - `featureSelectors`: feature-level selectors for block supports.
152
+ * - `fallbackGapValue`: fallback block gap value used by layout rules.
153
+ * - `hasLayoutSupport`: whether layout styles can be generated for the node.
154
+ * - `isStyleVariation`: whether this node is a block style variation.
155
+ * - `layoutSelector`: optional selector override for layout styles.
156
+ * - `layoutHasBlockGapSupport`: optional block gap support override for layout styles.
157
+ * - `name`: block name used by block-specific declaration adjustments.
158
+ * - `elementName`: element name used to resolve valid pseudo selectors.
159
+ */
160
+ interface StylesNode {
161
+ styles: any;
162
+ selector: string;
163
+ selectorSuffix?: string;
164
+ mediaQuery?: string;
165
+ skipSelectorWrapper?: boolean;
166
+ duotoneSelector?: string;
167
+ featureSelectors?:
168
+ | string
169
+ | Record< string, string | Record< string, string > >;
170
+ fallbackGapValue?: string;
171
+ hasLayoutSupport?: boolean;
172
+ isStyleVariation?: boolean;
173
+ layoutSelector?: string;
174
+ layoutHasBlockGapSupport?: boolean;
175
+ name?: string;
176
+ elementName?: string;
177
+ }
178
+
141
179
  type ElementName = keyof typeof ELEMENTS;
142
180
 
143
181
  // Elements that rely on class names in their selectors.
@@ -163,6 +201,38 @@ const VALID_BLOCK_PSEUDO_SELECTORS: Record< string, string[] > = {
163
201
  'core/navigation-link': [ ':hover', ':focus', ':focus-visible', ':active' ],
164
202
  };
165
203
 
204
+ // The valid pseudo-selectors that can be used for elements.
205
+ // Keep in sync with WP_Theme_JSON_Gutenberg::VALID_ELEMENT_PSEUDO_SELECTORS.
206
+ const VALID_ELEMENT_PSEUDO_SELECTORS: Record< string, string[] > = {
207
+ link: [
208
+ ':link',
209
+ ':any-link',
210
+ ':visited',
211
+ ':hover',
212
+ ':focus',
213
+ ':focus-visible',
214
+ ':active',
215
+ ],
216
+ button: [
217
+ ':link',
218
+ ':any-link',
219
+ ':visited',
220
+ ':hover',
221
+ ':focus',
222
+ ':focus-visible',
223
+ ':active',
224
+ ],
225
+ };
226
+
227
+ /**
228
+ * Responsive breakpoint state keys and their corresponding CSS media queries.
229
+ * Keep in sync with WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS.
230
+ */
231
+ const RESPONSIVE_BREAKPOINTS: Record< string, string > = {
232
+ mobile: '@media (width <= 480px)',
233
+ tablet: '@media (480px < width <= 782px)',
234
+ };
235
+
166
236
  /**
167
237
  * Transform given preset tree into a set of preset class declarations.
168
238
  *
@@ -875,9 +945,12 @@ function pickStyleAndPseudoKeys(
875
945
  const allowedPseudoSelectors = blockName
876
946
  ? VALID_BLOCK_PSEUDO_SELECTORS[ blockName ] ?? []
877
947
  : [];
948
+
878
949
  const pickedEntries = entries.filter(
879
950
  ( [ key ] ) =>
880
- STYLE_KEYS.includes( key ) || allowedPseudoSelectors.includes( key )
951
+ STYLE_KEYS.includes( key ) ||
952
+ allowedPseudoSelectors.includes( key ) ||
953
+ RESPONSIVE_BREAKPOINTS[ key ]
881
954
  );
882
955
  // clone the style objects so that `getFeatureDeclarations` can remove consumed keys from it
883
956
  const clonedEntries = pickedEntries.map( ( [ key, style ] ) => [
@@ -887,109 +960,157 @@ function pickStyleAndPseudoKeys(
887
960
  return Object.fromEntries( clonedEntries );
888
961
  }
889
962
 
890
- function appendPseudoSelectorStyles(
891
- styles: Record< string, any >,
892
- selector: string,
893
- ruleset: string,
894
- featureSelectors:
895
- | string
896
- | Record< string, string | Record< string, string > >
897
- | undefined,
898
- treeSettings: Record< string, any > | undefined,
899
- blockName: string | undefined,
900
- styleVariationSelector?: string
901
- ): string {
902
- const pseudoSelectorStyles = Object.entries( styles ).filter( ( [ key ] ) =>
903
- key.startsWith( ':' )
904
- );
905
-
906
- if ( ! pseudoSelectorStyles.length ) {
907
- return ruleset;
963
+ /**
964
+ * Creates style nodes for configured block and element pseudo selectors.
965
+ *
966
+ * Only pseudo selectors listed in the matching block or element allow-list are
967
+ * considered. This mirrors the PHP renderer and avoids treating arbitrary
968
+ * colon-prefixed keys as pseudo selectors.
969
+ *
970
+ * @param node Style node that may contain configured pseudo styles.
971
+ * @return Style nodes for the configured pseudo states.
972
+ */
973
+ function getPseudoStyleNodes( node: StylesNode ): StylesNode[] {
974
+ const {
975
+ styles,
976
+ selector,
977
+ featureSelectors,
978
+ name,
979
+ elementName,
980
+ mediaQuery,
981
+ } = node;
982
+ const pseudoSelectors = name
983
+ ? VALID_BLOCK_PSEUDO_SELECTORS[ name ] ?? []
984
+ : VALID_ELEMENT_PSEUDO_SELECTORS[ elementName ?? '' ] ?? [];
985
+
986
+ if ( ! pseudoSelectors.length ) {
987
+ return [];
908
988
  }
909
989
 
910
- pseudoSelectorStyles.forEach( ( [ pseudoKey, pseudoStyle ] ) => {
911
- if ( ! pseudoStyle || typeof pseudoStyle !== 'object' ) {
912
- return;
990
+ return pseudoSelectors.flatMap( ( pseudoSelector ) => {
991
+ const pseudoStyles = styles?.[ pseudoSelector ];
992
+ if ( ! pseudoStyles || typeof pseudoStyles !== 'object' ) {
993
+ return [];
913
994
  }
914
995
 
915
- const remainingPseudoStyles = JSON.parse(
916
- JSON.stringify( pseudoStyle )
917
- );
918
-
919
- if ( featureSelectors && typeof featureSelectors !== 'string' ) {
920
- let pseudoFeatureDeclarations = getFeatureDeclarations(
921
- featureSelectors,
922
- remainingPseudoStyles
923
- );
924
-
925
- pseudoFeatureDeclarations = updateParagraphTextIndentSelector(
926
- pseudoFeatureDeclarations,
927
- treeSettings,
928
- blockName
929
- );
930
-
931
- pseudoFeatureDeclarations = updateButtonWidthDeclarations(
932
- pseudoFeatureDeclarations,
933
- treeSettings
934
- );
996
+ return [
997
+ {
998
+ styles: JSON.parse( JSON.stringify( pseudoStyles ) ),
999
+ selector,
1000
+ selectorSuffix: pseudoSelector,
1001
+ mediaQuery,
1002
+ featureSelectors:
1003
+ featureSelectors && typeof featureSelectors !== 'string'
1004
+ ? featureSelectors
1005
+ : undefined,
1006
+ name,
1007
+ elementName,
1008
+ },
1009
+ ];
1010
+ } );
1011
+ }
935
1012
 
936
- Object.entries( pseudoFeatureDeclarations ).forEach(
937
- ( [ baseSelector, declarations ] ) => {
938
- if ( ! declarations.length ) {
939
- return;
940
- }
941
- const pseudoFeatureSelector = appendToSelector(
942
- baseSelector,
943
- pseudoKey
944
- );
945
- const cssSelector = styleVariationSelector
946
- ? concatFeatureVariationSelectorString(
947
- pseudoFeatureSelector,
948
- styleVariationSelector
949
- )
950
- : pseudoFeatureSelector;
951
- const rules = declarations.join( ';' );
952
- ruleset += `:root :where(${ cssSelector }){${ rules };}`;
953
- }
954
- );
955
- }
1013
+ /**
1014
+ * Creates style nodes for configured responsive breakpoint states.
1015
+ *
1016
+ * Breakpoint nodes render feature-level, base, and pseudo declarations through
1017
+ * the normal node renderer.
1018
+ *
1019
+ * @param node Style node that may contain configured responsive state styles.
1020
+ * @return Responsive style nodes in configured breakpoint order.
1021
+ */
1022
+ function getResponsiveStyleNodes( node: StylesNode ): StylesNode[] {
1023
+ const {
1024
+ styles,
1025
+ selector,
1026
+ featureSelectors,
1027
+ name,
1028
+ elementName,
1029
+ isStyleVariation,
1030
+ } = node;
1031
+
1032
+ if ( ! name && ! elementName ) {
1033
+ return [];
1034
+ }
956
1035
 
957
- const pseudoDeclarations = getStylesDeclarations(
958
- remainingPseudoStyles
959
- );
1036
+ return Object.entries( RESPONSIVE_BREAKPOINTS ).flatMap(
1037
+ ( [ breakpointKey, mediaQuery ] ) => {
1038
+ const breakpointStyles = styles?.[ breakpointKey ];
1039
+ if ( ! breakpointStyles || typeof breakpointStyles !== 'object' ) {
1040
+ return [];
1041
+ }
960
1042
 
961
- if ( ! pseudoDeclarations.length ) {
962
- return;
1043
+ return [
1044
+ {
1045
+ styles: JSON.parse( JSON.stringify( breakpointStyles ) ),
1046
+ selector,
1047
+ mediaQuery,
1048
+ featureSelectors:
1049
+ featureSelectors && typeof featureSelectors !== 'string'
1050
+ ? featureSelectors
1051
+ : undefined,
1052
+ name,
1053
+ elementName,
1054
+ isStyleVariation,
1055
+ },
1056
+ ];
963
1057
  }
1058
+ );
1059
+ }
964
1060
 
965
- const pseudoSelector = appendToSelector( selector, pseudoKey );
966
- const pseudoRule = `:root :where(${ pseudoSelector }){${ pseudoDeclarations.join(
967
- ';'
968
- ) };}`;
1061
+ /**
1062
+ * Scopes feature selectors to a style variation selector.
1063
+ *
1064
+ * Variation feature selectors are compound selectors rather than suffixes. For
1065
+ * example, `.wp-image-spacing` becomes `.is-style-foo.wp-image.wp-image-spacing`.
1066
+ *
1067
+ * @param featureSelectors Feature-level selectors from a style node.
1068
+ * @param styleVariationSelector Selector for the style variation.
1069
+ * @return Feature-level selectors scoped to the style variation.
1070
+ */
1071
+ function getVariationFeatureSelectors(
1072
+ featureSelectors: StylesNode[ 'featureSelectors' ],
1073
+ styleVariationSelector: string
1074
+ ): StylesNode[ 'featureSelectors' ] {
1075
+ if ( ! featureSelectors || typeof featureSelectors === 'string' ) {
1076
+ return undefined;
1077
+ }
969
1078
 
970
- ruleset += pseudoRule;
971
- } );
1079
+ return Object.fromEntries(
1080
+ Object.entries( featureSelectors ).map( ( [ feature, selector ] ) => {
1081
+ if ( typeof selector === 'string' ) {
1082
+ return [
1083
+ feature,
1084
+ concatFeatureVariationSelectorString(
1085
+ selector,
1086
+ styleVariationSelector
1087
+ ),
1088
+ ];
1089
+ }
972
1090
 
973
- return ruleset;
1091
+ return [
1092
+ feature,
1093
+ Object.fromEntries(
1094
+ Object.entries( selector ).map(
1095
+ ( [ subfeature, subfeatureSelector ] ) => [
1096
+ subfeature,
1097
+ concatFeatureVariationSelectorString(
1098
+ subfeatureSelector,
1099
+ styleVariationSelector
1100
+ ),
1101
+ ]
1102
+ )
1103
+ ),
1104
+ ];
1105
+ } )
1106
+ );
974
1107
  }
975
1108
 
976
1109
  export const getNodesWithStyles = (
977
1110
  tree: GlobalStylesConfig,
978
1111
  blockSelectors: string | BlockSelectors
979
1112
  ): any[] => {
980
- const nodes: {
981
- styles: Partial< Omit< GlobalStylesStyles, 'elements' | 'blocks' > >;
982
- selector: string;
983
- skipSelectorWrapper?: boolean;
984
- duotoneSelector?: string;
985
- featureSelectors?:
986
- | string
987
- | Record< string, string | Record< string, string > >;
988
- fallbackGapValue?: string;
989
- hasLayoutSupport?: boolean;
990
- styleVariationSelectors?: Record< string, string >;
991
- name?: string;
992
- }[] = [];
1113
+ const nodes: StylesNode[] = [];
993
1114
 
994
1115
  if ( ! tree?.styles ) {
995
1116
  return nodes;
@@ -1012,6 +1133,7 @@ export const getNodesWithStyles = (
1012
1133
  nodes.push( {
1013
1134
  styles: tree.styles?.elements?.[ name ] ?? {},
1014
1135
  selector: selector as string,
1136
+ elementName: name,
1015
1137
  // Top level elements that don't use a class name should not receive the
1016
1138
  // `:root :where()` wrapper to maintain backwards compatibility.
1017
1139
  skipSelectorWrapper: ! (
@@ -1027,22 +1149,20 @@ export const getNodesWithStyles = (
1027
1149
  const blockStyles = pickStyleAndPseudoKeys( node, blockName );
1028
1150
  const typedNode = node as BlockNode;
1029
1151
 
1030
- // Store variation data for later processing, but don't add to nodes yet.
1031
- // Variations should be processed AFTER the main block styles to match PHP order.
1152
+ // Store variation child nodes so they can be inserted after the block's own elements.
1032
1153
  const variationNodesToAdd: typeof nodes = [];
1154
+ const variationStyleNodesToAdd: typeof nodes = [];
1033
1155
 
1034
1156
  if ( typedNode?.variations ) {
1035
- const variations: Record< string, any > = {};
1036
1157
  Object.entries( typedNode.variations ).forEach(
1037
1158
  ( [ variationName, variation ] ) => {
1038
1159
  const typedVariation = variation as BlockVariation;
1039
- variations[ variationName ] = pickStyleAndPseudoKeys(
1160
+ const variationStyles = pickStyleAndPseudoKeys(
1040
1161
  typedVariation,
1041
1162
  blockName
1042
1163
  );
1043
1164
  if ( typedVariation?.css ) {
1044
- variations[ variationName ].css =
1045
- typedVariation.css;
1165
+ variationStyles.css = typedVariation.css;
1046
1166
  }
1047
1167
  const variationSelector =
1048
1168
  typeof blockSelectors !== 'string'
@@ -1051,6 +1171,29 @@ export const getNodesWithStyles = (
1051
1171
  variationName
1052
1172
  ]
1053
1173
  : undefined;
1174
+ if (
1175
+ variationSelector &&
1176
+ typeof blockSelectors !== 'string'
1177
+ ) {
1178
+ const blockSelector = blockSelectors[ blockName ];
1179
+ variationStyleNodesToAdd.push( {
1180
+ styles: variationStyles,
1181
+ selector: variationSelector,
1182
+ featureSelectors: getVariationFeatureSelectors(
1183
+ blockSelector?.featureSelectors,
1184
+ variationSelector
1185
+ ),
1186
+ fallbackGapValue:
1187
+ blockSelector?.fallbackGapValue,
1188
+ hasLayoutSupport:
1189
+ blockSelector?.hasLayoutSupport,
1190
+ isStyleVariation: true,
1191
+ layoutSelector:
1192
+ variationSelector + blockSelector.selector,
1193
+ layoutHasBlockGapSupport: true,
1194
+ name: blockName,
1195
+ } );
1196
+ }
1054
1197
 
1055
1198
  // Process the variation's inner element styles.
1056
1199
  // This comes before the inner block styles so the
@@ -1069,6 +1212,8 @@ export const getNodesWithStyles = (
1069
1212
  variationSelector,
1070
1213
  ELEMENTS[ element as ElementName ]
1071
1214
  ),
1215
+ elementName: element,
1216
+ isStyleVariation: true,
1072
1217
  } );
1073
1218
  }
1074
1219
  } );
@@ -1127,6 +1272,8 @@ export const getNodesWithStyles = (
1127
1272
 
1128
1273
  variationNodesToAdd.push( {
1129
1274
  selector: variationBlockSelector,
1275
+ name: variationBlockName,
1276
+ isStyleVariation: true,
1130
1277
  duotoneSelector: variationDuotoneSelector,
1131
1278
  featureSelectors: variationFeatureSelectors,
1132
1279
  fallbackGapValue:
@@ -1161,6 +1308,9 @@ export const getNodesWithStyles = (
1161
1308
  variationBlockElement as ElementName
1162
1309
  ]
1163
1310
  ),
1311
+ elementName:
1312
+ variationBlockElement,
1313
+ isStyleVariation: true,
1164
1314
  } );
1165
1315
  }
1166
1316
  }
@@ -1169,7 +1319,6 @@ export const getNodesWithStyles = (
1169
1319
  );
1170
1320
  }
1171
1321
  );
1172
- blockStyles.variations = variations;
1173
1322
  }
1174
1323
 
1175
1324
  if (
@@ -1187,12 +1336,12 @@ export const getNodesWithStyles = (
1187
1336
  styles: blockStyles,
1188
1337
  featureSelectors:
1189
1338
  blockSelectors[ blockName ].featureSelectors,
1190
- styleVariationSelectors:
1191
- blockSelectors[ blockName ].styleVariationSelectors,
1192
1339
  name: blockName,
1193
1340
  } );
1194
1341
  }
1195
1342
 
1343
+ nodes.push( ...variationStyleNodesToAdd );
1344
+
1196
1345
  Object.entries( typedNode?.elements ?? {} ).forEach(
1197
1346
  ( [ elementName, value ] ) => {
1198
1347
  if (
@@ -1216,6 +1365,7 @@ export const getNodesWithStyles = (
1216
1365
  );
1217
1366
  } )
1218
1367
  .join( ',' ),
1368
+ elementName,
1219
1369
  } );
1220
1370
  }
1221
1371
  }
@@ -1437,6 +1587,154 @@ export const generateCustomProperties = (
1437
1587
  return ruleset;
1438
1588
  };
1439
1589
 
1590
+ /**
1591
+ * Renders CSS rules for a single style node.
1592
+ *
1593
+ * The node renderer handles feature-level selectors, duotone declarations,
1594
+ * layout styles, base declarations, and custom CSS. State nodes are expanded
1595
+ * before rendering so ordering matches the PHP renderer.
1596
+ *
1597
+ * @param node Style node metadata and styles.
1598
+ * @param context Render context and feature flags.
1599
+ * @param context.tree Global styles tree.
1600
+ * @param context.useRootPaddingAlign Whether root padding alignment is enabled.
1601
+ * @param context.disableLayoutStyles Whether layout styles are disabled.
1602
+ * @param context.hasBlockGapSupport Whether block gap support is enabled.
1603
+ * @param context.hasFallbackGapSupport Whether fallback gap support is enabled.
1604
+ * @param context.disableRootPadding Whether root padding declarations are disabled.
1605
+ * @return Rendered CSS rules for the node.
1606
+ */
1607
+ function renderStylesNode(
1608
+ node: StylesNode,
1609
+ {
1610
+ tree,
1611
+ useRootPaddingAlign,
1612
+ disableLayoutStyles,
1613
+ hasBlockGapSupport,
1614
+ hasFallbackGapSupport,
1615
+ disableRootPadding,
1616
+ }: {
1617
+ tree: GlobalStylesConfig;
1618
+ useRootPaddingAlign?: boolean;
1619
+ disableLayoutStyles: boolean;
1620
+ hasBlockGapSupport?: boolean;
1621
+ hasFallbackGapSupport?: boolean;
1622
+ disableRootPadding: boolean;
1623
+ }
1624
+ ): string {
1625
+ const {
1626
+ selector,
1627
+ selectorSuffix,
1628
+ mediaQuery,
1629
+ duotoneSelector,
1630
+ styles,
1631
+ fallbackGapValue,
1632
+ hasLayoutSupport,
1633
+ featureSelectors,
1634
+ layoutSelector,
1635
+ layoutHasBlockGapSupport,
1636
+ skipSelectorWrapper,
1637
+ name,
1638
+ } = node;
1639
+ let ruleset = '';
1640
+ const effectiveSelector = selectorSuffix
1641
+ ? appendToSelector( selector, selectorSuffix )
1642
+ : selector;
1643
+
1644
+ // Process styles for block support features with custom feature level
1645
+ // CSS selectors set.
1646
+ if ( featureSelectors && typeof featureSelectors !== 'string' ) {
1647
+ let featureDeclarations = getFeatureDeclarations(
1648
+ featureSelectors,
1649
+ styles
1650
+ );
1651
+
1652
+ // Update text indent selector for paragraph blocks based on the textIndent setting.
1653
+ featureDeclarations = updateParagraphTextIndentSelector(
1654
+ featureDeclarations,
1655
+ tree.settings,
1656
+ name
1657
+ );
1658
+
1659
+ // Update button width declarations for percentage values to use calc() with block gap.
1660
+ featureDeclarations = updateButtonWidthDeclarations(
1661
+ featureDeclarations,
1662
+ tree.settings
1663
+ );
1664
+
1665
+ Object.entries( featureDeclarations ).forEach(
1666
+ ( [ featureSelector, declarations ] ) => {
1667
+ if ( declarations.length ) {
1668
+ const selectorForRule = selectorSuffix
1669
+ ? appendToSelector( featureSelector, selectorSuffix )
1670
+ : featureSelector;
1671
+ const rules = declarations.join( ';' );
1672
+ ruleset += `:root :where(${ selectorForRule }){${ rules };}`;
1673
+ }
1674
+ }
1675
+ );
1676
+ }
1677
+
1678
+ // Process duotone styles.
1679
+ if ( duotoneSelector ) {
1680
+ const duotoneStyles: any = {};
1681
+ if ( styles?.filter ) {
1682
+ duotoneStyles.filter = styles.filter;
1683
+ delete styles.filter;
1684
+ }
1685
+ const duotoneDeclarations = getStylesDeclarations( duotoneStyles );
1686
+ if ( duotoneDeclarations.length ) {
1687
+ ruleset += `${ duotoneSelector }{${ duotoneDeclarations.join(
1688
+ ';'
1689
+ ) };}`;
1690
+ }
1691
+ }
1692
+
1693
+ // Process blockGap and layout styles.
1694
+ const selectorForLayout = layoutSelector ?? effectiveSelector;
1695
+ const hasBlockGapSupportForLayout =
1696
+ layoutHasBlockGapSupport ?? hasBlockGapSupport;
1697
+ if (
1698
+ ! disableLayoutStyles &&
1699
+ ( ROOT_BLOCK_SELECTOR === selectorForLayout || hasLayoutSupport )
1700
+ ) {
1701
+ ruleset += getLayoutStyles( {
1702
+ style: styles,
1703
+ selector: selectorForLayout,
1704
+ hasBlockGapSupport: hasBlockGapSupportForLayout,
1705
+ hasFallbackGapSupport,
1706
+ fallbackGapValue,
1707
+ } );
1708
+ }
1709
+
1710
+ // Process the remaining block styles (they use either normal block class or __experimentalSelector).
1711
+ const styleDeclarations = getStylesDeclarations(
1712
+ styles,
1713
+ effectiveSelector,
1714
+ useRootPaddingAlign,
1715
+ tree,
1716
+ disableRootPadding
1717
+ );
1718
+ if ( styleDeclarations?.length ) {
1719
+ const generalSelector = skipSelectorWrapper
1720
+ ? effectiveSelector
1721
+ : `:root :where(${ effectiveSelector })`;
1722
+ ruleset += `${ generalSelector }{${ styleDeclarations.join( ';' ) };}`;
1723
+ }
1724
+ if ( styles?.css ) {
1725
+ ruleset += processCSSNesting(
1726
+ styles.css,
1727
+ `:root :where(${ effectiveSelector })`
1728
+ );
1729
+ }
1730
+
1731
+ if ( mediaQuery && ruleset ) {
1732
+ return `${ mediaQuery }{${ ruleset }}`;
1733
+ }
1734
+
1735
+ return ruleset;
1736
+ }
1737
+
1440
1738
  export const transformToStyles = (
1441
1739
  tree: GlobalStylesConfig,
1442
1740
  blockSelectors: string | BlockSelectors,
@@ -1506,213 +1804,29 @@ export const transformToStyles = (
1506
1804
  }
1507
1805
 
1508
1806
  if ( options.blockStyles ) {
1509
- nodesWithStyles.forEach(
1510
- ( {
1511
- selector,
1512
- duotoneSelector,
1513
- styles,
1514
- fallbackGapValue,
1515
- hasLayoutSupport,
1516
- featureSelectors,
1517
- styleVariationSelectors,
1518
- skipSelectorWrapper,
1519
- name,
1520
- } ) => {
1521
- // Process styles for block support features with custom feature level
1522
- // CSS selectors set.
1523
- if ( featureSelectors ) {
1524
- let featureDeclarations = getFeatureDeclarations(
1525
- featureSelectors,
1526
- styles
1527
- );
1528
-
1529
- // Update text indent selector for paragraph blocks based on the textIndent setting.
1530
- featureDeclarations = updateParagraphTextIndentSelector(
1531
- featureDeclarations,
1532
- tree.settings,
1533
- name
1534
- );
1535
-
1536
- // Update button width declarations for percentage values to use calc() with block gap.
1537
- featureDeclarations = updateButtonWidthDeclarations(
1538
- featureDeclarations,
1539
- tree.settings
1540
- );
1541
-
1542
- Object.entries( featureDeclarations ).forEach(
1543
- ( [ cssSelector, declarations ] ) => {
1544
- if ( declarations.length ) {
1545
- const rules = declarations.join( ';' );
1546
- ruleset += `:root :where(${ cssSelector }){${ rules };}`;
1547
- }
1548
- }
1549
- );
1550
- }
1551
-
1552
- // Process duotone styles.
1553
- if ( duotoneSelector ) {
1554
- const duotoneStyles: any = {};
1555
- if ( styles?.filter ) {
1556
- duotoneStyles.filter = styles.filter;
1557
- delete styles.filter;
1558
- }
1559
- const duotoneDeclarations =
1560
- getStylesDeclarations( duotoneStyles );
1561
- if ( duotoneDeclarations.length ) {
1562
- ruleset += `${ duotoneSelector }{${ duotoneDeclarations.join(
1563
- ';'
1564
- ) };}`;
1565
- }
1566
- }
1567
-
1568
- // Process blockGap and layout styles.
1569
- if (
1570
- ! disableLayoutStyles &&
1571
- ( ROOT_BLOCK_SELECTOR === selector || hasLayoutSupport )
1572
- ) {
1573
- ruleset += getLayoutStyles( {
1574
- style: styles,
1575
- selector,
1576
- hasBlockGapSupport,
1577
- hasFallbackGapSupport,
1578
- fallbackGapValue,
1579
- } );
1580
- }
1807
+ nodesWithStyles.forEach( ( node ) => {
1808
+ if ( node.isStyleVariation && ! options.variationStyles ) {
1809
+ return;
1810
+ }
1581
1811
 
1582
- // Process the remaining block styles (they use either normal block class or __experimentalSelector).
1583
- const styleDeclarations = getStylesDeclarations(
1584
- styles,
1585
- selector,
1586
- useRootPaddingAlign,
1812
+ const responsiveNodes = getResponsiveStyleNodes( node );
1813
+ // Match PHP node order: base, responsive base, pseudo, responsive pseudo.
1814
+ [
1815
+ node,
1816
+ ...responsiveNodes,
1817
+ ...getPseudoStyleNodes( node ),
1818
+ ...responsiveNodes.flatMap( getPseudoStyleNodes ),
1819
+ ].forEach( ( expandedNode ) => {
1820
+ ruleset += renderStylesNode( expandedNode, {
1587
1821
  tree,
1588
- disableRootPadding
1589
- );
1590
- if ( styleDeclarations?.length ) {
1591
- const generalSelector = skipSelectorWrapper
1592
- ? selector
1593
- : `:root :where(${ selector })`;
1594
- ruleset += `${ generalSelector }{${ styleDeclarations.join(
1595
- ';'
1596
- ) };}`;
1597
- }
1598
- if ( styles?.css ) {
1599
- ruleset += processCSSNesting(
1600
- styles.css,
1601
- `:root :where(${ selector })`
1602
- );
1603
- }
1604
-
1605
- if ( options.variationStyles && styleVariationSelectors ) {
1606
- Object.entries( styleVariationSelectors ).forEach(
1607
- ( [ styleVariationName, styleVariationSelector ] ) => {
1608
- const styleVariations =
1609
- styles?.variations?.[ styleVariationName ];
1610
- if ( styleVariations ) {
1611
- // If the block uses any custom selectors for block support, add those first.
1612
- if ( featureSelectors ) {
1613
- let featureDeclarations =
1614
- getFeatureDeclarations(
1615
- featureSelectors,
1616
- styleVariations
1617
- );
1618
-
1619
- // Update text indent selector for paragraph blocks based on the textIndent setting.
1620
- featureDeclarations =
1621
- updateParagraphTextIndentSelector(
1622
- featureDeclarations,
1623
- tree.settings,
1624
- name
1625
- );
1626
-
1627
- // Update button width declarations for percentage values to use calc() with block gap.
1628
- featureDeclarations =
1629
- updateButtonWidthDeclarations(
1630
- featureDeclarations,
1631
- tree.settings
1632
- );
1633
-
1634
- Object.entries(
1635
- featureDeclarations
1636
- ).forEach(
1637
- ( [ baseSelector, declarations ]: [
1638
- string,
1639
- string[],
1640
- ] ) => {
1641
- if ( declarations.length ) {
1642
- const cssSelector =
1643
- concatFeatureVariationSelectorString(
1644
- baseSelector,
1645
- styleVariationSelector as string
1646
- );
1647
- const rules =
1648
- declarations.join( ';' );
1649
- ruleset += `:root :where(${ cssSelector }){${ rules };}`;
1650
- }
1651
- }
1652
- );
1653
- }
1654
-
1655
- // Otherwise add regular selectors.
1656
- const styleVariationDeclarations =
1657
- getStylesDeclarations(
1658
- styleVariations,
1659
- styleVariationSelector as string,
1660
- useRootPaddingAlign,
1661
- tree
1662
- );
1663
- if ( styleVariationDeclarations.length ) {
1664
- ruleset += `:root :where(${ styleVariationSelector }){${ styleVariationDeclarations.join(
1665
- ';'
1666
- ) };}`;
1667
- }
1668
- if ( styleVariations?.css ) {
1669
- ruleset += processCSSNesting(
1670
- styleVariations.css,
1671
- `:root :where(${ styleVariationSelector })`
1672
- );
1673
- }
1674
-
1675
- ruleset = appendPseudoSelectorStyles(
1676
- styleVariations,
1677
- styleVariationSelector as string,
1678
- ruleset,
1679
- featureSelectors,
1680
- tree.settings,
1681
- name,
1682
- styleVariationSelector as string
1683
- );
1684
-
1685
- // Generate layout styles for the variation if it supports layout and has blockGap defined.
1686
- if (
1687
- hasLayoutSupport &&
1688
- styleVariations?.spacing?.blockGap
1689
- ) {
1690
- // Append block selector to variation selector so layout classes are properly constructed.
1691
- const variationSelectorWithBlock =
1692
- styleVariationSelector + selector;
1693
- ruleset += getLayoutStyles( {
1694
- style: styleVariations,
1695
- selector: variationSelectorWithBlock,
1696
- hasBlockGapSupport: true,
1697
- hasFallbackGapSupport,
1698
- fallbackGapValue,
1699
- } );
1700
- }
1701
- }
1702
- }
1703
- );
1704
- }
1705
-
1706
- ruleset = appendPseudoSelectorStyles(
1707
- styles,
1708
- selector,
1709
- ruleset,
1710
- featureSelectors,
1711
- tree.settings,
1712
- name
1713
- );
1714
- }
1715
- );
1822
+ useRootPaddingAlign,
1823
+ disableLayoutStyles,
1824
+ hasBlockGapSupport,
1825
+ hasFallbackGapSupport,
1826
+ disableRootPadding,
1827
+ } );
1828
+ } );
1829
+ } );
1716
1830
  }
1717
1831
 
1718
1832
  if ( options.layoutStyles ) {