From 46a64c9617b9296636aa18039e902ab2e2d755ab Mon Sep 17 00:00:00 2001 From: Arukuen Date: Wed, 23 Sep 2026 13:34:42 +0800 Subject: [PATCH 1/3] fix: change the current shadow control following gutenberg's new --- src/components/shadow-control/editor.scss | 65 +++++++++++++ src/components/shadow-control/index.js | 107 +++++++++++++++++----- 2 files changed, 147 insertions(+), 25 deletions(-) diff --git a/src/components/shadow-control/editor.scss b/src/components/shadow-control/editor.scss index 36689af37c..38a13973fe 100644 --- a/src/components/shadow-control/editor.scss +++ b/src/components/shadow-control/editor.scss @@ -11,3 +11,68 @@ align-items: center; } } + +.stk-shadow-control__presets-button { + justify-content: flex-start; + width: 100%; + + .dashicon { + margin-right: 8px; + } +} + +.stk-shadow-control__preset-grid { + display: flex; + flex-wrap: wrap; + gap: 12px; + padding-bottom: 8px; +} + +.stk-shadow-control__preset { + appearance: none; + align-items: center; + background: #fff; + border: 1px solid #e0e0e0; + border-radius: 2px; + box-sizing: border-box; + color: #2f2f2f; + cursor: pointer; + display: inline-flex; + height: 26px; + justify-content: center; + padding: 0; + position: relative; + transform: scale(1); + width: 26px; + + &:hover { + transform: scale(1.2); + } + + &:focus-visible { + border: 2px solid #757575; + outline: none; + } + + &.is-none { + background: linear-gradient(-45deg, transparent 48%, #ddd 48%, #ddd 52%, transparent 52%); + } + + .dashicon { + color: #2f2f2f; + font-size: 20px; + height: 20px; + width: 20px; + } +} + +@media (prefers-reduced-motion: no-preference) { + .stk-shadow-control__preset { + transition: transform 0.1s ease; + } +} + +.stk-shadow-control__clear.components-button { + display: block; + margin: 18px 0 0 auto; +} diff --git a/src/components/shadow-control/index.js b/src/components/shadow-control/index.js index 5f08a652b8..7ed2767a86 100644 --- a/src/components/shadow-control/index.js +++ b/src/components/shadow-control/index.js @@ -20,7 +20,9 @@ import { memo, } from '@wordpress/element' import { applyFilters } from '@wordpress/hooks' -import { Button, Dashicon } from '@wordpress/components' +import { + Button, Dashicon, PanelBody, Tooltip, +} from '@wordpress/components' export const getShadows = () => { return applyFilters( 'stackable.shadows', [ @@ -254,8 +256,9 @@ const ShadowControl = memo( props => { } = props const shadows = options || getShadows() - const buttonRef = useRef( null ) - const [ isPopoverOpen, setIsPopoverOpen ] = useState( false ) + const presetButtonRef = useRef( null ) + const settingsButtonRef = useRef( null ) + const [ openPopover, setOpenPopover ] = useState( '' ) const valueCallback = value => { return value ? shadows.indexOf( value ) === -1 ? 'custom' : shadows.indexOf( value ) : '' @@ -267,63 +270,117 @@ const ShadowControl = memo( props => { const [ _value, onChange ] = useControlHandlers( props.attribute, props.responsive, props.hover, valueCallback, changeCallback ) const value = typeof props.value === 'undefined' ? _value : props.value + const effectiveOnChange = typeof props.onChange === 'undefined' ? onChange : props.onChange + const selectedValue = value === '' ? valueCallback( props.placeholder ) : value const [ propsToPass, controlProps ] = extractControlProps( _props ) useEffect( () => { const clickOutsideListener = event => { - if ( isPopoverOpen ) { + if ( openPopover ) { if ( ! event.target.closest( '.shadow-control__popover' ) && + ! event.target.closest( '.stk-shadow-control__presets-button' ) && ! event.target.closest( '.stk-shadow-control__more-button' ) && ! event.target.closest( '.components-color-picker' ) && ! event.target.closest( '.react-autosuggest__suggestions-container' ) && ! event.target.closest( '.components-dropdown__content' ) ) { - setIsPopoverOpen( false ) + setOpenPopover( '' ) } } } document.body.addEventListener( 'mousedown', clickOutsideListener ) return () => document.body.removeEventListener( 'mousedown', clickOutsideListener ) - }, [ isPopoverOpen ] ) - - useEffect( () => { - if ( isPopoverOpen ) { - } - }, [ value, isPopoverOpen ] ) + }, [ openPopover ] ) return ( <> - setIsPopoverOpen( ! isPopoverOpen ) } + onClick={ () => setOpenPopover( openPopover === 'settings' ? '' : 'settings' ) } icon={ } /> ) } - /> - { isPopoverOpen && ( + > + + + { openPopover === 'presets' && ( + setOpenPopover( '' ) } + > + +

{ __( 'Drop shadow', i18n ) }

