diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index d0376ef0006ec..57fac0b7e9543 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -83,6 +83,9 @@ Settings related to spacing. | margin | boolean | false | | | padding | boolean | false | | | units | array | px,em,rem,vh,vw,% | | +| customSpacingSize | boolean | true | | +| spacingSizes | array | | name, size, slug | +| spacingScale | object | | | --- diff --git a/lib/block-supports/border.php b/lib/block-supports/border.php index 9f9053cf97005..34c51eca86881 100644 --- a/lib/block-supports/border.php +++ b/lib/block-supports/border.php @@ -119,7 +119,7 @@ function gutenberg_apply_border_support( $block_type, $block_attributes ) { // Collect classes and styles. $attributes = array(); - $styles = gutenberg_style_engine_generate( array( 'border' => $border_block_styles ) ); + $styles = gutenberg_style_engine_generate( array( 'border' => $border_block_styles ), array( 'css_vars' => true ) ); if ( ! empty( $styles['classnames'] ) ) { $attributes['class'] = $styles['classnames']; diff --git a/lib/block-supports/spacing.php b/lib/block-supports/spacing.php index b20c7d4406f4f..8366b9775b748 100644 --- a/lib/block-supports/spacing.php +++ b/lib/block-supports/spacing.php @@ -58,7 +58,8 @@ function gutenberg_apply_spacing_support( $block_type, $block_attributes ) { $spacing_block_styles['padding'] = $has_padding_support && ! $skip_padding ? _wp_array_get( $block_styles, array( 'spacing', 'padding' ), null ) : null; $spacing_block_styles['margin'] = $has_margin_support && ! $skip_margin ? _wp_array_get( $block_styles, array( 'spacing', 'margin' ), null ) : null; $styles = gutenberg_style_engine_generate( - array( 'spacing' => $spacing_block_styles ) + array( 'spacing' => $spacing_block_styles ), + array( 'css_vars' => true ) ); if ( ! empty( $styles['css'] ) ) { diff --git a/lib/compat/wordpress-6.0/block-editor-settings.php b/lib/compat/wordpress-6.0/block-editor-settings.php index 9030998aed250..21f1f05bb45f1 100644 --- a/lib/compat/wordpress-6.0/block-editor-settings.php +++ b/lib/compat/wordpress-6.0/block-editor-settings.php @@ -47,149 +47,3 @@ function gutenberg_is_global_styles_in_5_9( $style ) { return false; } - -/** - * Adds styles and __experimentalFeatures to the block editor settings. - * - * @param array $settings Existing block editor settings. - * - * @return array New block editor settings. - */ -function gutenberg_get_block_editor_settings( $settings ) { - // Set what is the context for this data request. - $context = 'other'; - if ( - defined( 'REST_REQUEST' ) && - REST_REQUEST && - isset( $_GET['context'] ) && - 'mobile' === $_GET['context'] - ) { - $context = 'mobile'; - } - - if ( 'other' === $context ) { - global $wp_version; - $is_wp_5_8 = version_compare( $wp_version, '5.8', '>=' ) && version_compare( $wp_version, '5.9', '<' ); - $is_wp_5_9 = version_compare( $wp_version, '5.9', '>=' ) && version_compare( $wp_version, '6.0-beta1', '<' ); - $is_wp_6_0 = version_compare( $wp_version, '6.0-beta1', '>=' ); - - // Make sure the styles array exists. - // In some contexts, like the navigation editor, it doesn't. - if ( ! isset( $settings['styles'] ) ) { - $settings['styles'] = array(); - } - - // Remove existing global styles provided by core. - $styles_without_existing_global_styles = array(); - foreach ( $settings['styles'] as $style ) { - if ( - ( $is_wp_5_8 && ! gutenberg_is_global_styles_in_5_8( $style ) ) || // Can be removed when plugin minimum version is 5.9. - ( $is_wp_5_9 && ! gutenberg_is_global_styles_in_5_9( $style ) ) || // Can be removed when plugin minimum version is 6.0. - ( $is_wp_6_0 && ( ! isset( $style['isGlobalStyles'] ) || ! $style['isGlobalStyles'] ) ) - ) { - $styles_without_existing_global_styles[] = $style; - } - } - - // Recreate global styles. - $new_global_styles = array(); - $presets = array( - array( - 'css' => 'variables', - '__unstableType' => 'presets', - 'isGlobalStyles' => true, - ), - array( - 'css' => 'presets', - '__unstableType' => 'presets', - 'isGlobalStyles' => true, - ), - ); - foreach ( $presets as $preset_style ) { - $actual_css = gutenberg_get_global_stylesheet( array( $preset_style['css'] ) ); - if ( '' !== $actual_css ) { - $preset_style['css'] = $actual_css; - $new_global_styles[] = $preset_style; - } - } - - if ( WP_Theme_JSON_Resolver::theme_has_support() ) { - $block_classes = array( - 'css' => 'styles', - '__unstableType' => 'theme', - 'isGlobalStyles' => true, - ); - $actual_css = gutenberg_get_global_stylesheet( array( $block_classes['css'] ) ); - if ( '' !== $actual_css ) { - $block_classes['css'] = $actual_css; - $new_global_styles[] = $block_classes; - } - } - - $settings['styles'] = array_merge( $new_global_styles, $styles_without_existing_global_styles ); - } - - // Copied from get_block_editor_settings() at wordpress-develop/block-editor.php. - $settings['__experimentalFeatures'] = gutenberg_get_global_settings(); - - if ( isset( $settings['__experimentalFeatures']['color']['palette'] ) ) { - $colors_by_origin = $settings['__experimentalFeatures']['color']['palette']; - $settings['colors'] = isset( $colors_by_origin['custom'] ) ? - $colors_by_origin['custom'] : ( - isset( $colors_by_origin['theme'] ) ? - $colors_by_origin['theme'] : - $colors_by_origin['default'] - ); - } - - if ( isset( $settings['__experimentalFeatures']['color']['gradients'] ) ) { - $gradients_by_origin = $settings['__experimentalFeatures']['color']['gradients']; - $settings['gradients'] = isset( $gradients_by_origin['custom'] ) ? - $gradients_by_origin['custom'] : ( - isset( $gradients_by_origin['theme'] ) ? - $gradients_by_origin['theme'] : - $gradients_by_origin['default'] - ); - } - - if ( isset( $settings['__experimentalFeatures']['typography']['fontSizes'] ) ) { - $font_sizes_by_origin = $settings['__experimentalFeatures']['typography']['fontSizes']; - $settings['fontSizes'] = isset( $font_sizes_by_origin['custom'] ) ? - $font_sizes_by_origin['custom'] : ( - isset( $font_sizes_by_origin['theme'] ) ? - $font_sizes_by_origin['theme'] : - $font_sizes_by_origin['default'] - ); - } - - if ( isset( $settings['__experimentalFeatures']['color']['custom'] ) ) { - $settings['disableCustomColors'] = ! $settings['__experimentalFeatures']['color']['custom']; - unset( $settings['__experimentalFeatures']['color']['custom'] ); - } - if ( isset( $settings['__experimentalFeatures']['color']['customGradient'] ) ) { - $settings['disableCustomGradients'] = ! $settings['__experimentalFeatures']['color']['customGradient']; - unset( $settings['__experimentalFeatures']['color']['customGradient'] ); - } - if ( isset( $settings['__experimentalFeatures']['typography']['customFontSize'] ) ) { - $settings['disableCustomFontSizes'] = ! $settings['__experimentalFeatures']['typography']['customFontSize']; - unset( $settings['__experimentalFeatures']['typography']['customFontSize'] ); - } - if ( isset( $settings['__experimentalFeatures']['typography']['lineHeight'] ) ) { - $settings['enableCustomLineHeight'] = $settings['__experimentalFeatures']['typography']['lineHeight']; - unset( $settings['__experimentalFeatures']['typography']['lineHeight'] ); - } - if ( isset( $settings['__experimentalFeatures']['spacing']['units'] ) ) { - $settings['enableCustomUnits'] = $settings['__experimentalFeatures']['spacing']['units']; - unset( $settings['__experimentalFeatures']['spacing']['units'] ); - } - if ( isset( $settings['__experimentalFeatures']['spacing']['padding'] ) ) { - $settings['enableCustomSpacing'] = $settings['__experimentalFeatures']['spacing']['padding']; - unset( $settings['__experimentalFeatures']['spacing']['padding'] ); - } - - $settings['localAutosaveInterval'] = 15; - - return $settings; -} - -add_filter( 'block_editor_settings_all', 'gutenberg_get_block_editor_settings', PHP_INT_MAX ); diff --git a/lib/compat/wordpress-6.1/block-editor-settings.php b/lib/compat/wordpress-6.1/block-editor-settings.php new file mode 100644 index 0000000000000..ed591cf60196a --- /dev/null +++ b/lib/compat/wordpress-6.1/block-editor-settings.php @@ -0,0 +1,166 @@ +=' ) && version_compare( $wp_version, '5.9', '<' ); + $is_wp_5_9 = version_compare( $wp_version, '5.9', '>=' ) && version_compare( $wp_version, '6.0-beta1', '<' ); + $is_wp_6_0 = version_compare( $wp_version, '6.0-beta1', '>=' ); + + // Make sure the styles array exists. + // In some contexts, like the navigation editor, it doesn't. + if ( ! isset( $settings['styles'] ) ) { + $settings['styles'] = array(); + } + + // Remove existing global styles provided by core. + $styles_without_existing_global_styles = array(); + foreach ( $settings['styles'] as $style ) { + if ( + ( $is_wp_5_8 && ! gutenberg_is_global_styles_in_5_8( $style ) ) || // Can be removed when plugin minimum version is 5.9. + ( $is_wp_5_9 && ! gutenberg_is_global_styles_in_5_9( $style ) ) || // Can be removed when plugin minimum version is 6.0. + ( $is_wp_6_0 && ( ! isset( $style['isGlobalStyles'] ) || ! $style['isGlobalStyles'] ) ) + ) { + $styles_without_existing_global_styles[] = $style; + } + } + + // Recreate global styles. + $new_global_styles = array(); + $presets = array( + array( + 'css' => 'variables', + '__unstableType' => 'presets', + 'isGlobalStyles' => true, + ), + array( + 'css' => 'presets', + '__unstableType' => 'presets', + 'isGlobalStyles' => true, + ), + ); + foreach ( $presets as $preset_style ) { + $actual_css = gutenberg_get_global_stylesheet( array( $preset_style['css'] ) ); + if ( '' !== $actual_css ) { + $preset_style['css'] = $actual_css; + $new_global_styles[] = $preset_style; + } + } + + if ( WP_Theme_JSON_Resolver::theme_has_support() ) { + $block_classes = array( + 'css' => 'styles', + '__unstableType' => 'theme', + 'isGlobalStyles' => true, + ); + $actual_css = gutenberg_get_global_stylesheet( array( $block_classes['css'] ) ); + if ( '' !== $actual_css ) { + $block_classes['css'] = $actual_css; + $new_global_styles[] = $block_classes; + } + } + + $settings['styles'] = array_merge( $new_global_styles, $styles_without_existing_global_styles ); + } + + // Copied from get_block_editor_settings() at wordpress-develop/block-editor.php. + $settings['__experimentalFeatures'] = gutenberg_get_global_settings(); + + if ( isset( $settings['__experimentalFeatures']['color']['palette'] ) ) { + $colors_by_origin = $settings['__experimentalFeatures']['color']['palette']; + $settings['colors'] = isset( $colors_by_origin['custom'] ) ? + $colors_by_origin['custom'] : ( + isset( $colors_by_origin['theme'] ) ? + $colors_by_origin['theme'] : + $colors_by_origin['default'] + ); + } + + if ( isset( $settings['__experimentalFeatures']['color']['gradients'] ) ) { + $gradients_by_origin = $settings['__experimentalFeatures']['color']['gradients']; + $settings['gradients'] = isset( $gradients_by_origin['custom'] ) ? + $gradients_by_origin['custom'] : ( + isset( $gradients_by_origin['theme'] ) ? + $gradients_by_origin['theme'] : + $gradients_by_origin['default'] + ); + } + + if ( isset( $settings['__experimentalFeatures']['typography']['fontSizes'] ) ) { + $font_sizes_by_origin = $settings['__experimentalFeatures']['typography']['fontSizes']; + $settings['fontSizes'] = isset( $font_sizes_by_origin['custom'] ) ? + $font_sizes_by_origin['custom'] : ( + isset( $font_sizes_by_origin['theme'] ) ? + $font_sizes_by_origin['theme'] : + $font_sizes_by_origin['default'] + ); + } + + if ( isset( $settings['__experimentalFeatures']['color']['custom'] ) ) { + $settings['disableCustomColors'] = ! $settings['__experimentalFeatures']['color']['custom']; + unset( $settings['__experimentalFeatures']['color']['custom'] ); + } + if ( isset( $settings['__experimentalFeatures']['color']['customGradient'] ) ) { + $settings['disableCustomGradients'] = ! $settings['__experimentalFeatures']['color']['customGradient']; + unset( $settings['__experimentalFeatures']['color']['customGradient'] ); + } + if ( isset( $settings['__experimentalFeatures']['typography']['customFontSize'] ) ) { + $settings['disableCustomFontSizes'] = ! $settings['__experimentalFeatures']['typography']['customFontSize']; + unset( $settings['__experimentalFeatures']['typography']['customFontSize'] ); + } + if ( isset( $settings['__experimentalFeatures']['typography']['lineHeight'] ) ) { + $settings['enableCustomLineHeight'] = $settings['__experimentalFeatures']['typography']['lineHeight']; + unset( $settings['__experimentalFeatures']['typography']['lineHeight'] ); + } + if ( isset( $settings['__experimentalFeatures']['spacing']['units'] ) ) { + $settings['enableCustomUnits'] = $settings['__experimentalFeatures']['spacing']['units']; + unset( $settings['__experimentalFeatures']['spacing']['units'] ); + } + if ( isset( $settings['__experimentalFeatures']['spacing']['padding'] ) ) { + $settings['enableCustomSpacing'] = $settings['__experimentalFeatures']['spacing']['padding']; + unset( $settings['__experimentalFeatures']['spacing']['padding'] ); + } + if ( isset( $settings['__experimentalFeatures']['spacing']['customSpacingSize'] ) ) { + $settings['disableCustomSpacingSize'] = ! $settings['__experimentalFeatures']['spacing']['customSpacingSize']; + unset( $settings['__experimentalFeatures']['spacing']['customSpacingSize'] ); + } + + if ( isset( $settings['__experimentalFeatures']['spacing']['spacingSizes'] ) ) { + $spacing_sizes_by_origin = $settings['__experimentalFeatures']['spacing']['spacingSizes']; + $settings['spacingSizes'] = isset( $spacing_sizes_by_origin['custom'] ) ? + $spacing_sizes_by_origin['custom'] : ( + isset( $spacing_sizes_by_origin['theme'] ) ? + $spacing_sizes_by_origin['theme'] : + $spacing_sizes_by_origin['default'] + ); + } + + $settings['localAutosaveInterval'] = 15; + + return $settings; +} + +add_filter( 'block_editor_settings_all', 'gutenberg_get_block_editor_settings', PHP_INT_MAX ); diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php index d093480e924e4..5abf3b5a8dd91 100644 --- a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php +++ b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php @@ -674,4 +674,276 @@ protected static function get_property_value( $styles, $path, $theme_json = null return $value; } + + /* + * Presets are a set of values that serve + * to bootstrap some styles: colors, font sizes, etc. + * + * They are a unkeyed array of values such as: + * + * ```php + * array( + * array( + * 'slug' => 'unique-name-within-the-set', + * 'name' => 'Name for the UI', + * => 'value' + * ), + * ) + * ``` + * + * This contains the necessary metadata to process them: + * + * - path => Where to find the preset within the settings section. + * - prevent_override => Disables override of default presets by theme presets. + * The relationship between whether to override the defaults + * and whether the defaults are enabled is inverse: + * - If defaults are enabled => theme presets should not be overriden + * - If defaults are disabled => theme presets should be overriden + * For example, a theme sets defaultPalette to false, + * making the default palette hidden from the user. + * In that case, we want all the theme presets to be present, + * so they should override the defaults by setting this false. + * - use_default_names => whether to use the default names + * - value_key => the key that represents the value + * - value_func => optionally, instead of value_key, a function to generate + * the value that takes a preset as an argument + * (either value_key or value_func should be present) + * - css_vars => template string to use in generating the CSS Custom Property. + * Example output: "--wp--preset--duotone--blue: " will generate as many CSS Custom Properties as presets defined + * substituting the $slug for the slug's value for each preset value. + * - classes => array containing a structure with the classes to + * generate for the presets, where for each array item + * the key is the class name and the value the property name. + * The "$slug" substring will be replaced by the slug of each preset. + * For example: + * 'classes' => array( + * '.has-$slug-color' => 'color', + * '.has-$slug-background-color' => 'background-color', + * '.has-$slug-border-color' => 'border-color', + * ) + * - properties => array of CSS properties to be used by kses to + * validate the content of each preset + * by means of the remove_insecure_properties method. + */ + const PRESETS_METADATA = array( + array( + 'path' => array( 'color', 'palette' ), + 'prevent_override' => array( 'color', 'defaultPalette' ), + 'use_default_names' => false, + 'value_key' => 'color', + 'css_vars' => '--wp--preset--color--$slug', + 'classes' => array( + '.has-$slug-color' => 'color', + '.has-$slug-background-color' => 'background-color', + '.has-$slug-border-color' => 'border-color', + ), + 'properties' => array( 'color', 'background-color', 'border-color' ), + ), + array( + 'path' => array( 'color', 'gradients' ), + 'prevent_override' => array( 'color', 'defaultGradients' ), + 'use_default_names' => false, + 'value_key' => 'gradient', + 'css_vars' => '--wp--preset--gradient--$slug', + 'classes' => array( '.has-$slug-gradient-background' => 'background' ), + 'properties' => array( 'background' ), + ), + array( + 'path' => array( 'color', 'duotone' ), + 'prevent_override' => array( 'color', 'defaultDuotone' ), + 'use_default_names' => false, + 'value_func' => 'gutenberg_get_duotone_filter_property', + 'css_vars' => '--wp--preset--duotone--$slug', + 'classes' => array(), + 'properties' => array( 'filter' ), + ), + array( + 'path' => array( 'typography', 'fontSizes' ), + 'prevent_override' => false, + 'use_default_names' => true, + 'value_key' => 'size', + 'css_vars' => '--wp--preset--font-size--$slug', + 'classes' => array( '.has-$slug-font-size' => 'font-size' ), + 'properties' => array( 'font-size' ), + ), + array( + 'path' => array( 'typography', 'fontFamilies' ), + 'prevent_override' => false, + 'use_default_names' => false, + 'value_key' => 'fontFamily', + 'css_vars' => '--wp--preset--font-family--$slug', + 'classes' => array( '.has-$slug-font-family' => 'font-family' ), + 'properties' => array( 'font-family' ), + ), + array( + 'path' => array( 'spacing', 'spacingSizes' ), + 'prevent_override' => false, + 'use_default_names' => true, + 'value_key' => 'size', + 'css_vars' => '--wp--preset--spacing--$slug', + 'classes' => array(), + 'properties' => array( 'padding', 'margin' ), + ), + array( + 'path' => array( 'spacing', 'spacingScale' ), + 'prevent_override' => false, + 'use_default_names' => true, + 'value_key' => 'size', + 'css_vars' => '--wp--preset--spacing--$slug', + 'classes' => array(), + 'properties' => array( 'padding', 'margin' ), + ), + ); + + /** + * The valid properties under the settings key. + * + * @var array + */ + const VALID_SETTINGS = array( + 'appearanceTools' => null, + 'border' => array( + 'color' => null, + 'radius' => null, + 'style' => null, + 'width' => null, + ), + 'color' => array( + 'background' => null, + 'custom' => null, + 'customDuotone' => null, + 'customGradient' => null, + 'defaultGradients' => null, + 'defaultPalette' => null, + 'duotone' => null, + 'gradients' => null, + 'link' => null, + 'palette' => null, + 'text' => null, + ), + 'custom' => null, + 'layout' => array( + 'contentSize' => null, + 'wideSize' => null, + ), + 'spacing' => array( + 'customSpacingSize' => null, + 'spacingSizes' => null, + 'spacingScale' => null, + 'blockGap' => null, + 'margin' => null, + 'padding' => null, + 'units' => null, + ), + 'typography' => array( + 'customFontSize' => null, + 'dropCap' => null, + 'fontFamilies' => null, + 'fontSizes' => null, + 'fontStyle' => null, + 'fontWeight' => null, + 'letterSpacing' => null, + 'lineHeight' => null, + 'textDecoration' => null, + 'textTransform' => null, + ), + ); + + /** + * Transform the spacing scale values into an array of spacing scale presets. + */ + public function set_spacing_sizes() { + $spacing_scale = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'spacingScale' ), array() ); + + if ( ! is_numeric( $spacing_scale['steps'] ) + || ! $spacing_scale['steps'] > 0 + || ! isset( $spacing_scale['mediumStep'] ) + || ! isset( $spacing_scale['unit'] ) + || ! isset( $spacing_scale['operator'] ) + || ! isset( $spacing_scale['increment'] ) + || ! isset( $spacing_scale['steps'] ) + || ! is_numeric( $spacing_scale['increment'] ) + || ! is_numeric( $spacing_scale['mediumStep'] ) + || ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) ) { + if ( ! empty( $spacing_scale ) ) { + trigger_error( __( 'Some of the theme.json settings.spacing.spacingScale values are invalid', 'gutenberg' ), E_USER_NOTICE ); + } + return null; + } + + $unit = sanitize_title( $spacing_scale['unit'] ); + $current_step = $spacing_scale['mediumStep']; + $steps_mid_point = round( ( ( $spacing_scale['steps'] ) / 2 ), 0 ); + $x_small_count = null; + $below_sizes = array(); + $slug = 40; + $remainder = 0; + + for ( $x = $steps_mid_point - 1; $spacing_scale['steps'] > 1 && $slug > 0 && $x > 0; $x-- ) { + $current_step = '+' === $spacing_scale['operator'] + ? $current_step - $spacing_scale['increment'] + : ( $spacing_scale['increment'] > 1 ? $current_step / $spacing_scale['increment'] : $current_step * $spacing_scale['increment'] ); + + if ( $current_step <= 0 ) { + $remainder = $x; + break; + } + + $below_sizes[] = array( + /* translators: %s: Muliple of t-shirt sizing, eg. 2X-Small */ + 'name' => $x === $steps_mid_point - 1 ? __( 'Small', 'gutenberg' ) : sprintf( __( '%sX-Small', 'gutenberg' ), strval( $x_small_count ) ), + 'slug' => $slug, + 'size' => round( $current_step, 2 ) . $unit, + ); + + if ( $x === $steps_mid_point - 2 ) { + $x_small_count = 2; + } + + if ( $x < $steps_mid_point - 2 ) { + $x_small_count++; + } + + $slug = $slug - 10; + } + + $below_sizes = array_reverse( $below_sizes ); + + $below_sizes[] = array( + 'name' => __( 'Medium', 'gutenberg' ), + 'slug' => 50, + 'size' => $spacing_scale['mediumStep'] . $unit, + ); + + $current_step = $spacing_scale['mediumStep']; + $x_large_count = null; + $above_sizes = array(); + $slug = 60; + $steps_above = ( $spacing_scale['steps'] - $steps_mid_point ) + $remainder; + + for ( $x = 0; $x < $steps_above; $x++ ) { + $current_step = '+' === $spacing_scale['operator'] + ? $current_step + $spacing_scale['increment'] + : ( $spacing_scale['increment'] >= 1 ? $current_step * $spacing_scale['increment'] : $current_step / $spacing_scale['increment'] ); + + $above_sizes[] = array( + /* translators: %s: Muliple of t-shirt sizing, eg. 2X-Large */ + 'name' => 0 === $x ? __( 'Large', 'gutenberg' ) : sprintf( __( '%sX-Large', 'gutenberg' ), strval( $x_large_count ) ), + 'slug' => $slug, + 'size' => round( $current_step, 2 ) . $unit, + ); + + if ( 1 === $x ) { + $x_large_count = 2; + } + + if ( $x > 1 ) { + $x_large_count++; + } + + $slug = $slug + 10; + } + + _wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), array_merge( $below_sizes, $above_sizes ) ); + } } diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php b/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php index 58abcc27c04ec..efb2025d6f4dd 100644 --- a/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php +++ b/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php @@ -16,6 +16,25 @@ * @access private */ class WP_Theme_JSON_Resolver_6_1 extends WP_Theme_JSON_Resolver_6_0 { + /** + * Given a theme.json structure modifies it in place + * to update certain values by its translated strings + * according to the language set by the user. + * + * @param array $theme_json The theme.json to translate. + * @param string $domain Optional. Text domain. Unique identifier for retrieving translated strings. + * Default 'default'. + * @return array Returns the modified $theme_json_structure. + */ + protected static function translate( $theme_json, $domain = 'default' ) { + if ( null === static::$i18n_schema ) { + $i18n_schema = wp_json_file_decode( __DIR__ . '/theme-i18n.json' ); + static::$i18n_schema = null === $i18n_schema ? array() : $i18n_schema; + } + + return translate_settings_using_i18n_schema( static::$i18n_schema, $theme_json, $domain ); + } + /** * Return core's origin config. * diff --git a/lib/compat/wordpress-6.1/theme-i18n.json b/lib/compat/wordpress-6.1/theme-i18n.json new file mode 100644 index 0000000000000..469783409c891 --- /dev/null +++ b/lib/compat/wordpress-6.1/theme-i18n.json @@ -0,0 +1,86 @@ +{ + "title": "Style variation name", + "settings": { + "typography": { + "fontSizes": [ + { + "name": "Font size name" + } + ], + "fontFamilies": [ + { + "name": "Font family name" + } + ] + }, + "color": { + "palette": [ + { + "name": "Color name" + } + ], + "gradients": [ + { + "name": "Gradient name" + } + ], + "duotone": [ + { + "name": "Duotone name" + } + ] + }, + "spacing": { + "spacingSizes": [ + { + "name": "Space size name" + } + ] + }, + "blocks": { + "*": { + "typography": { + "fontSizes": [ + { + "name": "Font size name" + } + ], + "fontFamilies": [ + { + "name": "Font family name" + } + ] + }, + "color": { + "palette": [ + { + "name": "Color name" + } + ], + "gradients": [ + { + "name": "Gradient name" + } + ] + }, + "spacing": { + "spacingSizes": [ + { + "name": "Space size name" + } + ] + } + } + } + }, + "customTemplates": [ + { + "title": "Custom template name" + } + ], + "templateParts": [ + { + "title": "Template part name" + } + ] +} diff --git a/lib/compat/wordpress-6.1/theme.json b/lib/compat/wordpress-6.1/theme.json index d036c158d2f8e..8aaab09312974 100644 --- a/lib/compat/wordpress-6.1/theme.json +++ b/lib/compat/wordpress-6.1/theme.json @@ -189,7 +189,15 @@ "blockGap": null, "margin": false, "padding": false, - "units": [ "px", "em", "rem", "vh", "vw", "%" ] + "customSpacingSizes": true, + "units": [ "px", "em", "rem", "vh", "vw", "%" ], + "spacingScale": { + "operator": "*", + "increment": 1.5, + "steps": 10, + "mediumStep": 1.5, + "unit": "rem" + } }, "typography": { "customFontSize": true, diff --git a/lib/experimental/class-wp-rest-block-editor-settings-controller.php b/lib/experimental/class-wp-rest-block-editor-settings-controller.php index 0d1aa47fb2b02..d9b726f1158da 100644 --- a/lib/experimental/class-wp-rest-block-editor-settings-controller.php +++ b/lib/experimental/class-wp-rest-block-editor-settings-controller.php @@ -275,6 +275,16 @@ public function get_item_schema() { 'type' => 'array', 'context' => array( 'post-editor', 'site-editor', 'widgets-editor' ), ), + 'spacingSizes' => array( + 'description' => __( 'Active theme spacing sizes.', 'gutenberg' ), + 'type' => 'array', + 'context' => array( 'post-editor', 'site-editor', 'widgets-editor' ), + ), + 'spacingScale' => array( + 'description' => __( 'Active theme spacing scale.', 'gutenberg' ), + 'type' => 'array', + 'context' => array( 'post-editor', 'site-editor', 'widgets-editor' ), + ), ), ); diff --git a/lib/experimental/class-wp-theme-json-resolver-gutenberg.php b/lib/experimental/class-wp-theme-json-resolver-gutenberg.php index 7f7d4a3ace2f1..2007893afea7f 100644 --- a/lib/experimental/class-wp-theme-json-resolver-gutenberg.php +++ b/lib/experimental/class-wp-theme-json-resolver-gutenberg.php @@ -173,6 +173,8 @@ public static function get_merged_data( $origin = 'custom' ) { if ( 'custom' === $origin ) { $result->merge( static::get_user_data() ); } + // Generate the default spacing sizes presets. + $result->set_spacing_sizes(); return $result; } diff --git a/lib/load.php b/lib/load.php index 5504e8bd3fc3d..d16096e03dabc 100644 --- a/lib/load.php +++ b/lib/load.php @@ -122,6 +122,7 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.1 compat. require __DIR__ . '/compat/wordpress-6.1/blocks.php'; +require __DIR__ . '/compat/wordpress-6.1/block-editor-settings.php'; require __DIR__ . '/compat/wordpress-6.1/persisted-preferences.php'; require __DIR__ . '/compat/wordpress-6.1/get-global-styles-and-settings.php'; require __DIR__ . '/compat/wordpress-6.1/class-wp-theme-json-6-1.php'; diff --git a/packages/style-engine/class-wp-style-engine.php b/packages/style-engine/class-wp-style-engine.php index c4cdd0c19419c..85cbdbf2e0513 100644 --- a/packages/style-engine/class-wp-style-engine.php +++ b/packages/style-engine/class-wp-style-engine.php @@ -51,7 +51,7 @@ class WP_Style_Engine { ), 'path' => array( 'color', 'text' ), 'css_vars' => array( - '--wp--preset--color--$slug' => 'color', + 'color' => '--wp--preset--color--$slug', ), 'classnames' => array( 'has-text-color' => true, @@ -148,6 +148,9 @@ class WP_Style_Engine { 'individual' => 'padding-%s', ), 'path' => array( 'spacing', 'padding' ), + 'css_vars' => array( + 'spacing' => '--wp--preset--spacing--$slug', + ), ), 'margin' => array( 'property_keys' => array( @@ -155,6 +158,9 @@ class WP_Style_Engine { 'individual' => 'margin-%s', ), 'path' => array( 'spacing', 'margin' ), + 'css_vars' => array( + 'spacing' => '--wp--preset--spacing--$slug', + ), ), ), 'typography' => array( @@ -246,6 +252,28 @@ protected static function get_slug_from_preset_value( $style_value, $property_ke return null; } + /** + * Generates a css var string, eg var(--wp--preset--color--background) from a preset string, eg. `var:preset|space|50`. + * + * @param string $style_value A single css preset value. + * @param array $css_vars The css var patterns used to generate the var string. + * + * @return string|null The css var, or null if no match for slug found. + */ + protected static function get_css_var_value( $style_value, $css_vars ) { + foreach ( $css_vars as $property_key => $css_var_pattern ) { + $slug = static::get_slug_from_preset_value( $style_value, $property_key ); + if ( $slug ) { + $var = strtr( + $css_var_pattern, + array( '$slug' => $slug ) + ); + return "var($var)"; + } + } + return null; + } + /** * Checks whether an incoming block style value is valid. * @@ -315,7 +343,7 @@ protected static function get_css_declarations( $style_value, $style_definition, isset( $style_definition['value_func'] ) && is_callable( $style_definition['value_func'] ) ) { - return call_user_func( $style_definition['value_func'], $style_value, $style_definition ); + return call_user_func( $style_definition['value_func'], $style_value, $style_definition, $should_return_css_vars ); } $css_declarations = array(); @@ -325,15 +353,9 @@ protected static function get_css_declarations( $style_value, $style_definition, // Check if the value is a CSS preset and there's a corresponding css_var pattern in the style definition. if ( is_string( $style_value ) && strpos( $style_value, 'var:' ) !== false ) { if ( $should_return_css_vars && ! empty( $style_definition['css_vars'] ) ) { - foreach ( $style_definition['css_vars'] as $css_var_pattern => $property_key ) { - $slug = static::get_slug_from_preset_value( $style_value, $property_key ); - if ( $slug ) { - $css_var = strtr( - $css_var_pattern, - array( '$slug' => $slug ) - ); - $css_declarations[ $style_property_keys['default'] ] = "var($css_var)"; - } + $css_var = static::get_css_var_value( $style_value, $style_definition['css_vars'] ); + if ( $css_var ) { + $css_declarations[ $style_property_keys['default'] ] = $css_var; } } return $css_declarations; @@ -344,8 +366,13 @@ protected static function get_css_declarations( $style_value, $style_definition, // for styles such as margins and padding. if ( is_array( $style_value ) ) { foreach ( $style_value as $key => $value ) { - $individual_property = sprintf( $style_property_keys['individual'], _wp_to_kebab_case( $key ) ); - $css_declarations[ $individual_property ] = $value; + if ( is_string( $value ) && strpos( $value, 'var:' ) !== false && $should_return_css_vars && ! empty( $style_definition['css_vars'] ) ) { + $value = static::get_css_var_value( $value, $style_definition['css_vars'] ); + } + $individual_property = sprintf( $style_property_keys['individual'], _wp_to_kebab_case( $key ) ); + if ( $value ) { + $css_declarations[ $individual_property ] = $value; + } } } else { $css_declarations[ $style_property_keys['default'] ] = $style_value; @@ -441,12 +468,13 @@ public function generate( $block_styles, $options ) { * "border-{top|right|bottom|left}-{color|width|style}: {value};" or, * "border-image-{outset|source|width|repeat|slice}: {value};" * - * @param array $style_value A single raw Gutenberg style attributes value for a CSS property. - * @param array $individual_property_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. + * @param array $style_value A single raw Gutenberg style attributes value for a CSS property. + * @param array $individual_property_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. + * @param boolean $should_return_css_vars Whether to try to build and return CSS var values. * * @return array An array of CSS definitions, e.g., array( "$property" => "$value" ). */ - protected static function get_individual_property_css_declarations( $style_value, $individual_property_definition ) { + protected static function get_individual_property_css_declarations( $style_value, $individual_property_definition, $should_return_css_vars ) { $css_declarations = array(); if ( ! is_array( $style_value ) || empty( $style_value ) || empty( $individual_property_definition['path'] ) ) { @@ -470,13 +498,8 @@ protected static function get_individual_property_css_declarations( $style_value if ( $style_definition && isset( $style_definition['property_keys']['individual'] ) ) { // Set a CSS var if there is a valid preset value. - $slug = isset( $individual_property_definition['css_vars'][ $css_property ] ) ? static::get_slug_from_preset_value( $value, $css_property ) : null; - if ( $slug ) { - $css_var = strtr( - $individual_property_definition['css_vars'][ $css_property ], - array( '$slug' => $slug ) - ); - $value = "var($css_var)"; + if ( is_string( $value ) && strpos( $value, 'var:' ) !== false && $should_return_css_vars && ! empty( $individual_property_definition['css_vars'] ) ) { + $value = static::get_css_var_value( $value, $individual_property_definition['css_vars'] ); } $individual_css_property = sprintf( $style_definition['property_keys']['individual'], $individual_property_key ); $css_declarations[ $individual_css_property ] = $value; diff --git a/packages/style-engine/phpunit/class-wp-style-engine-test.php b/packages/style-engine/phpunit/class-wp-style-engine-test.php index e8274e85425fa..9ff42a75f7b44 100644 --- a/packages/style-engine/phpunit/class-wp-style-engine-test.php +++ b/packages/style-engine/phpunit/class-wp-style-engine-test.php @@ -246,6 +246,59 @@ public function data_generate_styles_fixtures() { ), ), + 'valid_spacing_single_preset_values' => array( + 'block_styles' => array( + 'spacing' => array( + 'margin' => 'var:preset|spacing|10', + 'padding' => 'var:preset|spacing|20', + ), + ), + 'options' => array( 'css_vars' => true ), + 'expected_output' => array( + 'css' => 'padding: var(--wp--preset--spacing--20); margin: var(--wp--preset--spacing--10);', + ), + ), + + 'valid_spacing_multi_preset_values' => array( + 'block_styles' => array( + 'spacing' => array( + 'margin' => array( + 'left' => 'var:preset|spacing|10', + 'right' => 'var:preset|spacing|20', + 'top' => '1rem', + 'bottom' => '1rem', + ), + 'padding' => array( + 'left' => 'var:preset|spacing|30', + 'right' => 'var:preset|spacing|40', + 'top' => '14px', + 'bottom' => '14px', + ), + ), + ), + 'options' => array( 'css_vars' => true ), + 'expected_output' => array( + 'css' => 'padding-left: var(--wp--preset--spacing--30); padding-right: var(--wp--preset--spacing--40); padding-top: 14px; padding-bottom: 14px; margin-left: var(--wp--preset--spacing--10); margin-right: var(--wp--preset--spacing--20); margin-top: 1rem; margin-bottom: 1rem;', + ), + ), + + 'invalid_spacing_multi_preset_values' => array( + 'block_styles' => array( + 'spacing' => array( + 'margin' => array( + 'left' => 'var:preset|spaceman|10', + 'right' => 'var:preset|spaceman|20', + 'top' => '1rem', + 'bottom' => '1rem', + ), + ), + ), + 'options' => array( 'css_vars' => true ), + 'expected_output' => array( + 'css' => 'margin-top: 1rem; margin-bottom: 1rem;', + ), + ), + 'invalid_classnames_options' => array( 'block_styles' => array( 'typography' => array( @@ -285,7 +338,7 @@ public function data_generate_styles_fixtures() { ), ), ), - 'options' => array(), + 'options' => array( 'css_vars' => true ), 'expected_output' => array( 'css' => 'border-top-color: #fe1; border-top-width: 1.5rem; border-top-style: dashed; border-right-color: #fe2; border-right-width: 1.4rem; border-right-style: solid; border-bottom-color: #fe3; border-bottom-width: 1.3rem; border-left-color: var(--wp--preset--color--swampy-yellow); border-left-width: 0.5rem; border-left-style: dotted;', ), @@ -315,7 +368,7 @@ public function data_generate_styles_fixtures() { ), ), ), - 'options' => array(), + 'options' => array( 'css_vars' => true ), 'expected_output' => array( 'css' => 'border-bottom-color: var(--wp--preset--color--terrible-lizard);', ), diff --git a/packages/style-engine/src/styles/utils.ts b/packages/style-engine/src/styles/utils.ts index a905e1929fdb3..28609794f23ad 100644 --- a/packages/style-engine/src/styles/utils.ts +++ b/packages/style-engine/src/styles/utils.ts @@ -81,7 +81,9 @@ export function generateBoxRules( } else { const sideRules = individualProperties.reduce( ( acc: GeneratedCSSRule[], side: string ) => { - const value: string | undefined = get( boxStyle, [ side ] ); + const value: string | undefined = getCSSVarFromStyleValue( + get( boxStyle, [ side ] ) + ); if ( value ) { acc.push( { selector: options?.selector, diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index ed5b867e77534..36f28873fc0ac 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -2936,4 +2936,302 @@ function test_get_property_value_self() { $expected = 'body { margin: 0; }body{background-color: #ffffff;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; $this->assertEquals( $expected, $theme_json->get_stylesheet() ); } + + /** + * Tests generating the spacing presets array based on the spacing scale provided. + * + * @dataProvider data_generate_spacing_scale_fixtures + */ + function test_set_spacing_sizes( $spacing_scale, $expected_output ) { + $theme_json = new WP_Theme_JSON_Gutenberg( + array( + 'version' => 2, + 'settings' => array( + 'spacing' => array( + 'spacingScale' => $spacing_scale, + ), + ), + ) + ); + + $theme_json->set_spacing_sizes(); + $this->assertSame( $expected_output, _wp_array_get( $theme_json->get_raw_data(), array( 'settings', 'spacing', 'spacingSizes', 'default' ) ) ); + } + + /** + * Data provider for spacing scale tests. + * + * @return array + */ + function data_generate_spacing_scale_fixtures() { + return array( + 'empty_spacing_scale' => array( + 'spacing_scale' => array(), + 'expected_output' => null, + ), + + 'invalid_spacing_scale_values_missing_operator' => array( + 'spacingScale' => array( + 'operator' => '', + 'increment' => 1.5, + 'steps' => 1, + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => null, + ), + + 'invalid_spacing_scale_values_non_numeric_increment' => array( + 'spacingScale' => array( + 'operator' => '+', + 'increment' => 'add two to previous value', + 'steps' => 1, + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => null, + ), + + 'invalid_spacing_scale_values_non_numeric_steps' => array( + 'spacingScale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 'spiral staircase preferred', + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => null, + ), + + 'invalid_spacing_scale_values_non_numeric_medium_step' => array( + 'spacingScale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 5, + 'mediumStep' => 'That which is just right', + 'unit' => 'rem', + ), + 'expected_output' => null, + ), + + 'invalid_spacing_scale_values_missing_unit' => array( + 'spacingScale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 5, + 'mediumStep' => 4, + ), + 'expected_output' => null, + ), + + 'one_step_spacing_scale' => array( + 'spacingScale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 1, + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => 'Medium', + 'slug' => 50, + 'size' => '4rem', + ), + ), + ), + + 'two_step_spacing_scale_should_add_step_above_medium' => array( + 'spacingScale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 2, + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => 'Medium', + 'slug' => 50, + 'size' => '4rem', + ), + array( + 'name' => 'Large', + 'slug' => 60, + 'size' => '5.5rem', + ), + ), + ), + + 'three_step_spacing_scale_should_add_step_above_and_below_medium' => array( + 'spacingScale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 3, + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => 'Small', + 'slug' => 40, + 'size' => '2.5rem', + ), + array( + 'name' => 'Medium', + 'slug' => 50, + 'size' => '4rem', + ), + array( + 'name' => 'Large', + 'slug' => 60, + 'size' => '5.5rem', + ), + ), + ), + + 'even_step_spacing_scale_steps_should_add_extra_step_above_medium' => array( + 'spacingScale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 4, + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => 'Small', + 'slug' => 40, + 'size' => '2.5rem', + ), + array( + 'name' => 'Medium', + 'slug' => 50, + 'size' => '4rem', + ), + array( + 'name' => 'Large', + 'slug' => 60, + 'size' => '5.5rem', + ), + array( + 'name' => 'X-Large', + 'slug' => 70, + 'size' => '7rem', + ), + ), + ), + + 'if_bottom_end_will_go_below_zero_should_add_extra_steps_above_medium_instead' => array( + 'spacingScale' => array( + 'operator' => '+', + 'increment' => 2.5, + 'steps' => 5, + 'mediumStep' => 5, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => 'Small', + 'slug' => 40, + 'size' => '2.5rem', + ), + array( + 'name' => 'Medium', + 'slug' => 50, + 'size' => '5rem', + ), + array( + 'name' => 'Large', + 'slug' => 60, + 'size' => '7.5rem', + ), + array( + 'name' => 'X-Large', + 'slug' => 70, + 'size' => '10rem', + ), + array( + 'name' => '2X-Large', + 'slug' => 80, + 'size' => '12.5rem', + ), + ), + ), + + 'multiplier_should_correctly_calculate_above_and_below_medium' => array( + 'spacingScale' => array( + 'operator' => '*', + 'increment' => 1.5, + 'steps' => 5, + 'mediumStep' => 1.5, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => 'X-Small', + 'slug' => 30, + 'size' => '0.67rem', + ), + array( + 'name' => 'Small', + 'slug' => 40, + 'size' => '1rem', + ), + array( + 'name' => 'Medium', + 'slug' => 50, + 'size' => '1.5rem', + ), + array( + 'name' => 'Large', + 'slug' => 60, + 'size' => '2.25rem', + ), + array( + 'name' => 'X-Large', + 'slug' => 70, + 'size' => '3.38rem', + ), + ), + ), + + 'increment_<_1_combined_with_*_operator_should_act_as_divisor_to_calculate_above_and_below_medium' => array( + 'spacingScale' => array( + 'operator' => '*', + 'increment' => 0.25, + 'steps' => 5, + 'mediumStep' => 1.5, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => 'X-Small', + 'slug' => 30, + 'size' => '0.09rem', + ), + array( + 'name' => 'Small', + 'slug' => 40, + 'size' => '0.38rem', + ), + array( + 'name' => 'Medium', + 'slug' => 50, + 'size' => '1.5rem', + ), + array( + 'name' => 'Large', + 'slug' => 60, + 'size' => '6rem', + ), + array( + 'name' => 'X-Large', + 'slug' => 70, + 'size' => '24rem', + ), + ), + ), + ); + } } diff --git a/schemas/json/theme.json b/schemas/json/theme.json index 661de26ad91eb..36ed92025ec15 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -226,6 +226,69 @@ "type": "string" }, "default": [ "px", "em", "rem", "vh", "vw", "%" ] + }, + "customSpacingSize": { + "description": "Allow users to set custom space sizes.", + "type": "boolean", + "default": true + }, + "spacingSizes": { + "description": "Space size presets for the space size selector.\nGenerates a custom property (`--wp--preset--space-size--{slug}`) per preset value.", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "description": "Name of the space size preset, translatable.", + "type": "string" + }, + "slug": { + "description": "Kebab-case unique identifier for the space size preset.", + "type": "string" + }, + "size": { + "description": "CSS space-size value, including units.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "spacingScale": { + "description": "Space size presets for the space size selector.\nGenerates a custom property (`--wp--preset--space-size--{slug}`) per preset value.", + "type": "object", + "properties": { + "operator": { + "description": "With + or * depending on whether scale is generated by increment or mulitplier.", + "type": "string", + "enum": [ "+", "*" ] + }, + "increment": { + "description": "The amount to increment each step by.", + "type": "number" + }, + "steps": { + "description": "Number of steps to generate in scale.", + "type": "integer" + }, + "mediumStep": { + "description": "The value to medium setting in the scale.", + "type": "number" + }, + "unit": { + "description": "Unit that the scale uses, eg. rem, em, px.", + "type": "string", + "enum": [ + "px", + "em", + "rem", + "vh", + "vw", + "%" + ] + } + }, + "additionalProperties": false } }, "additionalProperties": false