Skip to content

Add/Edit Color Settings + Palette to color tab#845

Open
bph wants to merge 23 commits into
trunkfrom
add/edit-theme-settings-modal-color
Open

Add/Edit Color Settings + Palette to color tab#845
bph wants to merge 23 commits into
trunkfrom
add/edit-theme-settings-modal-color

Conversation

@bph
Copy link
Copy Markdown
Contributor

@bph bph commented May 21, 2026

Summary

Adds a new Edit Theme Settings entry to the Create Block Theme sidebar (cog icon, between Create Theme Variation and Edit Theme Metadata). Clicking it opens a modal with a single Color tab containing two panels:

  • Color Settings — toggles for default/custom presets (defaultPalette, defaultGradients, defaultDuotone, custom, customGradient, customDuotone) and the link color control.
  • Palette — full CRUD over settings.color.palette (add row, edit name and slug, pick color via ColorPicker, remove row), with Name / Slug column headers above the rows.

The modal owns the working state for both panels, seeded from getCurrentTheme().theme_json.settings.color. Clicking Update posts a partial-theme.json payload to POST /create-block-theme/v1/theme-settings (the endpoint that landed in #843) and reseeds the panels from the refreshed server state after a successful save. The modal stays open on success so the user can keep editing — theme.json-only edits do not need a page reload.

This is the foundation PR for the Edit Theme Settings feature. Gradients, Duotone, and the Dimensions / Typography / Shadows / Templates tabs land in follow-up PRs (#839#842).

Screenshots

(See the modal under: Site Editor → Create Block Theme sidebar → Edit Theme Settings)

Notable UX details

  • Update button shows Update (N changes) when there are pending edits, plain Update otherwise. Disabled when nothing has changed and shows a busy state during save.
  • Conditional warning notice — the disclaimer about Site-Editor-vs-theme.json conflicts is only shown when the active theme has user-level Global Styles customizations (saved record + in-editor edits). The notice lists which top-level slices are customized (e.g. "color, typography") so the user knows what would conflict.
  • Notices — success/error use snackbar notices via @wordpress/notices.

Test plan

  • Open the Site Editor on a block theme; click the CBT cog icon → "Edit Theme Settings" appears in the sidebar.
  • Open the modal; the Color tab renders with both Color Settings and Palette panels.
  • Toggle any Color Settings boolean → Update button reflects the change count → click Update → reload editor → toggle state is persisted in theme.json.
  • Add a new palette entry, edit name/slug, pick a color → Update → reload → entry is present in theme.json.
  • Remove a palette entry → Update → reload → entry is gone from theme.json.
  • With no user Global Styles customizations, the warning notice is hidden.
  • With user customizations present, the warning notice appears and lists the customized sections in bold.
  • Trigger a server error (e.g. block the endpoint) → error snackbar surfaces with the message; modal stays open.

Refs

Updates since open

  • Color Settings panel now uses a 2-column layout (Default / Custom) above $break-small, with per-toggle help copy; pushes the Palette panel above the fold.
  • Renamed the first Color tab section header from "Color Settings" → "Default and custom presets" (more descriptive, no redundancy with the surrounding tab name).
  • Fixed top alignment between the two Color Settings columns (was vertically centered, now top-aligned).
  • Sidebar: switched Edit Theme Metadata to the pencil icon so its row icon renders consistently with the other menu entries.

Further updates

  • Palette panel: dropped the redundant "Color presets" sub-label and replaced the icon-only + with a labeled "+ Add a color" tertiary button.
  • Color tab: added a Site-Editor-style Palette summary card at the top of the tab (overlapping swatches + "Edit palette →"). Clicking it expands the Palette accordion (now defaulted to closed when the palette is non-empty); clicking again scrolls the panel into view even when already open.
  • Empty palette: force the Palette accordion open so the "+ Add a color" entry stays discoverable; keep it open through the 0 → 1 transition so the first added row doesn't get hidden by the accordion collapsing.
  • Palette summary card: borderless and left-aligned with the swatches to reduce visual rule noise.
  • Palette: new rows scroll into view automatically after Add.
  • Sync with upstream polish direction: bumped palette-row remove icon to 20px, nudged the right-edge + Add a color to align with the PanelBody chevron, dropped the doubled border between the summary card and the first accordion.

Save / refresh cycle

  • After Update, reseed local state from the response's theme_json payload so the Update button resets immediately and the in-memory state matches what was just persisted.
  • View theme.json fetches /wp/v2/themes?status=active directly via apiFetch on every open, so closing the Edit Theme Settings modal and going straight to View theme.json shows the on-disk file, not the previous cached snapshot.

Review feedback addressed

Single commit covering the must-fix + should-fix items from review:

  • Duplicate slugs on Add → Remove → Add — fixed by deriving next index from max existing new-color-N suffix.
  • React keys — palette rows get a stable client-only __cbtRowId assigned at creation (stripped from the payload before send), so removing a middle row no longer leaks Dropdown/ColorPicker state.
  • Empty slug guard — Update is disabled (with explanatory tooltip) when any row has an empty slug.
  • Plural form — Update label uses _n() so Update (1 change) vs. Update (3 changes) pluralize correctly.
  • i18n separator — section list in the warning uses Intl.ListFormat for locale-aware joining (with , .join() fallback).
  • Minimal patch on save — only fields the user touched in this session are sent to the endpoint. Stops concurrent palette edits from being clobbered by an unrelated toggle save (the RFC 7396 list-replacement issue Codex flagged).
  • Reseed guard — refresh-effect skips while the user has unsaved edits, so mid-flight cache invalidations don't wipe in-progress work.
  • changeCount granularity — toggles count individually; palette counts as one unit (deliberate: reordering / per-row diffs would be a UX rabbit hole).
  • SCSS color vars#757575/#ddd/#fff replaced with $gray-700/$gray-300/$white from @wordpress/base-styles.
  • Silent catchapiFetch failures in View theme.json now log to console.

Deferred to follow-ups:

  • Different sidebar icon for "Edit Theme Settings" (currently cog, same as Editor Preferences) — aesthetic, not blocking.
  • requestAnimationFrame timing on the Edit-palette scroll — current build opens the PanelBody synchronously, no transition; revisit if a future Gutenberg version animates the accordion.
  • In-modal save confirmation beyond the snackbar — disagree that it's needed.
  • ETag / If-Match for the endpoint to fully solve concurrent palette edits — endpoint-side follow-up.

bph added 6 commits May 21, 2026 19:51
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
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
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
Wraps the comma-separated section list in a <strong> tag so the user's
eye lands on what's customized before reading the rest of the sentence.

Refs #838
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
The outer wrapper padding already matches the row Item's internal padding,
so the inner padding-left on labels was overshooting by ~12px and pushing
Name / Slug headers visibly right of the input text. Removing it lands
the labels closer to the input text edge.

Refs #838
@bph bph changed the title Add Edit Theme Settings modal — Color Settings + Palette (foundation) Add/Edit Color Settings + Palette (foundation) May 22, 2026
bph added 4 commits May 22, 2026 10:04
Switches the Color Settings panel from a single vertical stack of two
toggle groups to a CSS grid that places "Default presets" and "Custom
presets" side by side on modals wider than the WordPress $break-small
breakpoint (600px), collapsing to a single column below it.

Adds a short help string under each toggle so users understand the
effect without leaving the modal. The combination roughly halves the
panel's height on a typical modal width, pushing the Palette panel
above the fold.

Refs #838
CSS grid defaults to align-items: stretch which let the inner VStacks
vertically center their contents, producing visibly offset group headers
across the two columns. Forcing align-items: start lines up Default
Presets and Custom Presets headers (and their first toggles).

Refs #838
The previously-imported `edit` icon from @wordpress/icons was not
rendering visibly in the sidebar menu, leaving the Edit Theme Metadata
row icon-less and visually misaligned from the surrounding entries.
Switching to the `pencil` icon — the one the Site Editor uses elsewhere
for "edit metadata"-style actions — restores the icon and lines all
sidebar entries up.

Refs #838
The previous title ("Color Settings") was redundant with the surrounding
"Color" tab and didn't describe the panel's actual content. The new
title names exactly what the toggles control.

Refs #838
@bph bph changed the title Add/Edit Color Settings + Palette (foundation) Add/Edit Color Settings + Palette to color tab May 22, 2026
@bph bph added Edit Theme settings enhancement New feature or request labels May 22, 2026
bph added 7 commits May 22, 2026 12:34
The Palette PanelBody header already names the section, so the
duplicate "Color presets" sub-label was noise. Replaces it with a
right-aligned "+ Add a color" tertiary button that combines the
labeling and the add affordance into one element.

Refs #838
Adds a Site-Editor-style summary row at the top of the Color tab that
shows up to five overlapping swatches from the active palette plus a
right-aligned "Edit palette" button (with chevron). Clicking the
button expands the Palette accordion, which now defaults to closed and
is driven by controlled \`opened\` / \`onToggle\` state.

The Default and custom presets accordion stays default-open as before.

Refs #838
Previously, clicking "Edit palette" while the accordion was already
open did nothing (setState was a no-op so React skipped the render
that would have caused a scroll). Now the click handler always calls
scrollIntoView on a wrapper ref around the Palette PanelBody,
regardless of whether the accordion was open or closed.

Refs #838
Drops the border / rounded corners around the palette summary row and
left-aligns the "Edit palette" button so it sits right next to the
swatch stack instead of being pushed to the far right. Reduces visual
noise on a modal that already has enough horizontal rules.

Refs #838
Without a palette, the summary card is hidden and the Palette accordion
would otherwise default to closed — leaving the "Add a color" button
hidden behind it. Force the accordion open whenever \`palette.length\`
is zero so the empty-state add affordance is always visible.

Refs #838
Clicking "Add a color" appends a row to the bottom of the palette list,
which on a tall list ends up below the modal's visible area. Add an
effect that compares the row count against the previous render and
scrolls the new last row into view when it grows. Edits and removes do
not trigger a scroll.

Refs #838
When the palette was empty the accordion was force-open so the "Add a
color" button stayed visible. Adding a color flipped palette.length
from 0 to 1, the force-open condition stopped applying, and the
accordion collapsed — hiding the row that had just been added.

Track palette.length transitions and explicitly set isPaletteOpen=true
on the 0→1 jump so the accordion stays expanded after the first add.

Refs #838
bph added 5 commits May 22, 2026 16:18
Three minor visual tweaks aligned with the broader Edit Theme Settings
modal direction:

- Palette: bump remove-button iconSize to 20px so the minus icon
  matches the visual weight of the surrounding swatch button.
- Palette section header: nudge the right-edge action -8px so
  "+ Add a color" aligns with the PanelBody chevron in the accordion
  header directly above.
- Modal: drop the top border on the first PanelBody when it sits
  below a non-panel sibling (our PaletteSummary card), so the
  boundary doesn't double-up with the summary card's bottom edge.

Refs #838
The endpoint returns the merged theme.json on success. Use that
response directly to update colorSettings, palette, AND snapshot
instead of relying on invalidateResolution('getCurrentTheme') to
trigger a re-fetch.

Two problems with the previous approach:

- `getCurrentTheme` is entity-record-backed; invalidating it doesn't
  reliably re-fetch immediately, so the modal's reseed effect could
  fire late (or not at all in the same interaction), leaving the
  Update button showing pending changes even after a successful save.
- Other views (e.g. View theme.json) read through the same cached
  path and were similarly stuck on pre-save data, making it look as
  if booleans (Link color, etc.) hadn't persisted when in fact they
  had on disk.

Driving state directly from the response makes the dirty count drop
to 0 immediately on save, and the invalidate call is kept so other
callers eventually refresh.

Refs #838
The modal previously rendered the cached \`getCurrentTheme\` resolution
without re-resolving it, so writes made by the Edit Theme Settings flow
since the last fetch did not show up until the page was reloaded.

Invalidating the resolution on mount triggers a fresh fetch so the
modal always shows the on-disk theme.json.

Refs #838
The /theme-settings endpoint wraps the merged theme.json in
{ status, theme_json: <merged> }, but the client was reading
response.settings.color directly — which is always undefined. As a
result every save snapped the local state back to
COLOR_SETTINGS_DEFAULTS and cleared the palette in the UI (the on-disk
theme.json was correct; only the modal's in-memory state was wrong).

Most visible symptom: toggling Default gradients or Default duotone
off and clicking Update made the toggle pop right back to on. Link
color set to true behaved the same way (snapped back to false).

Refs #838
invalidateResolution alone wasn't enough: useSelect returns the cached
theme synchronously on mount and the modal renders that stale data
before the refetch lands. Replace useSelect with a direct
apiFetch('/wp/v2/themes?status=active') in a mount effect so the modal
always shows the on-disk theme.json.

Keep invalidateResolution so other subscribers eventually see fresh
data too.

Refs #838
@bph
Copy link
Copy Markdown
Contributor Author

bph commented May 22, 2026

Here is the latest video

Screen.Recording.2026-05-22.at.17.13.20.mov

@bph
Copy link
Copy Markdown
Contributor Author

bph commented May 22, 2026

Claude and Codex did a first pass at reviewing this PR.

Bugs / correctness:

- addColor: derive next index from the max existing `new-color-N` suffix
  instead of `value.length + 1`. Add → Remove → Add no longer produces
  duplicate slugs (P1).
- Palette rows: assign a stable client-only `__cbtRowId` at creation
  time and use it as the React key, so removing a middle row no longer
  leaks Dropdown/ColorPicker state to subsequent rows. The ID is
  stripped from the payload before posting.
- Update button: disable when any palette row has an empty slug; show
  a tooltip explaining why. Prevents the modal from POSTing an entry
  with `slug: ""`.

Concurrent edits / lost updates (Codex P2, reviewer #12, #13):

- Build a minimal patch on save: only the color-settings keys that
  differ from the snapshot, and the palette only if the user touched
  it. Stops unrelated toggles from clobbering a concurrent palette
  edit (RFC 7396 list-replacement) made elsewhere.
- Skip the post-refresh reseed effect while the user has unsaved
  edits, so a mid-flight cache invalidation doesn't silently wipe
  in-progress work.
- changeCount now reports `dirtyColorKeys.length + (paletteDirty ? 1
  : 0)` — toggles count individually, palette counts as one unit.
  Documented in the PR description.

i18n:

- Update label uses `_n( 'Update (%d change)', 'Update (%d changes)',
  count )` so single-change pluralization is correct.
- Section list in the warning notice uses `Intl.ListFormat` for
  locale-aware joining instead of a hard-coded English comma, with a
  plain `, `.join() fallback for environments without it.

SCSS / observability:

- Replace hard-coded `#757575`, `#ddd`, `#fff` with `$gray-700`,
  `$gray-300`, `$white` from `@wordpress/base-styles/_colors`.
- json-editor-modal: log fetch failures via `console.error` instead
  of swallowing them silently.

Refs #838
@bph bph marked this pull request as ready for review May 22, 2026 16:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant