diff --git a/docs/designers-developers/developers/themes/theme-json.md b/docs/designers-developers/developers/themes/theme-json.md
index e1a50ce95a428f..29d31d5ec3b3e9 100644
--- a/docs/designers-developers/developers/themes/theme-json.md
+++ b/docs/designers-developers/developers/themes/theme-json.md
@@ -163,6 +163,7 @@ The settings section has the following structure and default values:
"color": {
"custom": true, /* false to opt-out, as in add_theme_support('disable-custom-colors') */
"customGradient": true, /* false to opt-out, as in add_theme_support('disable-custom-gradients') */
+ "duotone": [ ... ], /* duotone presets, a list of { "colors": [ "#000", "#FFF" ] } */
"gradients": [ ... ], /* gradient presets, as in add_theme_support('editor-gradient-presets', ... ) */
"link": false, /* true to opt-in, as in add_theme_support('experimental-link-color') */
"palette": [ ... ], /* color presets, as in add_theme_support('editor-color-palette', ... ) */
diff --git a/lib/blocks.php b/lib/blocks.php
index 8fbc900d7ecb19..c7ec3e3c04d52b 100644
--- a/lib/blocks.php
+++ b/lib/blocks.php
@@ -26,7 +26,6 @@ function gutenberg_reregister_core_block_types() {
'group',
'heading',
'html',
- 'image',
'list',
'media-text',
'missing',
@@ -55,6 +54,7 @@ function gutenberg_reregister_core_block_types() {
'calendar.php' => 'core/calendar',
'categories.php' => 'core/categories',
'cover.php' => 'core/cover',
+ 'image.php' => 'core/image',
'latest-comments.php' => 'core/latest-comments',
'latest-posts.php' => 'core/latest-posts',
'navigation.php' => 'core/navigation',
diff --git a/lib/class-wp-theme-json-resolver.php b/lib/class-wp-theme-json-resolver.php
index 0352e9e9a44798..263a2f3847de59 100644
--- a/lib/class-wp-theme-json-resolver.php
+++ b/lib/class-wp-theme-json-resolver.php
@@ -255,6 +255,22 @@ public static function get_core_data() {
}
}
+ $default_duotone_i18n = array(
+ 'dark-grayscale' => __( 'Dark grayscale', 'gutenberg' ),
+ 'grayscale' => __( 'Grayscale', 'gutenberg' ),
+ 'purple-yellow' => __( 'Purple and yellow', 'gutenberg' ),
+ 'blue-red' => __( 'Blue and red', 'gutenberg' ),
+ 'midnight' => __( 'Midnight', 'gutenberg' ),
+ 'magenta-yellow' => __( 'Magenta and yellow', 'gutenberg' ),
+ 'purple-green' => __( 'Purple and green', 'gutenberg' ),
+ 'blue-orange' => __( 'Blue and orange', 'gutenberg' ),
+ );
+ if ( ! empty( $config['global']['settings']['color']['duotone'] ) ) {
+ foreach ( $config['global']['settings']['color']['duotone'] as &$gradient ) {
+ $gradient['name'] = $default_duotone_i18n[ $gradient['slug'] ];
+ }
+ }
+
$default_font_sizes_i18n = array(
'small' => __( 'Small', 'gutenberg' ),
'normal' => __( 'Normal', 'gutenberg' ),
diff --git a/lib/class-wp-theme-json.php b/lib/class-wp-theme-json.php
index 107ef3d3f6a757..becdacc066d05c 100644
--- a/lib/class-wp-theme-json.php
+++ b/lib/class-wp-theme-json.php
@@ -149,6 +149,7 @@ class WP_Theme_JSON {
'gradients' => null,
'link' => null,
'palette' => null,
+ 'duotone' => null,
),
'spacing' => array(
'customPadding' => null,
diff --git a/lib/duotone-filter.php b/lib/duotone-filter.php
new file mode 100644
index 00000000000000..ea3a3bf3bd970f
--- /dev/null
+++ b/lib/duotone-filter.php
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/packages/block-editor/src/components/duotone-toolbar/custom-duotone-bar.js b/packages/block-editor/src/components/duotone-toolbar/custom-duotone-bar.js
new file mode 100644
index 00000000000000..75410ca876b2e0
--- /dev/null
+++ b/packages/block-editor/src/components/duotone-toolbar/custom-duotone-bar.js
@@ -0,0 +1,45 @@
+/**
+ * WordPress dependencies
+ */
+import { __experimentalCustomGradientBar as CustomGradientBar } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import {
+ getColorStopsFromValues,
+ getCustomDuotoneIdFromColorStops,
+ getGradientFromValues,
+ getValuesFromColorStops,
+} from './utils';
+
+const PLACEHOLDER_VALUES = {
+ r: [ 0.2, 0.8 ],
+ g: [ 0.2, 0.8 ],
+ b: [ 0.2, 0.8 ],
+};
+
+export default function CustomDuotoneBar( { value, onChange } ) {
+ const hasGradient = !! value?.values;
+ const values = hasGradient ? value.values : PLACEHOLDER_VALUES;
+ const background = getGradientFromValues( values );
+ const controlPoints = getColorStopsFromValues( values );
+ return (
+
+ {
+ const newDuotone = {
+ id: getCustomDuotoneIdFromColorStops( newColorStops ),
+ values: getValuesFromColorStops( newColorStops ),
+ };
+ onChange( newDuotone );
+ } }
+ />
+
+ );
+}
diff --git a/packages/block-editor/src/components/duotone-toolbar/custom-duotone-picker.js b/packages/block-editor/src/components/duotone-toolbar/custom-duotone-picker.js
new file mode 100644
index 00000000000000..57343e5500eb10
--- /dev/null
+++ b/packages/block-editor/src/components/duotone-toolbar/custom-duotone-picker.js
@@ -0,0 +1,109 @@
+/**
+ * WordPress dependencies
+ */
+import { Button, ColorPalette, Icon } from '@wordpress/components';
+import { useMemo, useState } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import { swatch } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import Swatch from './duotone-swatch';
+import {
+ getCustomDuotoneIdFromHexColors,
+ getDefaultColors,
+ getHexColorsFromValues,
+ getValuesFromColors,
+} from './utils';
+
+function CustomColorOption( { label, value, colors, onChange } ) {
+ const [ isOpen, setIsOpen ] = useState( false );
+ const icon = value ? : ;
+ return (
+ <>
+
+ { isOpen && (
+
+ ) }
+ >
+ );
+}
+
+function CustomColorPicker( { colors, palette, onChange } ) {
+ const [ defaultDark, defaultLight ] = useMemo(
+ () => getDefaultColors( palette ),
+ [ palette ]
+ );
+
+ return (
+
+ {
+ const newColors = colors.slice();
+ newColors[ 0 ] = newColor;
+ if ( ! newColors[ 0 ] ) {
+ newColors[ 0 ] = defaultDark;
+ }
+ if ( ! newColors[ 1 ] ) {
+ newColors[ 1 ] = defaultLight;
+ }
+ onChange( newColors );
+ } }
+ />
+ {
+ const newColors = colors.slice();
+ newColors[ 1 ] = newColor;
+ if ( ! newColors[ 0 ] ) {
+ newColors[ 0 ] = defaultDark;
+ }
+ if ( ! newColors[ 1 ] ) {
+ newColors[ 1 ] = defaultLight;
+ }
+ onChange( newColors );
+ } }
+ />
+
+ );
+}
+
+function CustomDuotonePicker( { colorPalette, value, onChange } ) {
+ return (
+
+ onChange(
+ newColors.length >= 2
+ ? {
+ values: getValuesFromColors( newColors ),
+ id: getCustomDuotoneIdFromHexColors(
+ newColors
+ ),
+ }
+ : undefined
+ )
+ }
+ />
+ );
+}
+
+export default CustomDuotonePicker;
diff --git a/packages/block-editor/src/components/duotone-toolbar/duotone-picker-popover.js b/packages/block-editor/src/components/duotone-toolbar/duotone-picker-popover.js
new file mode 100644
index 00000000000000..1b4d66f56aded0
--- /dev/null
+++ b/packages/block-editor/src/components/duotone-toolbar/duotone-picker-popover.js
@@ -0,0 +1,100 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ CircularOptionPicker,
+ Popover,
+ MenuGroup,
+} from '@wordpress/components';
+import { __, sprintf } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import CustomDuotoneBar from './custom-duotone-bar';
+import CustomDuotonePicker from './custom-duotone-picker';
+import { getGradientFromCSSColors, getValuesFromColors } from './utils';
+
+function DuotonePickerPopover( {
+ value,
+ onChange,
+ onToggle,
+ duotonePalette,
+ colorPalette,
+} ) {
+ return (
+
+
+ {
+ const isSelected = option.slug === value?.slug;
+ const style = {
+ background: getGradientFromCSSColors(
+ option.colors,
+ '135deg'
+ ),
+ color: 'transparent',
+ };
+ const code = sprintf(
+ // translators: %s: duotone code e.g: "dark-grayscale" or "7f7f7f-ffffff".
+ __( 'Duotone code: %s' ),
+ option.slug
+ );
+ const label = sprintf(
+ // translators: %s: The name of the option e.g: "Dark grayscale".
+ __( 'Duotone: %s' ),
+ option.name
+ );
+
+ return (
+ {
+ const newValue = {
+ values: getValuesFromColors(
+ option.colors
+ ),
+ id: `duotone-filter-${ option.slug }`,
+ };
+ onChange(
+ isSelected ? undefined : newValue
+ );
+ } }
+ aria-label={ option.name ? label : code }
+ />
+ );
+ } ) }
+ actions={
+ onChange( undefined ) }
+ >
+ { __( 'Clear' ) }
+
+ }
+ >
+
+
+
+
+
+ { __(
+ 'The duotone filter creates a two-color version of your image, where you choose the colors.'
+ ) }
+
+
+ );
+}
+
+export default DuotonePickerPopover;
diff --git a/packages/block-editor/src/components/duotone-toolbar/duotone-swatch.js b/packages/block-editor/src/components/duotone-toolbar/duotone-swatch.js
new file mode 100644
index 00000000000000..64d5fcc26753a8
--- /dev/null
+++ b/packages/block-editor/src/components/duotone-toolbar/duotone-swatch.js
@@ -0,0 +1,10 @@
+function DuotoneSwatch( { fill } ) {
+ return (
+
+ );
+}
+
+export default DuotoneSwatch;
diff --git a/packages/block-editor/src/components/duotone-toolbar/index.js b/packages/block-editor/src/components/duotone-toolbar/index.js
new file mode 100644
index 00000000000000..3e0dfc663ecceb
--- /dev/null
+++ b/packages/block-editor/src/components/duotone-toolbar/index.js
@@ -0,0 +1,64 @@
+/**
+ * WordPress dependencies
+ */
+import { ToolbarButton } from '@wordpress/components';
+import { useState } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import { swatch } from '@wordpress/icons';
+import { DOWN } from '@wordpress/keycodes';
+
+/**
+ * Internal dependencies
+ */
+import DuotonePickerPopover from './duotone-picker-popover';
+import Swatch from './duotone-swatch';
+import { getGradientFromValues } from './utils';
+
+function DuotoneToolbar( { value, onChange, duotonePalette, colorPalette } ) {
+ const [ isOpen, setIsOpen ] = useState( false );
+ const onToggle = () => {
+ setIsOpen( ( prev ) => ! prev );
+ };
+ const openOnArrowDown = ( event ) => {
+ if ( ! isOpen && event.keyCode === DOWN ) {
+ event.preventDefault();
+ event.stopPropagation();
+ onToggle();
+ }
+ };
+ return (
+ <>
+
+ ) : (
+ swatch
+ )
+ }
+ />
+ { isOpen && (
+
+ ) }
+ >
+ );
+}
+
+export default DuotoneToolbar;
diff --git a/packages/block-editor/src/components/duotone-toolbar/style.scss b/packages/block-editor/src/components/duotone-toolbar/style.scss
new file mode 100644
index 00000000000000..1e0c44a4c49d0e
--- /dev/null
+++ b/packages/block-editor/src/components/duotone-toolbar/style.scss
@@ -0,0 +1,59 @@
+.block-editor-duotone-toolbar__swatch {
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ color: transparent;
+ background: transparent;
+
+ // Regular border doesn't seem to work in the toolbar button, but pseudo-selector border does.
+ &::after {
+ content: "";
+ display: block;
+ width: 100%;
+ height: 100%;
+ border: $border-width solid rgba(0, 0, 0, 0.2);
+ border-radius: 50%;
+ }
+}
+
+.components-button.has-icon.has-text .block-editor-duotone-toolbar__swatch {
+ margin-right: $grid-unit;
+}
+
+.block-editor-duotone-toolbar__popover {
+ .components-popover__content {
+ border: $border-width solid $gray-900;
+ min-width: 214px;
+ }
+
+ .components-circular-option-picker,
+ .block-editor-duotone-toolbar__description {
+ padding: $grid-unit-15;
+ }
+
+ .components-menu-group__label {
+ padding: $grid-unit-15 $grid-unit-15 0 $grid-unit-15;
+ }
+
+ .components-menu-group__label,
+ .block-editor-duotone-toolbar__custom-colors,
+ .block-editor-duotone-toolbar__color-button {
+ width: 100%;
+ }
+
+ .block-editor-duotone-toolbar__description {
+ border-top: $border-width solid $gray-900;
+ color: $gray-700;
+ }
+}
+
+.block-editor-duotone-toolbar__popover > .components-popover__content {
+ // Matches 8 swatches in width.
+ width: 334px;
+}
+
+// Better align the popover under the swatch.
+// @todo: when the positioning for popovers gets refactored, this can presumably be removed.
+.block-editor-duotone-toolbar__popover:not([data-y-axis="middle"][data-x-axis="right"]) > .components-popover__content {
+ margin-left: -14px;
+}
diff --git a/packages/block-editor/src/components/duotone-toolbar/utils.js b/packages/block-editor/src/components/duotone-toolbar/utils.js
new file mode 100644
index 00000000000000..2b5ed968f5686b
--- /dev/null
+++ b/packages/block-editor/src/components/duotone-toolbar/utils.js
@@ -0,0 +1,190 @@
+/**
+ * External dependencies
+ */
+import tinycolor from 'tinycolor2';
+
+/**
+ * Object representation for a color.
+ *
+ * @typedef {Object} RGBColor
+ * @property {number} r Red component of the color in the range [0,1].
+ * @property {number} g Green component of the color in the range [0,1].
+ * @property {number} b Blue component of the color in the range [0,1].
+ */
+
+/**
+ * Arrays of values in convenient format for SVG feComponentTransfer.
+ *
+ * @typedef {Object} RGBValues
+ * @property {number[]} r Array of red components of the colors in the range [0,1].
+ * @property {number[]} g Array of green components of the colors in the range [0,1].
+ * @property {number[]} b Array of blue components of the colors in the range [0,1].
+ */
+
+/**
+ * Calculate the brightest and darkest values from a color palette.
+ *
+ * @param {Object[]} palette Color palette for the theme.
+ *
+ * @return {string[]} Tuple of the darkest color and brightest color.
+ */
+export function getDefaultColors( palette ) {
+ // A default dark and light color are required.
+ if ( ! palette || palette.length < 2 ) return [ '#000', '#fff' ];
+
+ return palette
+ .map( ( { color } ) => ( {
+ color,
+ brightness: tinycolor( color ).getBrightness() / 255,
+ } ) )
+ .reduce(
+ ( [ min, max ], current ) => {
+ return [
+ current.brightness <= min.brightness ? current : min,
+ current.brightness >= max.brightness ? current : max,
+ ];
+ },
+ [ { brightness: 1 }, { brightness: 0 } ]
+ )
+ .map( ( { color } ) => color );
+}
+
+/**
+ * Generate a duotone gradient from a list of colors.
+ *
+ * @param {string[]} colors CSS color strings.
+ * @param {string} angle CSS gradient angle.
+ *
+ * @return {string} CSS gradient string for the duotone swatch.
+ */
+export function getGradientFromCSSColors( colors = [], angle = '90deg' ) {
+ const l = 100 / colors.length;
+
+ const stops = colors
+ .map( ( c, i ) => `${ c } ${ i * l }%, ${ c } ${ ( i + 1 ) * l }%` )
+ .join( ', ' );
+
+ return `linear-gradient( ${ angle }, ${ stops } )`;
+}
+
+/**
+ * Create a CSS gradient for duotone swatches.
+ *
+ * @param {RGBValues} values R, G, and B values.
+ * @param {string} angle CSS gradient angle.
+ *
+ * @return {string} CSS gradient string for the duotone swatch.
+ */
+export function getGradientFromValues(
+ values = { r: [], g: [], b: [] },
+ angle
+) {
+ return getGradientFromCSSColors(
+ getColorsFromValues( values ).map( ( tcolor ) => tcolor.toRgbString() ),
+ angle
+ );
+}
+
+/**
+ * Convert a list of colors to an object of R, G, and B values.
+ *
+ * @param {string[]} colors Array of RBG color strings.
+ *
+ * @return {RGBValues} R, G, and B values.
+ */
+export function getValuesFromColors( colors = [] ) {
+ const values = { r: [], g: [], b: [] };
+
+ colors.forEach( ( color ) => {
+ // Access values directly to skip rounding that tinycolor.toRgb() does.
+ const tcolor = tinycolor( color );
+ values.r.push( tcolor._r / 255 );
+ values.g.push( tcolor._g / 255 );
+ values.b.push( tcolor._b / 255 );
+ } );
+
+ return values;
+}
+
+/**
+ * Convert a color values object to an array of colors.
+ *
+ * @param {RGBValues} values R, G, and B values.
+ *
+ * @return {Object[]} Tinycolor object array.
+ */
+function getColorsFromValues( values = { r: [], g: [], b: [] } ) {
+ // R, G, and B should all be the same length, so we only need to map over one.
+ return values.r.map( ( x, i ) => {
+ return tinycolor( {
+ r: values.r[ i ] * 255,
+ g: values.g[ i ] * 255,
+ b: values.b[ i ] * 255,
+ } );
+ } );
+}
+
+/**
+ * Convert a color values object to an array of color stops.
+ *
+ * @param {RGBValues} values R, G, and B values.
+ *
+ * @return {Object[]} Color stop information.
+ */
+export function getColorStopsFromValues( values = { r: [], g: [], b: [] } ) {
+ const colors = getColorsFromValues( values );
+ return colors.map( ( tcolor, i ) => ( {
+ position: ( i * 100 ) / ( colors.length - 1 ),
+ color: tcolor.toRgbString(),
+ } ) );
+}
+
+/**
+ * Convert a color values object to an array of color stops.
+ *
+ * @param {Object[]} colorStops Color stop information.
+ *
+ * @return {RGBValues} R, G, and B values.
+ */
+export function getValuesFromColorStops( colorStops = [] ) {
+ return getValuesFromColors( colorStops.map( ( { color } ) => color ) );
+}
+
+/**
+ * Convert a color values object to an array of colors.
+ *
+ * @param {RGBValues} values R, G, and B values.
+ *
+ * @return {string[]} Hex color array.
+ */
+export function getHexColorsFromValues( values = { r: [], g: [], b: [] } ) {
+ return getColorsFromValues( values ).map( ( tcolor ) =>
+ tcolor.toHexString()
+ );
+}
+
+/**
+ * Gets a duotone id from custom hex colors.
+ *
+ * @param {string[]} colors Hex color array.
+ *
+ * @return {string} Custom duotone id.
+ */
+export function getCustomDuotoneIdFromHexColors( colors ) {
+ return `duotone-filter-custom-${ colors
+ .map( ( hex ) => hex.slice( 1 ).toLowerCase() )
+ .join( '-' ) }`;
+}
+
+/**
+ * Convert a color stops array to a custom duotone id.
+ *
+ * @param {Object[]} colorStops Color stop information.
+ *
+ * @return {string} Custom duotone id.
+ */
+export function getCustomDuotoneIdFromColorStops( colorStops = [] ) {
+ return getCustomDuotoneIdFromHexColors(
+ colorStops.map( ( { color } ) => tinycolor( color ).toHexString() )
+ );
+}
diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js
index 16977e34af4976..5d0416309eb4a9 100644
--- a/packages/block-editor/src/components/index.js
+++ b/packages/block-editor/src/components/index.js
@@ -31,6 +31,7 @@ export { default as ButtonBlockerAppender } from './button-block-appender';
export { default as ColorPalette } from './color-palette';
export { default as ColorPaletteControl } from './color-palette/control';
export { default as ContrastChecker } from './contrast-checker';
+export { default as __experimentalDuotoneToolbar } from './duotone-toolbar';
export { default as __experimentalGradientPicker } from './gradient-picker';
export { default as __experimentalGradientPickerControl } from './gradient-picker/control';
export { default as __experimentalGradientPickerPanel } from './gradient-picker/panel';
@@ -99,6 +100,7 @@ export {
useClipboardHandler as __unstableUseClipboardHandler,
} from './copy-handler';
export { default as DefaultBlockAppender } from './default-block-appender';
+export { default as __experimentalDuotoneFilter } from './duotone-filter';
export { default as __unstableEditorStyles } from './editor-styles';
export { default as Inserter } from './inserter';
export { default as __experimentalLibrary } from './inserter/library';
diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss
index 9469ed8cdf9096..fec56bc7683ea1 100644
--- a/packages/block-editor/src/style.scss
+++ b/packages/block-editor/src/style.scss
@@ -31,6 +31,7 @@
@import "./components/colors-gradients/style.scss";
@import "./components/contrast-checker/style.scss";
@import "./components/default-block-appender/style.scss";
+@import "./components/duotone-toolbar/style.scss";
@import "./components/font-appearance-control/style.scss";
@import "./components/justify-toolbar/style.scss";
@import "./components/link-control/style.scss";
diff --git a/packages/block-library/src/image/block.json b/packages/block-library/src/image/block.json
index 3952230ded6a31..dd6d93f0f43c0d 100644
--- a/packages/block-library/src/image/block.json
+++ b/packages/block-library/src/image/block.json
@@ -68,6 +68,9 @@
"source": "attribute",
"selector": "figure > a",
"attribute": "target"
+ },
+ "duotone": {
+ "type": "object"
}
},
"supports": {
diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js
index 15477e3e1eaca9..70e6be6279e656 100644
--- a/packages/block-library/src/image/edit.js
+++ b/packages/block-library/src/image/edit.js
@@ -91,6 +91,7 @@ export function ImageEdit( {
width,
height,
sizeSlug,
+ duotone,
} = attributes;
const altRef = useRef();
@@ -275,6 +276,7 @@ export function ImageEdit( {
);
const classes = classnames( className, {
+ [ duotone?.id ]: duotone,
'is-transient': isBlobURL( url ),
'is-resized': !! width || !! height,
'is-focused': isSelected,
diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js
index 543a5580216523..d7b0b7bb8a36fd 100644
--- a/packages/block-library/src/image/image.js
+++ b/packages/block-library/src/image/image.js
@@ -29,6 +29,9 @@ import {
MediaReplaceFlow,
store as blockEditorStore,
BlockAlignmentControl,
+ __experimentalUseEditorFeature as useEditorFeature,
+ __experimentalDuotoneToolbar as DuotoneToolbar,
+ __experimentalDuotoneFilter as DuotoneFilter,
} from '@wordpress/block-editor';
import { useEffect, useState, useRef } from '@wordpress/element';
import { __, sprintf, isRTL } from '@wordpress/i18n';
@@ -78,6 +81,7 @@ export default function Image( {
height,
linkTarget,
sizeSlug,
+ duotone,
},
setAttributes,
isSelected,
@@ -270,6 +274,10 @@ export default function Image( {
} );
}
+ function onDuotoneChange( newDuotone ) {
+ setAttributes( { duotone: newDuotone } );
+ }
+
useEffect( () => {
if ( ! isSelected ) {
setIsEditingImage( false );
@@ -279,6 +287,10 @@ export default function Image( {
const canEditImage = id && naturalWidth && naturalHeight && imageEditing;
const allowCrop = ! multiImageSelection && canEditImage && ! isEditingImage;
+ const duotonePalette = useEditorFeature( 'color.duotone' );
+
+ const colorPalette = useEditorFeature( 'color.palette' );
+
const controls = (
<>
@@ -306,6 +318,14 @@ export default function Image( {
label={ __( 'Crop' ) }
/>
) }
+ { ! isEditingImage && (
+
+ ) }
{ externalBlob && (
{ controls }
{ img }
+ { duotone && (
+
+ ) }
{ ( ! RichText.isEmpty( caption ) || isSelected ) && (
'render_block_core_image',
+ )
+ );
+}
+add_action( 'init', 'register_block_core_image' );
diff --git a/packages/block-library/src/image/save.js b/packages/block-library/src/image/save.js
index ccf816c121e909..9e20327117a89f 100644
--- a/packages/block-library/src/image/save.js
+++ b/packages/block-library/src/image/save.js
@@ -24,11 +24,13 @@ export default function save( { attributes } ) {
linkTarget,
sizeSlug,
title,
+ duotone,
} = attributes;
const newRel = isEmpty( rel ) ? undefined : rel;
const classes = classnames( {
+ [ duotone?.id ]: duotone,
[ `align${ align }` ]: align,
[ `size-${ sizeSlug }` ]: sizeSlug,
'is-resized': width || height,
diff --git a/packages/components/src/custom-gradient-bar/control-points.js b/packages/components/src/custom-gradient-bar/control-points.js
index d8d5ec16c2fa71..5bec42a2f2ae6c 100644
--- a/packages/components/src/custom-gradient-bar/control-points.js
+++ b/packages/components/src/custom-gradient-bar/control-points.js
@@ -109,6 +109,8 @@ function ControlPointButton( {
}
function ControlPoints( {
+ disableRemove,
+ disableAlpha,
gradientPickerDomRef,
ignoreMarkerPosition,
value: controlPoints,
@@ -223,6 +225,7 @@ function ControlPoints( {
renderContent={ ( { onClose } ) => (
<>
{
onChange(
@@ -234,21 +237,23 @@ function ControlPoints( {
);
} }
/>
-
+ { ! disableRemove && (
+
+ ) }
>
) }
popoverProps={ COLOR_POPOVER_PROPS }
@@ -264,6 +269,7 @@ function InsertPoint( {
onOpenInserter,
onCloseInserter,
insertPosition,
+ disableAlpha,
} ) {
const [ alreadyInsertedPoint, setAlreadyInsertedPoint ] = useState( false );
return (
@@ -297,6 +303,7 @@ function InsertPoint( {
) }
renderContent={ () => (
{
if ( ! alreadyInsertedPoint ) {
onChange(
diff --git a/packages/components/src/custom-gradient-bar/index.js b/packages/components/src/custom-gradient-bar/index.js
index bd8b1922cdf6e6..823e3d00992a32 100644
--- a/packages/components/src/custom-gradient-bar/index.js
+++ b/packages/components/src/custom-gradient-bar/index.js
@@ -76,6 +76,8 @@ export default function CustomGradientBar( {
hasGradient,
value: controlPoints,
onChange,
+ disableInserter = false,
+ disableAlpha = false,
} ) {
const gradientPickerDomRef = useRef();
@@ -129,24 +131,28 @@ export default function CustomGradientBar( {
onMouseLeave={ onMouseLeave }
>
- { ( isMovingInserter || isInsertingControlPoint ) && (
-
{
- gradientBarStateDispatch( {
- type: 'OPEN_INSERTER',
- } );
- } }
- onCloseInserter={ () => {
- gradientBarStateDispatch( {
- type: 'CLOSE_INSERTER',
- } );
- } }
- />
- ) }
+ { ! disableInserter &&
+ ( isMovingInserter || isInsertingControlPoint ) && (
+ {
+ gradientBarStateDispatch( {
+ type: 'OPEN_INSERTER',
+ } );
+ } }
+ onCloseInserter={ () => {
+ gradientBarStateDispatch( {
+ type: 'CLOSE_INSERTER',
+ } );
+ } }
+ />
+ ) }
+
+
+);
+
+export default swatch;