From 523ca77c5dea68f09bfa024f3b643873cf33c10b Mon Sep 17 00:00:00 2001 From: Birgit Pauli-Haack Date: Thu, 21 May 2026 19:51:55 +0200 Subject: [PATCH 01/23] =?UTF-8?q?Add=20Edit=20Theme=20Settings=20modal=20?= =?UTF-8?q?=E2=80=94=20Color=20Settings=20+=20Palette=20(foundation)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an "Edit Theme Settings" entry to the CBT sidebar (cog icon, between Create Theme Variation and Edit Theme Metadata) that opens a modal with a single Color tab containing two panels: - Color Settings: toggles for default/custom presets and link color. - Palette: add / edit name / edit slug / pick color / remove rows. Modal owns the working state, seeded from `getCurrentTheme().theme_json` and reseeded after a successful save via `invalidateResolution`. Update button is disabled when nothing has changed and shows a count of pending field changes when dirty. Save calls `POST /create-block-theme/v1/theme-settings` (landed in #843) and surfaces success/error notices as snackbars. Modal stays open on success so the user can keep editing. Foundation only — Gradients and Duotone panels, and the Dimensions / Typography / Shadows / Templates tabs, land in follow-up PRs. Refs #838 --- readme.txt | 1 + .../edit-theme-settings-modal.js | 460 ++++++++++++++++++ src/plugin-sidebar.js | 20 + src/plugin-styles.scss | 23 + src/resolvers.js | 11 + 5 files changed, 515 insertions(+) create mode 100644 src/editor-sidebar/edit-theme-settings-modal.js diff --git a/readme.txt b/readme.txt index 7c5a8eaf..5ae0cea7 100644 --- a/readme.txt +++ b/readme.txt @@ -20,6 +20,7 @@ This plugin allows you to: - Create a new style variation - Export a theme - Save user changed templates and styles to the active theme +- Edit theme settings (color presets and palette) and save them to the active theme's theme.json - Expand the typography controls in the block inspector while authoring a theme (opt-in, under Editor preferences) All newly created themes or style variations will include changes made within the WordPress Editor. diff --git a/src/editor-sidebar/edit-theme-settings-modal.js b/src/editor-sidebar/edit-theme-settings-modal.js new file mode 100644 index 00000000..61964c8b --- /dev/null +++ b/src/editor-sidebar/edit-theme-settings-modal.js @@ -0,0 +1,460 @@ +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { + useState, + useEffect, + useMemo, + createInterpolateElement, +} from '@wordpress/element'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as noticesStore } from '@wordpress/notices'; +import { + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalHStack as HStack, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalVStack as VStack, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalText as Text, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalItem as Item, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalItemGroup as ItemGroup, + BaseControl, + Button, + ColorIndicator, + ColorPicker, + Dropdown, + FlexBlock, + Modal, + Notice, + PanelBody, + TabPanel, + TextControl, + ToggleControl, +} from '@wordpress/components'; +import { plus, lineSolid } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { postUpdateThemeSettings } from '../resolvers'; + +const COLOR_SETTINGS_KEYS = [ + 'defaultPalette', + 'defaultGradients', + 'defaultDuotone', + 'custom', + 'customGradient', + 'customDuotone', + 'link', +]; + +const COLOR_SETTINGS_DEFAULTS = { + defaultPalette: true, + defaultGradients: true, + defaultDuotone: true, + custom: true, + customGradient: true, + customDuotone: true, + link: false, +}; + +const ColorSettingsPanel = ( { value, onChange } ) => { + const update = ( key ) => ( next ) => + onChange( { ...value, [ key ]: next } ); + + return ( + + + + { __( 'Default presets', 'create-block-theme' ) } + + + + + + + + + + { __( 'Custom presets', 'create-block-theme' ) } + + + + + + + + + + ); +}; + +const PaletteRow = ( { entry, onUpdate, onRemove } ) => ( + + + ( + + ) } + renderContent={ () => ( + + onUpdate( { ...entry, color } ) + } + /> + ) } + /> + + onUpdate( { ...entry, name } ) } + /> + + + onUpdate( { ...entry, slug } ) } + /> + + + + + ); +}; diff --git a/src/plugin-sidebar.js b/src/plugin-sidebar.js index 2d283361..fe744acf 100644 --- a/src/plugin-sidebar.js +++ b/src/plugin-sidebar.js @@ -58,6 +58,7 @@ import GlobalStylesJsonEditorModal from './editor-sidebar/global-styles-json-edi import { SaveThemePanel } from './editor-sidebar/save-panel'; import { CreateVariationPanel } from './editor-sidebar/create-variation-panel'; import { ThemeMetadataEditorModal } from './editor-sidebar/metadata-editor-modal'; +import { EditThemeSettingsModal } from './editor-sidebar/edit-theme-settings-modal'; import ScreenHeader from './editor-sidebar/screen-header'; import { downloadExportedTheme } from './resolvers'; import downloadFile from './utils/download-file'; @@ -90,6 +91,8 @@ const CreateBlockThemePlugin = () => { const [ isMetadataEditorOpen, setIsMetadataEditorOpen ] = useState( false ); + const [ isThemeSettingsOpen, setIsThemeSettingsOpen ] = useState( false ); + const [ cloneCreateType, setCloneCreateType ] = useState( '' ); const { createErrorNotice } = useDispatch( noticesStore ); @@ -156,6 +159,17 @@ const CreateBlockThemePlugin = () => { 'create-block-theme' ) } + + setIsThemeSettingsOpen( true ) + } + > + { __( + 'Edit Theme Settings', + 'create-block-theme' + ) } + @@ -378,6 +392,12 @@ const CreateBlockThemePlugin = () => { onRequestClose={ () => setIsMetadataEditorOpen( false ) } /> ) } + + { isThemeSettingsOpen && ( + setIsThemeSettingsOpen( false ) } + /> + ) } ); }; diff --git a/src/plugin-styles.scss b/src/plugin-styles.scss index f80c7463..70a5bf57 100644 --- a/src/plugin-styles.scss +++ b/src/plugin-styles.scss @@ -27,3 +27,26 @@ $modal-footer-height: 70px; object-fit: cover; } +.create-block-theme__edit-theme-settings-modal { + padding-bottom: $modal-footer-height; +} + +.create-block-theme__edit-theme-settings-modal__footer { + border-top: 1px solid #ddd; + background-color: #fff; + position: absolute; + bottom: 0; + margin: 0 -32px; + padding: 16px 32px; + height: $modal-footer-height; +} + +.cbt-palette-swatch-button { + min-width: 24px; + padding: 0; +} + +.cbt-palette-section-header { + margin-bottom: 8px; +} + diff --git a/src/resolvers.js b/src/resolvers.js index d0219fc8..2cc4ac0c 100644 --- a/src/resolvers.js +++ b/src/resolvers.js @@ -85,6 +85,17 @@ export async function postUpdateThemeMetadata( theme ) { } ); } +export async function postUpdateThemeSettings( payload ) { + return apiFetch( { + path: '/create-block-theme/v1/theme-settings', + method: 'POST', + data: payload, + headers: { + 'Content-Type': 'application/json', + }, + } ); +} + export async function downloadExportedTheme() { return apiFetch( { path: '/create-block-theme/v1/export', From 2a050b97802b786e097845ebd7f7c8fd8f385a35 Mon Sep 17 00:00:00 2001 From: Birgit Pauli-Haack Date: Thu, 21 May 2026 19:56:55 +0200 Subject: [PATCH 02/23] Palette panel: add Name / Slug column headers Adds a header row above the palette ItemGroup with "Name" and "Slug" labels aligned to the underlying input columns. Spacer cells match the swatch button (left) and remove button (right) widths so columns line up. Refs #838 --- .../edit-theme-settings-modal.js | 48 ++++++++++++++----- src/plugin-styles.scss | 11 +++++ 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src/editor-sidebar/edit-theme-settings-modal.js b/src/editor-sidebar/edit-theme-settings-modal.js index 61964c8b..96397b10 100644 --- a/src/editor-sidebar/edit-theme-settings-modal.js +++ b/src/editor-sidebar/edit-theme-settings-modal.js @@ -232,18 +232,44 @@ const PalettePanel = ( { value, onChange } ) => { /> { value.length > 0 && ( - - { value.map( ( entry, index ) => ( - - updateEntry( index, updated ) - } - onRemove={ () => removeEntry( index ) } + <> + + + + + { value.map( ( entry, index ) => ( + + updateEntry( index, updated ) + } + onRemove={ () => removeEntry( index ) } + /> + ) ) } + + ) } ); diff --git a/src/plugin-styles.scss b/src/plugin-styles.scss index 70a5bf57..3c67729a 100644 --- a/src/plugin-styles.scss +++ b/src/plugin-styles.scss @@ -50,3 +50,14 @@ $modal-footer-height: 70px; margin-bottom: 8px; } +.cbt-palette-column-headers { + padding: 0 12px; + + &__swatch, + &__remove { + display: inline-block; + width: 24px; + flex-shrink: 0; + } +} + From 271dd42bc6008c542da8e24a1c302b7a67b5ff6e Mon Sep 17 00:00:00 2001 From: Birgit Pauli-Haack Date: Thu, 21 May 2026 22:45:25 +0200 Subject: [PATCH 03/23] Modal: only warn when user customizations are present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reads the user-origin Global Styles entity via __experimentalGetCurrentGlobalStylesId and surfaces the warning only when the saved record or its in-editor edits contain non-empty top-level settings/styles slices. Lists which slices are customized (color, typography, spacing, ...) so the user knows what would conflict. When the theme has no user-level customizations the warning is hidden entirely — there is nothing to conflict with. Refs #838 --- .../edit-theme-settings-modal.js | 134 +++++++++++++++--- 1 file changed, 113 insertions(+), 21 deletions(-) diff --git a/src/editor-sidebar/edit-theme-settings-modal.js b/src/editor-sidebar/edit-theme-settings-modal.js index 96397b10..077d9ee6 100644 --- a/src/editor-sidebar/edit-theme-settings-modal.js +++ b/src/editor-sidebar/edit-theme-settings-modal.js @@ -307,6 +307,73 @@ const pickColorSettings = ( themeColor ) => { return out; }; +// Human-readable label for each top-level slice of `settings` / `styles` +// that the user may have customized in the Site Editor. Keys are taken +// from the canonical theme.json schema; anything not in this map is +// surfaced under "other" as a catch-all so newly-introduced WP keys don't +// silently vanish from the warning. +const USER_CUSTOMIZATION_SECTION_LABELS = { + color: __( 'color', 'create-block-theme' ), + typography: __( 'typography', 'create-block-theme' ), + spacing: __( 'spacing', 'create-block-theme' ), + layout: __( 'layout', 'create-block-theme' ), + dimensions: __( 'dimensions', 'create-block-theme' ), + border: __( 'borders', 'create-block-theme' ), + shadow: __( 'shadows', 'create-block-theme' ), + background: __( 'background', 'create-block-theme' ), + elements: __( 'elements', 'create-block-theme' ), + blocks: __( 'block styles', 'create-block-theme' ), + filter: __( 'filters', 'create-block-theme' ), + css: __( 'additional CSS', 'create-block-theme' ), + custom: __( 'custom', 'create-block-theme' ), +}; + +const isNonEmpty = ( value ) => { + if ( value === null || value === undefined ) { + return false; + } + if ( Array.isArray( value ) ) { + return value.length > 0; + } + if ( typeof value === 'object' ) { + return Object.keys( value ).length > 0; + } + return true; +}; + +// Crawl the saved user Global Styles record + any in-editor edits and +// return the de-duplicated, human-readable list of top-level slices that +// have user-level customizations diverging from theme.json. +const getCustomizedSections = ( userGlobalStyles, edits ) => { + const sections = new Set(); + + const visit = ( record ) => { + if ( ! record ) { + return; + } + for ( const top of [ 'settings', 'styles' ] ) { + const slice = record[ top ]; + if ( ! slice || typeof slice !== 'object' ) { + continue; + } + for ( const [ key, value ] of Object.entries( slice ) ) { + if ( isNonEmpty( value ) ) { + sections.add( key ); + } + } + } + }; + + visit( userGlobalStyles ); + visit( edits ); + + return Array.from( sections ).map( + ( key ) => + USER_CUSTOMIZATION_SECTION_LABELS[ key ] || + __( 'other', 'create-block-theme' ) + ); +}; + // Per-field dirty diff between the modal's working state and the last-saved // snapshot from the server. Returns the number of fields that differ — // surfaced in the Update button label. @@ -330,6 +397,25 @@ export const EditThemeSettingsModal = ( { onRequestClose } ) => { ( select ) => select( 'core' ).getCurrentTheme(), [] ); + + const customizedSections = useSelect( ( select ) => { + const core = select( 'core' ); + const getId = core.__experimentalGetCurrentGlobalStylesId; + if ( typeof getId !== 'function' ) { + return []; + } + const id = getId(); + if ( ! id ) { + return []; + } + // Saved user-origin record (database). + const record = core.getEntityRecord( 'root', 'globalStyles', id ); + // In-editor edits not yet persisted to the database. + const edits = core.getEntityRecordEdits?.( 'root', 'globalStyles', id ); + return getCustomizedSections( record, edits ); + }, [] ); + const hasUserCustomizations = customizedSections.length > 0; + const { invalidateResolution } = useDispatch( 'core' ); const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore ); @@ -440,27 +526,33 @@ export const EditThemeSettingsModal = ( { onRequestClose } ) => { 'create-block-theme' ) } - -
- { __( - 'Changes you’ve saved in the Site Editor live in the database, not in your theme files.', - 'create-block-theme' - ) } -
-
- { createInterpolateElement( - __( - 'Click Save Changes to Theme first to write them to theme.json — otherwise the edits you make here may conflict with or hide them.', - 'create-block-theme' - ), - { strong: } - ) } -
-
+ { hasUserCustomizations && ( + +
+ { sprintf( + /* translators: %s: comma-separated list of customized sections (e.g. "color, typography") */ + __( + 'You have changes in the Site Editor that haven’t been written to theme.json: %s.', + 'create-block-theme' + ), + customizedSections.join( ', ' ) + ) } +
+
+ { createInterpolateElement( + __( + 'Click Save Changes to Theme first — otherwise the edits you make here may be hidden by those overrides.', + 'create-block-theme' + ), + { strong: } + ) } +
+
+ ) } Date: Thu, 21 May 2026 22:49:24 +0200 Subject: [PATCH 04/23] Modal: bold the list of customized sections in the warning Wraps the comma-separated section list in a tag so the user's eye lands on what's customized before reading the rest of the sentence. Refs #838 --- src/editor-sidebar/edit-theme-settings-modal.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/editor-sidebar/edit-theme-settings-modal.js b/src/editor-sidebar/edit-theme-settings-modal.js index 077d9ee6..dc9e38b8 100644 --- a/src/editor-sidebar/edit-theme-settings-modal.js +++ b/src/editor-sidebar/edit-theme-settings-modal.js @@ -533,13 +533,16 @@ export const EditThemeSettingsModal = ( { onRequestClose } ) => { className="create-block-theme__edit-theme-settings-modal__disclaimer" >
- { sprintf( - /* translators: %s: comma-separated list of customized sections (e.g. "color, typography") */ - __( - 'You have changes in the Site Editor that haven’t been written to theme.json: %s.', - 'create-block-theme' + { createInterpolateElement( + sprintf( + /* translators: %s: comma-separated list of customized sections, wrapped in (e.g. "color, typography") */ + __( + 'You have changes in the Site Editor that haven’t been written to theme.json: %s.', + 'create-block-theme' + ), + customizedSections.join( ', ' ) ), - customizedSections.join( ', ' ) + { list: } ) }
From 9426bd61ba3854df5576b215fd6b9bd9af4e7b1a Mon Sep 17 00:00:00 2001 From: Birgit Pauli-Haack Date: Thu, 21 May 2026 22:58:02 +0200 Subject: [PATCH 05/23] Palette panel: align column headers with input text Replaces BaseControl.VisualLabel with plain styled spans so the Name and Slug column headers have consistent, predictable padding. Header labels are inset 12px to line up with the visible text inside the TextControl inputs below. Refs #838 --- .../edit-theme-settings-modal.js | 12 ++++++------ src/plugin-styles.scss | 19 +++++++++++++++++-- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/editor-sidebar/edit-theme-settings-modal.js b/src/editor-sidebar/edit-theme-settings-modal.js index dc9e38b8..eff61784 100644 --- a/src/editor-sidebar/edit-theme-settings-modal.js +++ b/src/editor-sidebar/edit-theme-settings-modal.js @@ -239,21 +239,21 @@ const PalettePanel = ( { value, onChange } ) => { spacing={ 3 } > - + { __( 'Slug', 'create-block-theme' ) } - +