+
+ { shadows.map( ( shadow, index ) => { + const isSelected = selectedValue === index + const presetLabel = index === 0 + ? __( 'No shadow', i18n ) + : sprintf( __( 'Shadow %d', i18n ), index ) + + return ( + + + + ) + } ) } +
+ +
+
+ ) } + { openPopover === 'settings' && ( { parentProps={ props } hasInset={ props.hasInset } isFilter={ props.isFilter } - onEscape={ () => setIsPopoverOpen( false ) } + onEscape={ () => setOpenPopover( '' ) } value={ props.shadowFilterValue } onChange={ props.shadowFilterOnChange } /> From 5143b3639463ec9bd6e419b75220528dd4b97635 Mon Sep 17 00:00:00 2001 From: Arukuen Date: Tue, 29 Sep 2026 08:29:42 +0800 Subject: [PATCH 2/3] fix: shadows and border radius --- e2e/readme.md | 2 +- e2e/tests/admin.spec.ts | 1 + src/block-components/typography/edit.js | 6 +- src/components/index.js | 4 +- .../shadow-control/__test__/index.test.js | 32 +++ src/components/shadow-control/editor.scss | 25 ++- src/components/shadow-control/index.js | 184 +++++++++++++----- src/hooks/use-preset-controls.js | 37 ++-- .../preset-controls/editor-loader.js | 26 ++- .../global-settings/preset-controls/index.php | 104 +++++++--- .../preset-controls/presets.json | 125 ++++++++---- .../utils/use-block-layout-inspector-utils.js | 27 ++- src/welcome/admin.js | 10 + 13 files changed, 449 insertions(+), 134 deletions(-) create mode 100644 src/components/shadow-control/__test__/index.test.js diff --git a/e2e/readme.md b/e2e/readme.md index 17829f2137..4584fbead9 100644 --- a/e2e/readme.md +++ b/e2e/readme.md @@ -135,7 +135,7 @@ Fail if a panel still shows `ProControl` / "Get Premium" while premium is mocked | Surface | Flow | |---------|------| -| Design System | Sidebar opens; Preview; Color Schemes / Font pairs / Size presets / Icon Library are live controls | +| Design System | Sidebar opens; Preview; Color Schemes / Font pairs / Size and shadow presets / Icon Library are live controls | | Global Block Styles | Save a named style from one Text block, apply it to a second; canvas uses the style, inspector stays at defaults and can override; rename and delete from Design System; updating a style applies the change to other blocks that use it | | Dynamic Content | Post title / meta / featured image resolve on the frontend; custom date format apply path (`e2e/tests/dynamic-content-custom-date.spec.ts`) | | Conditional display | Logged-in condition visible on frontend; logged-out condition hidden while logged in | diff --git a/e2e/tests/admin.spec.ts b/e2e/tests/admin.spec.ts index d18a648829..fc05779791 100644 --- a/e2e/tests/admin.spec.ts +++ b/e2e/tests/admin.spec.ts @@ -94,6 +94,7 @@ test( 'Settings inner tabs render', async ( { await page.getByRole( 'tab', { name: /Miscellaneous/ } ).click() await expect( page.getByRole( 'heading', { name: 'Miscellaneous' } ) ).toBeVisible() + await expect( page.getByRole( 'checkbox', { name: 'Use Border Radius Presets from theme.json' } ) ).toBeVisible() await page.getByRole( 'tab', { name: 'Import/Export' } ).click() await expect( page.getByRole( 'heading', { name: 'Import' } ) ).toBeVisible() diff --git a/src/block-components/typography/edit.js b/src/block-components/typography/edit.js index 15b46f4f03..60491c86cf 100644 --- a/src/block-components/typography/edit.js +++ b/src/block-components/typography/edit.js @@ -23,7 +23,9 @@ import { PanelAdvancedSettings, ShadowControl, } from '~stackable/components' -import { getAttributeName, getAttrNameFunction } from '~stackable/util' +import { + getAttributeName, getAttrNameFunction, getFontFamily, +} from '~stackable/util' import { escapeHTMLIfInvalid } from './util' /** @@ -303,6 +305,8 @@ export const Controls = props => { label={ __( 'Shadow / Outline', i18n ) } attribute={ attributeName( 'textShadow' ) } options={ TYPOGRAPHY_SHADOWS } + previewType="text" + previewFontFamily={ fontFamily ? getFontFamily( fontFamily ) : undefined } placeholder="" hover="all" hasInset={ false } diff --git a/src/components/index.js b/src/components/index.js index 04d009af78..fa1ebd8485 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -80,7 +80,9 @@ export { default as AdvancedFocalPointControl } from './advanced-focal-point-con export { default as AdvancedTokenField } from './advanced-token-field' export { default as ImageFilterControl } from './image-filter-control' export { default as GroupPlaceholder } from './group-placeholder' -export { default as ShadowControl, getShadows } from './shadow-control' +export { + default as ShadowControl, getGlobalShadowOptions, getShadows, +} from './shadow-control' export { default as LinkControl } from './link-control' export { default as ColumnInserter } from './column-inserter' export { default as StyleControl } from './style-control' diff --git a/src/components/shadow-control/__test__/index.test.js b/src/components/shadow-control/__test__/index.test.js new file mode 100644 index 0000000000..82f74c6b4c --- /dev/null +++ b/src/components/shadow-control/__test__/index.test.js @@ -0,0 +1,32 @@ +import { getShadowFilterValue } from '../' + +describe( 'getShadowFilterValue', () => { + it( 'resolves a selected preset to its raw shadow for advanced editing', () => { + const presets = [ + { shadow: 'none' }, + { shadow: '0 5px 5px 0 #123f5209' }, + ] + + expect( getShadowFilterValue( + 1, + presets, + 'var(--stk--preset--shadow--shadow-3, 0 5px 5px 0 #123f5209)' + ) ).toBe( '0 5px 5px 0 #123f5209' ) + } ) + + it( 'keeps custom shadow values unchanged', () => { + expect( getShadowFilterValue( + 'custom', + [], + '2px 4px 8px 0 #00000040' + ) ).toBe( '2px 4px 8px 0 #00000040' ) + } ) + + it( 'falls back to the stored value when a selected preset is unavailable', () => { + expect( getShadowFilterValue( + 3, + [], + '2px 4px 8px 0 #00000040' + ) ).toBe( '2px 4px 8px 0 #00000040' ) + } ) +} ) diff --git a/src/components/shadow-control/editor.scss b/src/components/shadow-control/editor.scss index 38a13973fe..a0303b8c12 100644 --- a/src/components/shadow-control/editor.scss +++ b/src/components/shadow-control/editor.scss @@ -16,15 +16,16 @@ justify-content: flex-start; width: 100%; - .dashicon { + .stk-shadow-control__presets-icon { margin-right: 8px; } } .stk-shadow-control__preset-grid { - display: flex; - flex-wrap: wrap; - gap: 12px; + display: grid; + grid-template-columns: repeat(4, 44px); + justify-content: space-between; + row-gap: 16px; padding-bottom: 8px; } @@ -38,12 +39,12 @@ color: #2f2f2f; cursor: pointer; display: inline-flex; - height: 26px; + height: 44px; justify-content: center; padding: 0; position: relative; transform: scale(1); - width: 26px; + width: 44px; &:hover { transform: scale(1.2); @@ -66,6 +67,13 @@ } } +.stk-shadow-control__preset-text { + color: #2f2f2f; + font-size: 18px; + line-height: 1; + white-space: nowrap; +} + @media (prefers-reduced-motion: no-preference) { .stk-shadow-control__preset { transition: transform 0.1s ease; @@ -76,3 +84,8 @@ display: block; margin: 18px 0 0 auto; } + +.block-editor-global-styles__shadow-popover-container .block-editor-global-styles__shadow-indicator { + height: 44px; + width: 44px; +} diff --git a/src/components/shadow-control/index.js b/src/components/shadow-control/index.js index 7ed2767a86..316337c913 100644 --- a/src/components/shadow-control/index.js +++ b/src/components/shadow-control/index.js @@ -8,6 +8,9 @@ import { } from '~stackable/components' import AdvancedControl, { extractControlProps } from '~stackable/components/base-control2' import { useControlHandlers } from '~stackable/components/base-control2/hooks' +import { ResetButton } from '~stackable/components/base-control2/reset-button' +import { usePresetControls } from '~stackable/hooks' +import DEFAULT_PRESETS from '~stackable/plugins/global-settings/preset-controls/presets.json' /** * WordPress dependencies @@ -20,23 +23,57 @@ import { memo, } from '@wordpress/element' import { applyFilters } from '@wordpress/hooks' +import { Icon, shadow } from '@wordpress/icons' import { Button, Dashicon, PanelBody, Tooltip, } from '@wordpress/components' -export const getShadows = () => { - return applyFilters( 'stackable.shadows', [ - 'none', - '0 0 0 1px #7878781a', - '0 0 0 2px #7878781a', - '0 5px 5px 0 #123f5209', - '0px 2px 20px #99999933', - '0 5px 30px -10px #123f524d', - '0px 10px 30px #0000000d', - '7px 5px 30px #48497927', - '0px 10px 60px #0000001a', - '0px 70px 90px -20px #4849794d', - ] ) +/** + * Return the raw CSS shadow values used by the preset picker. + * + * `none` is a Stackable control option rather than a theme.json preset. + * The filter remains available for extensions that append or replace shadows. + * + * @param {string[]} shadows Raw CSS shadow values. + * @return {string[]} Filtered shadow values, including the `none` option. + */ +export const getShadows = ( shadows = DEFAULT_PRESETS.settings.shadow.presets.map( preset => preset.shadow ) ) => { + return applyFilters( 'stackable.shadows', [ 'none', ...shadows ] ) +} + +/** + * Convert preset marks into values that Stackable can persist in attributes. + * + * A preset mark contains both its raw `shadow`, used for previews, and its + * `value`, normally a `--stk--preset` variable with the raw shadow as fallback. + * Values added through the legacy `stackable.shadows` filter remain raw CSS. + * + * @param {Array} shadowPresetMarks Presets returned by `usePresetControls`. + * @return {string[]} Attribute-ready shadow values. + */ +export const getGlobalShadowOptions = shadowPresetMarks => { + return getShadows( shadowPresetMarks.map( preset => preset.shadow ) ) + .map( shadowValue => { + return shadowPresetMarks.find( preset => preset.shadow === shadowValue )?.value || shadowValue + } ) +} + +/** + * Resolve the value consumed by the advanced shadow fields. + * + * The advanced fields split a raw CSS shadow into offsets, blur, spread, and + * color, so they cannot parse a preset's CSS variable expression directly. + * Custom shadows are already raw CSS and pass through unchanged. + * + * @param {number|string} selectedValue Selected preset index or `custom`. + * @param {Array} shadowPresets Normalized picker presets. + * @param {string} shadowFilterValue Stored raw or variable value. + * @return {string|undefined} Raw shadow when available, otherwise the stored value. + */ +export const getShadowFilterValue = ( selectedValue, shadowPresets, shadowFilterValue ) => { + return typeof selectedValue === 'number' + ? shadowPresets[ selectedValue ]?.shadow ?? shadowFilterValue + : shadowFilterValue } const FILTERS = [ @@ -255,23 +292,50 @@ const ShadowControl = memo( props => { ..._props } = props - const shadows = options || getShadows() + const shadowPresetMarks = usePresetControls( 'shadows' )?.getPresetMarks() || [] + // Normalize theme, user, built-in, and caller-supplied values so the picker + // can always render a label, raw preview, and persisted attribute value. + const defaultShadowPresets = [ { + name: __( 'No shadow', i18n ), + shadow: 'none', + value: 'none', + }, ...shadowPresetMarks ] + const shadowValues = options || getGlobalShadowOptions( shadowPresetMarks ) + const shadowPresets = shadowValues.map( ( shadowValue, index ) => { + const preset = defaultShadowPresets.find( item => item.value === shadowValue ) + return preset || { + name: index === 0 + ? __( 'No shadow', i18n ) + : sprintf( __( 'Shadow %d', i18n ), index ), + shadow: shadowValue, + value: shadowValue, + } + } ) const presetButtonRef = useRef( null ) const settingsButtonRef = useRef( null ) const [ openPopover, setOpenPopover ] = useState( '' ) + // Control handlers use a compact UI value: an index for presets, `custom` + // for unmatched CSS, and an empty string when no shadow is selected. + // Accepting both the variable value and raw CSS keeps existing attributes + // compatible when a site begins using global shadow presets. const valueCallback = value => { - return value ? shadows.indexOf( value ) === -1 ? 'custom' : shadows.indexOf( value ) : '' + if ( ! value ) { + return '' + } + const index = shadowPresets.findIndex( preset => preset.value === value || preset.shadow === value ) + return index === -1 ? 'custom' : index } const changeCallback = index => { - return index !== '' ? shadows[ index ] : index + return index !== '' ? shadowPresets[ index ]?.value : index } const [ _value, onChange ] = useControlHandlers( props.attribute, props.responsive, props.hover, valueCallback, changeCallback ) const value = typeof props.value === 'undefined' ? _value : props.value const effectiveOnChange = typeof props.onChange === 'undefined' ? onChange : props.onChange const selectedValue = value === '' ? valueCallback( props.placeholder ) : value + const hasTextPreview = props.previewType === 'text' const [ propsToPass, controlProps ] = extractControlProps( _props ) @@ -303,16 +367,25 @@ const ShadowControl = memo( props => { helpTooltip={ props.helpTooltip } hover={ props.hover } after={ ( - { openPopover === 'presets' && ( @@ -336,44 +409,56 @@ const ShadowControl = memo( props => {

{ __( 'Drop shadow', i18n ) }

- { shadows.map( ( shadow, index ) => { + { shadowPresets.map( ( preset, index ) => { const isSelected = selectedValue === index - const presetLabel = index === 0 - ? __( 'No shadow', i18n ) - : sprintf( __( 'Shadow %d', i18n ), index ) + const presetLabel = preset.name || sprintf( __( 'Shadow %d', i18n ), index ) return ( ) } ) }
- + { props.showClear && ( + + ) }
) } @@ -389,7 +474,7 @@ const ShadowControl = memo( props => { hasInset={ props.hasInset } isFilter={ props.isFilter } onEscape={ () => setOpenPopover( '' ) } - value={ props.shadowFilterValue } + value={ getShadowFilterValue( selectedValue, shadowPresets, props.shadowFilterValue ) } onChange={ props.shadowFilterOnChange } /> ) } @@ -406,6 +491,13 @@ ShadowControl.defaultProps = { changeCallback: null, isFilter: false, // If the style rule is `filter`, disable spread. hasInset: true, + previewType: 'box', + previewFontFamily: '', + allowReset: false, + showReset: null, + default: '', + onReset: undefined, + showClear: true, helpTooltip: { video: 'general-shadow', title: __( 'Shadow/Outline', i18n ), diff --git a/src/hooks/use-preset-controls.js b/src/hooks/use-preset-controls.js index 8bacdde9cb..efa256049b 100644 --- a/src/hooks/use-preset-controls.js +++ b/src/hooks/use-preset-controls.js @@ -1,4 +1,4 @@ -import { i18n } from 'stackable' +import { i18n, settings as stackableSettings } from 'stackable' import DEFAULT_PRESETS from '~stackable/plugins/global-settings/preset-controls/presets.json' import { useSettings } from '@wordpress/block-editor' import { useSelect } from '@wordpress/data' @@ -24,11 +24,19 @@ const PRESET_MAPPING = { prefix: 'block-height', }, borderRadius: { - settings: [ 'borderRadius' ], + settings: [ 'border', 'radiusSizes' ], defaultSizes: '', defaultEnabled: '', + useWordPressPresetsSetting: 'stackable_use_theme_border_radius_presets', prefix: 'border-radius', }, + shadows: { + settings: [ 'shadow', 'presets' ], + defaultSizes: 'shadow.presets.default', + defaultEnabled: 'shadow.defaultPresets', + prefix: 'shadow', + valueKey: 'shadow', + }, } const nonePreset = { @@ -38,6 +46,10 @@ const nonePreset = { } export const usePresetControls = property => { + const mapping = PRESET_MAPPING[ property ] + const useWordPressPresets = ! mapping.useWordPressPresetsSetting || + !! ( stackableSettings?.[ mapping.useWordPressPresetsSetting ] ?? true ) + // Get the theme presets for the property const [ _themePresets, @@ -54,9 +66,9 @@ export const usePresetControls = property => { wpDefaultPresets, defaultSizesEnabled, ] = useSettings( - PRESET_MAPPING[ property ].settings.join( '.' ), - PRESET_MAPPING[ property ].defaultSizes, - PRESET_MAPPING[ property ].defaultEnabled + mapping.settings.join( '.' ), + mapping.defaultSizes, + mapping.defaultEnabled ) // Get all custom presets @@ -65,7 +77,7 @@ export const usePresetControls = property => { return { allCustomPresets: { ..._customPresetControls } } }, [] ) - let themePresets = _themePresets + let themePresets = useWordPressPresets ? _themePresets : [] const hasThemePresets = Array.isArray( themePresets ) && themePresets.length > 0 // Merge theme presets with default presets when default sizes are enabled. @@ -74,14 +86,14 @@ export const usePresetControls = property => { if ( hasThemePresets && wpDefaultPresets && defaultSizesEnabled !== false ) { // Create a set for removing duplicates. const existingSlugs = new Set() - _themePresets.forEach( item => { + themePresets.forEach( item => { if ( item && typeof item.slug === 'string' ) { existingSlugs.add( item.slug ) } } ) themePresets = [ - ..._themePresets, + ...themePresets, ...wpDefaultPresets.filter( item => ! existingSlugs.has( item.slug ) ), ] } @@ -89,7 +101,7 @@ export const usePresetControls = property => { // Get the theme/default presets if the user have one, else return the stackable presets const basePresets = hasThemePresets ? themePresets - : PRESET_MAPPING[ property ].settings.reduce( ( acc, key ) => acc?.[ key ], DEFAULT_PRESETS.settings ) + : mapping.settings.reduce( ( acc, key ) => acc?.[ key ], DEFAULT_PRESETS.settings ) // Returns the base presets overriden by the custom presets const getMergedPresets = () => { @@ -112,7 +124,10 @@ export const usePresetControls = property => { // Setting customOnly to true returns the preset marks for custom presets only // Setting addNonePreset to true adds a none preset with a value of 0 const getPresetMarks = ( { customOnly = false, addNonePreset = false } = {} ) => { - const prefix = PRESET_MAPPING[ property ].prefix + const { + prefix, + valueKey = 'size', + } = PRESET_MAPPING[ property ] let presets = customOnly ? allCustomPresets[ property ] ?? [] : getMergedPresets() // Add the none preset presets = [ ...( addNonePreset ? [ nonePreset ] : [] ), ...presets ] @@ -121,7 +136,7 @@ export const usePresetControls = property => { .filter( preset => ! ( preset?.isDiscarded ) ) .map( preset => ( { ...preset, - value: `var(--stk--preset--${ prefix }--${ preset.slug }, ${ preset.size })`, + value: `var(--stk--preset--${ prefix }--${ preset.slug }, ${ preset[ valueKey ] })`, } ) ) } diff --git a/src/plugins/global-settings/preset-controls/editor-loader.js b/src/plugins/global-settings/preset-controls/editor-loader.js index 25042dff93..15db819591 100644 --- a/src/plugins/global-settings/preset-controls/editor-loader.js +++ b/src/plugins/global-settings/preset-controls/editor-loader.js @@ -22,16 +22,36 @@ const PRESET_MAPPING = { borderRadius: { prefix: 'border-radius', }, + shadows: { + prefix: 'shadow', + valueKey: 'shadow', + }, } +/** + * Render editor-only variables for custom presets. + * + * PHP emits the base preset variables for the frontend and editor. + * This style element reflects unsaved Global Design System changes immediately. + * + * @param {Object} customPresets Custom presets grouped by preset family. + * @param {Function} setStyles Updates the editor style element. + */ const renderGlobalStyles = ( customPresets, setStyles ) => { let css = '' Object.entries( customPresets ).forEach( ( [ key, presets ] ) => { + const mapping = PRESET_MAPPING[ key ] + if ( ! mapping || ! Array.isArray( presets ) ) { + return + } + const valueKey = mapping.valueKey || 'size' const styleRules = presets?.map( preset => { - return preset && ( ! preset?.isDiscarded ) - ? `--stk--preset--${ PRESET_MAPPING[ key ]?.prefix }--${ preset?.slug || '' }: ${ preset?.size || '' };` - : '' + if ( ! preset || preset.isDiscarded ) { + return '' + } + const presetValue = preset[ valueKey ] || '' + return '--stk--preset--' + mapping.prefix + '--' + ( preset.slug || '' ) + ': ' + presetValue + ';' } ) css += compact( styleRules ).join( '' ) } ) diff --git a/src/plugins/global-settings/preset-controls/index.php b/src/plugins/global-settings/preset-controls/index.php index 512f8d1438..b5b65adeb5 100644 --- a/src/plugins/global-settings/preset-controls/index.php +++ b/src/plugins/global-settings/preset-controls/index.php @@ -28,9 +28,15 @@ class Stackable_Size_And_Spacing_Preset_Controls { 'prefix' => 'block-height', ), 'borderRadius' => array( - 'settings' => array( 'borderRadius' ), + 'settings' => array( 'border', 'radiusSizes' ), + 'use_wordpress_presets_setting' => 'stackable_use_theme_border_radius_presets', 'prefix' => 'border-radius', ), + 'shadows' => array( + 'settings' => array( 'shadow', 'presets' ), + 'prefix' => 'shadow', + 'value_key' => 'shadow', + ), ); public $custom_presets; @@ -41,9 +47,12 @@ class Stackable_Size_And_Spacing_Preset_Controls { /** * Initialize */ - function __construct() { + function __construct() { add_action( 'register_stackable_global_settings', array( $this, 'register_use_size_presets_by_default' ) ); + add_action( 'register_stackable_global_settings', array( $this, 'register_use_theme_border_radius_presets' ) ); add_action( 'stackable_early_version_upgraded', array( $this, 'use_size_presets_by_default_set_default' ), 10, 2 ); + add_action( 'stackable_early_version_upgraded', array( $this, 'initialize_use_theme_border_radius_presets' ), 10, 2 ); + add_action( 'stackable_early_version_upgraded_frontend', array( $this, 'initialize_use_theme_border_radius_presets' ), 10, 2 ); add_action( 'stackable_early_version_upgraded', array( $this, 'migrate_global_typography_font_size' ), 10, 2 ); add_filter( 'stackable_js_settings', array( $this, 'add_setting' ) ); @@ -66,6 +75,37 @@ function register_use_size_presets_by_default() { ); } + // Register the setting for using border-radius presets from WordPress. + function register_use_theme_border_radius_presets() { + register_setting( + 'stackable_global_settings', + 'stackable_use_theme_border_radius_presets', + array( + 'type' => 'boolean', + 'description' => __( 'Use border-radius presets from the active theme and WordPress', STACKABLE_I18N ), + 'sanitize_callback' => 'rest_sanitize_boolean', + 'show_in_rest' => true, + 'default' => true, + ) + ); + } + + /** + * Keep bundled border-radius presets for upgrades while enabling theme + * presets on fresh installations. add_option() preserves later choices. + * + * @param string $old_version Previously installed Stackable version. + * @param string $new_version Newly installed Stackable version. + */ + public function initialize_use_theme_border_radius_presets( $old_version, $new_version ) { + add_option( + 'stackable_use_theme_border_radius_presets', + empty( $old_version ), + '', + false + ); + } + /** * When upgrading to v3.16.0 and above, set option to false. * If new installation, set option to true. @@ -117,6 +157,7 @@ public function migrate_global_typography_font_size( $old_version, $new_version // Make the setting available in the editor public function add_setting( $settings ) { $settings['stackable_use_size_presets_by_default'] = get_option( 'stackable_use_size_presets_by_default' ); + $settings['stackable_use_theme_border_radius_presets'] = (bool) get_option( 'stackable_use_theme_border_radius_presets', true ); return $settings; } @@ -153,16 +194,20 @@ private function load_json_file( $json_path ) { } /** - * Generate CSS variable style definitions based on the property (e.g., fontSizes, spacing). - * The given presets will be overriden it match with a preset from custom. - * - * @param array $property - * @param array $presets - * @param array $prefix - * @param bool $isTheme - * @return mixed + * Generate Stackable CSS variables for one preset family. + * + * Custom presets override base presets with the same slug. + * WordPress presets point to their generated `--wp--preset` variables so + * theme.json behavior remains intact, while Stackable presets use raw values. + * + * @param string $property Stackable preset family key. + * @param array $presets Base presets for the selected origin. + * @param string $prefix CSS variable preset prefix. + * @param bool $isTheme Whether the presets came from WordPress. + * @param string $value_key Preset field containing the raw CSS value. + * @return array Style Engine rule containing the generated declarations. */ - public function generate_css_variables_styles( $property, $presets, $prefix, $isTheme = false ) { + public function generate_css_variables_styles( $property, $presets, $prefix, $isTheme = false, $value_key = 'size' ) { $filter_name = current_filter(); $custom_presets = $this->custom_presets[ $property ] ?? []; @@ -193,7 +238,7 @@ public function generate_css_variables_styles( $property, $presets, $prefix, $is $is_custom = $preset['__is_custom'] ?? false; $value = $is_custom || ! $isTheme - ? $preset['size'] + ? ( $preset[ $value_key ] ?? '' ) : "var(--wp--preset--$prefix--$slug)"; $css_vars[ "--stk--preset--$prefix--$slug" ] = $value; @@ -228,31 +273,42 @@ public function add_preset_controls_styles( $current_css ) { $generated_styles = array(); foreach ( self::PRESET_MAPPING as $key => $value ) { - if ( ! empty( $this->deepGet( $this->theme_presets, $value[ 'settings' ] )[ 'theme' ] ) ) { + $value_key = $value[ 'value_key' ] ?? 'size'; + $use_wordpress_presets = ! isset( $value[ 'use_wordpress_presets_setting' ] ) || + (bool) get_option( $value[ 'use_wordpress_presets_setting' ], true ); + $theme_presets = $use_wordpress_presets + ? ( $this->deepGet( $this->theme_presets, $value[ 'settings' ] )[ 'theme' ] ?? array() ) + : array(); + $default_presets = $use_wordpress_presets + ? ( $this->deepGet( $this->default_presets, $value[ 'settings' ] )[ 'default' ] ?? array() ) + : array(); + + if ( ! empty( $theme_presets ) ) { $styles = $this->generate_css_variables_styles( $key, - $this->deepGet( $this->theme_presets, $value[ 'settings' ] )[ 'theme' ], + $theme_presets, $value[ 'prefix' ], - true + true, + $value_key ); - $generated_styles[] = $styles; - - } elseif ( ! empty( $this->deepGet( $this->default_presets, $value[ 'settings' ] )[ 'default' ] ) ) { + } elseif ( ! empty( $default_presets ) ) { $styles = $this->generate_css_variables_styles( $key, - $this->deepGet( $this->default_presets, $value[ 'settings' ] )[ 'default' ], + $default_presets, $value[ 'prefix' ], - true + true, + $value_key ); - $generated_styles[] = $styles; } else { $styles = $this->generate_css_variables_styles( $key, - $this->deepGet( $this->stackable_presets, $value[ 'settings' ] ), + $this->deepGet( $this->stackable_presets, $value[ 'settings' ] ), $value[ 'prefix' ], + false, + $value_key ); - $generated_styles[] = $styles; } + $generated_styles[] = $styles; } $generated_css = wp_style_engine_get_stylesheet_from_css_rules( $generated_styles ); @@ -268,4 +324,4 @@ public function add_preset_controls_styles( $current_css ) { } new Stackable_Size_And_Spacing_Preset_Controls(); -} \ No newline at end of file +} diff --git a/src/plugins/global-settings/preset-controls/presets.json b/src/plugins/global-settings/preset-controls/presets.json index df2612f86e..f42fab638b 100644 --- a/src/plugins/global-settings/preset-controls/presets.json +++ b/src/plugins/global-settings/preset-controls/presets.json @@ -135,42 +135,93 @@ "slug": "full" } ], - "borderRadius": [ - { - "name": "XS", - "size": "2px", - "slug": "x-small" - }, - { - "name": "S", - "size": "4px", - "slug": "small" - }, - { - "name": "M", - "size": "8px", - "slug": "medium" - }, - { - "name": "L", - "size": "16px", - "slug": "large" - }, - { - "name": "XL", - "size": "24px", - "slug": "x-large" - }, - { - "name": "2XL", - "size": "32px", - "slug": "xx-large" - }, - { - "name": "Full", - "size": "9999px", - "slug": "full" - } - ] + "border": { + "radiusSizes": [ + { + "name": "XS", + "size": "2px", + "slug": "x-small" + }, + { + "name": "S", + "size": "4px", + "slug": "small" + }, + { + "name": "M", + "size": "8px", + "slug": "medium" + }, + { + "name": "L", + "size": "16px", + "slug": "large" + }, + { + "name": "XL", + "size": "24px", + "slug": "x-large" + }, + { + "name": "2XL", + "size": "32px", + "slug": "xx-large" + }, + { + "name": "Full", + "size": "9999px", + "slug": "full" + } + ] + }, + "shadow": { + "presets": [ + { + "name": "Shadow 1", + "slug": "shadow-1", + "shadow": "0 0 0 1px #7878781a" + }, + { + "name": "Shadow 2", + "slug": "shadow-2", + "shadow": "0 0 0 2px #7878781a" + }, + { + "name": "Shadow 3", + "slug": "shadow-3", + "shadow": "0 5px 5px 0 #123f5209" + }, + { + "name": "Shadow 4", + "slug": "shadow-4", + "shadow": "0px 2px 20px #99999933" + }, + { + "name": "Shadow 5", + "slug": "shadow-5", + "shadow": "0 5px 30px -10px #123f524d" + }, + { + "name": "Shadow 6", + "slug": "shadow-6", + "shadow": "0px 10px 30px #0000000d" + }, + { + "name": "Shadow 7", + "slug": "shadow-7", + "shadow": "7px 5px 30px #48497927" + }, + { + "name": "Shadow 8", + "slug": "shadow-8", + "shadow": "0px 10px 60px #0000001a" + }, + { + "name": "Shadow 9", + "slug": "shadow-9", + "shadow": "0px 70px 90px -20px #4849794d" + } + ] + } } } diff --git a/src/plugins/global-settings/utils/use-block-layout-inspector-utils.js b/src/plugins/global-settings/utils/use-block-layout-inspector-utils.js index 17adaa9033..2c3b93ba60 100644 --- a/src/plugins/global-settings/utils/use-block-layout-inspector-utils.js +++ b/src/plugins/global-settings/utils/use-block-layout-inspector-utils.js @@ -6,9 +6,11 @@ import { hoverState } from './block-layout-utils' /** * External dependencies */ -import { getShadows } from '~stackable/components' +import { getGlobalShadowOptions } from '~stackable/components' import { IMAGE_SHADOWS } from '~stackable/block-components' -import { useDeviceType, useBlockHoverState } from '~stackable/hooks' +import { + useDeviceType, useBlockHoverState, usePresetControls, +} from '~stackable/hooks' /** * WordPress dependencies @@ -26,7 +28,8 @@ export const useBlockLayoutInspectorUtils = ( storeName, optionName, setDisplayH const [ currentHoverState ] = useBlockHoverState( { forceUpdateHoverState: true } ) const deviceType = useDeviceType() - const shadows = getShadows() + const shadowPresetMarks = usePresetControls( 'shadows' )?.getPresetMarks() || [] + const shadows = getGlobalShadowOptions( shadowPresetMarks ) const getValue = ( property, { responsive = false, hover = false, unit = false, @@ -41,7 +44,23 @@ export const useBlockLayoutInspectorUtils = ( storeName, optionName, setDisplayH const valueCallback = ( value, isImage = false ) => { const options = isImage ? IMAGE_SHADOWS : shadows - return value ? ( options.indexOf( value ) === -1 ? 'custom' : options.indexOf( value ) ) : '' + if ( ! value ) { + return '' + } + const index = options.indexOf( value ) + if ( index !== -1 ) { + return index + } + if ( ! isImage ) { + // Older attributes may store raw CSS while current global presets use + // a Stackable variable. Match the raw shadow before marking it custom. + const rawPreset = shadowPresetMarks.find( preset => preset.shadow === value ) + const presetIndex = rawPreset ? options.indexOf( rawPreset.value ) : -1 + if ( presetIndex !== -1 ) { + return presetIndex + } + } + return 'custom' } const changeCallback = ( index, isImage = false ) => { diff --git a/src/welcome/admin.js b/src/welcome/admin.js index b621d886b6..5f042828a4 100644 --- a/src/welcome/admin.js +++ b/src/welcome/admin.js @@ -1554,6 +1554,16 @@ const AdditionalOptions = props => { } } __nextHasNoMarginBottom /> + { + handleSettingsChange( { stackable_use_theme_border_radius_presets: checked } ) // eslint-disable-line camelcase + } } + __nextHasNoMarginBottom + /> Date: Wed, 30 Sep 2026 10:22:21 +0800 Subject: [PATCH 3/3] fix: bigger typography shadow control --- src/components/shadow-control/editor.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/shadow-control/editor.scss b/src/components/shadow-control/editor.scss index a0303b8c12..52f68774f5 100644 --- a/src/components/shadow-control/editor.scss +++ b/src/components/shadow-control/editor.scss @@ -69,7 +69,7 @@ .stk-shadow-control__preset-text { color: #2f2f2f; - font-size: 18px; + font-size: 22px; line-height: 1; white-space: nowrap; }