@plone/volto 18.0.0-alpha.35 → 18.0.0-alpha.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.release-it.json +1 -1
  2. package/CHANGELOG.md +32 -0
  3. package/locales/ca/LC_MESSAGES/volto.po +41 -4
  4. package/locales/ca.json +1 -1
  5. package/locales/de/LC_MESSAGES/volto.po +41 -4
  6. package/locales/de.json +1 -1
  7. package/locales/en/LC_MESSAGES/volto.po +41 -4
  8. package/locales/en.json +1 -1
  9. package/locales/es/LC_MESSAGES/volto.po +41 -4
  10. package/locales/es.json +1 -1
  11. package/locales/eu/LC_MESSAGES/volto.po +41 -4
  12. package/locales/eu.json +1 -1
  13. package/locales/fi/LC_MESSAGES/volto.po +44 -7
  14. package/locales/fi.json +1 -1
  15. package/locales/fr/LC_MESSAGES/volto.po +41 -4
  16. package/locales/fr.json +1 -1
  17. package/locales/hi/LC_MESSAGES/volto.po +41 -4
  18. package/locales/hi.json +1 -1
  19. package/locales/it/LC_MESSAGES/volto.po +41 -4
  20. package/locales/it.json +1 -1
  21. package/locales/ja/LC_MESSAGES/volto.po +41 -4
  22. package/locales/ja.json +1 -1
  23. package/locales/nl/LC_MESSAGES/volto.po +41 -4
  24. package/locales/nl.json +1 -1
  25. package/locales/pt/LC_MESSAGES/volto.po +41 -4
  26. package/locales/pt.json +1 -1
  27. package/locales/pt_BR/LC_MESSAGES/volto.po +41 -4
  28. package/locales/pt_BR.json +1 -1
  29. package/locales/ro/LC_MESSAGES/volto.po +41 -4
  30. package/locales/ro.json +1 -1
  31. package/locales/volto.pot +42 -5
  32. package/locales/zh_CN/LC_MESSAGES/volto.po +41 -4
  33. package/locales/zh_CN.json +1 -1
  34. package/package.json +3 -3
  35. package/src/components/manage/AnchorPlugin/components/LinkButton/AddLinkForm.jsx +78 -72
  36. package/src/components/manage/AnchorPlugin/useLinkEditor.js +79 -0
  37. package/src/components/manage/Blocks/Image/Edit.jsx +45 -347
  38. package/src/components/manage/Blocks/Teaser/Data.jsx +69 -1
  39. package/src/components/manage/Blocks/Teaser/schema.js +24 -3
  40. package/src/components/manage/Form/BlocksToolbar.jsx +8 -7
  41. package/src/components/manage/Form/Form.jsx +8 -1
  42. package/src/components/manage/Form/InlineForm.jsx +4 -0
  43. package/src/components/manage/Widgets/ImageWidget.jsx +311 -0
  44. package/src/components/manage/Widgets/ImageWidget.stories.jsx +46 -0
  45. package/src/config/NonContentRoutes.jsx +2 -1
  46. package/src/config/Widgets.jsx +2 -0
  47. package/src/helpers/Blocks/cloneBlocks.test.js +54 -0
  48. package/src/helpers/Blocks/cloneBlocks.ts +43 -0
  49. package/src/icons/external-link.svg +4 -0
  50. package/theme/themes/pastanaga/collections/form.overrides +4 -0
  51. package/theme/themes/pastanaga/elements/button.overrides +2 -1
  52. package/theme/themes/pastanaga/extras/blocks.less +40 -4
  53. package/theme/themes/pastanaga/extras/grid.less +3 -3
  54. package/theme/themes/pastanaga/extras/teaser.less +53 -0
  55. package/theme/themes/pastanaga/extras/widgets.less +108 -0
  56. package/types/components/manage/AnchorPlugin/useLinkEditor.d.ts +7 -0
  57. package/types/components/manage/Blocks/Teaser/schema.d.ts +9 -2
  58. package/types/components/manage/Widgets/ImageWidget.d.ts +10 -0
  59. package/types/components/manage/Widgets/ImageWidget.stories.d.ts +11 -0
  60. package/types/config/Widgets.d.ts +2 -0
  61. package/types/helpers/Blocks/cloneBlocks.d.ts +1 -0
  62. package/types/helpers/Blocks/cloneBlocks.test.d.ts +1 -0
  63. /package/src/components/manage/Blocks/Image/{schema.js → schema.jsx} +0 -0
  64. /package/src/components/theme/LanguageSelector/{LanguageSelector.js → LanguageSelector.jsx} +0 -0
  65. /package/src/helpers/Extensions/{withBlockExtensions.js → withBlockExtensions.jsx} +0 -0
  66. /package/src/helpers/Extensions/{withBlockSchemaEnhancer.js → withBlockSchemaEnhancer.jsx} +0 -0
  67. /package/src/helpers/FormValidation/{FormValidation.js → FormValidation.jsx} +0 -0
  68. /package/src/helpers/Helmet/{Helmet.js → Helmet.jsx} +0 -0
  69. /package/src/helpers/Loadable/{Loadable.js → Loadable.jsx} +0 -0
  70. /package/src/helpers/Loadable/__mocks__/{Loadable.js → Loadable.jsx} +0 -0
  71. /package/src/helpers/Utils/{Utils.js → Utils.jsx} +0 -0
@@ -0,0 +1,311 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Button, Dimmer, Loader, Message } from 'semantic-ui-react';
3
+ import { useIntl, defineMessages } from 'react-intl';
4
+ import { useDispatch } from 'react-redux';
5
+ import { useLocation } from 'react-router-dom';
6
+ import loadable from '@loadable/component';
7
+ import { connect } from 'react-redux';
8
+ import { compose } from 'redux';
9
+ import useLinkEditor from '@plone/volto/components/manage/AnchorPlugin/useLinkEditor';
10
+ import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser';
11
+
12
+ import {
13
+ flattenToAppURL,
14
+ getBaseUrl,
15
+ isInternalURL,
16
+ validateFileUploadSize,
17
+ } from '@plone/volto/helpers';
18
+ import { createContent } from '@plone/volto/actions';
19
+ import { readAsDataURL } from 'promise-file-reader';
20
+ import { FormFieldWrapper, Icon } from '@plone/volto/components';
21
+
22
+ import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg';
23
+ import clearSVG from '@plone/volto/icons/clear.svg';
24
+ import navTreeSVG from '@plone/volto/icons/nav.svg';
25
+ import linkSVG from '@plone/volto/icons/link.svg';
26
+ import uploadSVG from '@plone/volto/icons/upload.svg';
27
+
28
+ const Dropzone = loadable(() => import('react-dropzone'));
29
+
30
+ export const ImageToolbar = ({ className, data, id, onChange, selected }) => (
31
+ <div className="image-upload-widget-toolbar">
32
+ <Button.Group>
33
+ <Button icon basic onClick={() => onChange(id, null)}>
34
+ <Icon className="circled" name={clearSVG} size="24px" color="#e40166" />
35
+ </Button>
36
+ </Button.Group>
37
+ </div>
38
+ );
39
+
40
+ const messages = defineMessages({
41
+ addImage: {
42
+ id: 'Browse the site, drop an image, or type a URL',
43
+ defaultMessage: 'Browse the site, drop an image, or use a URL',
44
+ },
45
+ pickAnImage: {
46
+ id: 'pickAnImage',
47
+ defaultMessage: 'Pick an existing image',
48
+ },
49
+ uploadAnImage: {
50
+ id: 'uploadAnImage',
51
+ defaultMessage: 'Upload an image from your computer',
52
+ },
53
+ linkAnImage: {
54
+ id: 'linkAnImage',
55
+ defaultMessage: 'Enter a URL to an image',
56
+ },
57
+ uploadingImage: {
58
+ id: 'Uploading image',
59
+ defaultMessage: 'Uploading image',
60
+ },
61
+ });
62
+
63
+ const UnconnectedImageInput = (props) => {
64
+ const {
65
+ id,
66
+ onChange,
67
+ onFocus,
68
+ openObjectBrowser,
69
+ value,
70
+ imageSize = 'teaser',
71
+ selected = true,
72
+ hideLinkPicker = false,
73
+ hideObjectBrowserPicker = false,
74
+ restrictFileUpload = false,
75
+ objectBrowserPickerType = 'image',
76
+ description,
77
+ placeholderLinkInput = '',
78
+ } = props;
79
+
80
+ const intl = useIntl();
81
+ const linkEditor = useLinkEditor();
82
+ const location = useLocation();
83
+ const dispatch = useDispatch();
84
+ const contextUrl = location.pathname;
85
+
86
+ const [uploading, setUploading] = React.useState(false);
87
+ const [dragging, setDragging] = React.useState(false);
88
+
89
+ const imageUploadInputRef = useRef(null);
90
+
91
+ const requestId = `image-upload-${id}`;
92
+
93
+ const { loading, loaded } = props.request;
94
+ const { content } = props;
95
+ const imageId = content?.['@id'];
96
+ const image = content?.image;
97
+
98
+ useEffect(() => {
99
+ if (uploading && !loading && loaded) {
100
+ setUploading(false);
101
+ onChange(id, imageId, {
102
+ image_field: 'image',
103
+ image_scales: { image: [image] },
104
+ });
105
+ }
106
+ }, [loading, loaded, uploading, imageId, image, id, onChange]); // Explicitly list all dependencies
107
+
108
+ const handleUpload = React.useCallback(
109
+ (eventOrFile) => {
110
+ if (restrictFileUpload === true) return;
111
+ eventOrFile.target && eventOrFile.stopPropagation();
112
+
113
+ setUploading(true);
114
+ const file = eventOrFile.target
115
+ ? eventOrFile.target.files[0]
116
+ : eventOrFile[0];
117
+ if (!validateFileUploadSize(file, intl.formatMessage)) return;
118
+ readAsDataURL(file).then((fileData) => {
119
+ const fields = fileData.match(/^data:(.*);(.*),(.*)$/);
120
+ dispatch(
121
+ createContent(
122
+ getBaseUrl(contextUrl),
123
+ {
124
+ '@type': 'Image',
125
+ title: file.name,
126
+ image: {
127
+ data: fields[3],
128
+ encoding: fields[2],
129
+ 'content-type': fields[1],
130
+ filename: file.name,
131
+ },
132
+ },
133
+ props.block || requestId,
134
+ ),
135
+ );
136
+ });
137
+ },
138
+ [
139
+ restrictFileUpload,
140
+ intl.formatMessage,
141
+ dispatch,
142
+ props,
143
+ contextUrl,
144
+ requestId,
145
+ ],
146
+ );
147
+
148
+ const onDragEnter = React.useCallback(() => {
149
+ if (restrictFileUpload === false) setDragging(true);
150
+ }, [restrictFileUpload]);
151
+ const onDragLeave = React.useCallback(() => setDragging(false), []);
152
+
153
+ return value ? (
154
+ <div
155
+ className="image-upload-widget-image"
156
+ onClick={onFocus}
157
+ onKeyDown={onFocus}
158
+ role="toolbar"
159
+ >
160
+ {selected && <ImageToolbar {...props} />}
161
+ <img
162
+ className={props.className}
163
+ src={`${flattenToAppURL(value)}/@@images/image/${imageSize}`}
164
+ alt=""
165
+ />
166
+ </div>
167
+ ) : (
168
+ <div
169
+ className="image-upload-widget"
170
+ onClick={onFocus}
171
+ onKeyDown={onFocus}
172
+ role="toolbar"
173
+ >
174
+ <Dropzone
175
+ noClick
176
+ onDrop={handleUpload}
177
+ onDragEnter={onDragEnter}
178
+ onDragLeave={onDragLeave}
179
+ className="dropzone"
180
+ >
181
+ {({ getRootProps, getInputProps }) => (
182
+ <div {...getRootProps()}>
183
+ <Message>
184
+ {dragging && <Dimmer active></Dimmer>}
185
+ {uploading && (
186
+ <Dimmer active>
187
+ <Loader indeterminate>
188
+ {intl.formatMessage(messages.uploadingImage)}
189
+ </Loader>
190
+ </Dimmer>
191
+ )}
192
+ <img src={imageBlockSVG} alt="" className="placeholder" />
193
+ <p>{description || intl.formatMessage(messages.addImage)}</p>
194
+ <div className="toolbar-wrapper">
195
+ <div className="toolbar-inner" ref={linkEditor.anchorNode}>
196
+ {hideObjectBrowserPicker === false && (
197
+ <Button.Group>
198
+ <Button
199
+ aria-label={intl.formatMessage(messages.pickAnImage)}
200
+ icon
201
+ basic
202
+ onClick={(e) => {
203
+ onFocus && onFocus();
204
+ e.preventDefault();
205
+ openObjectBrowser({
206
+ mode: objectBrowserPickerType,
207
+ onSelectItem: (
208
+ url,
209
+ { title, image_field, image_scales },
210
+ ) => {
211
+ onChange(props.id, flattenToAppURL(url), {
212
+ title,
213
+ image_field,
214
+ image_scales,
215
+ });
216
+ },
217
+ currentPath: contextUrl,
218
+ });
219
+ }}
220
+ >
221
+ <Icon name={navTreeSVG} size="24px" />
222
+ </Button>
223
+ </Button.Group>
224
+ )}
225
+ {restrictFileUpload === false && (
226
+ <Button.Group>
227
+ <Button
228
+ aria-label={intl.formatMessage(messages.uploadAnImage)}
229
+ icon
230
+ basic
231
+ compact
232
+ onClick={() => {
233
+ imageUploadInputRef.current.click();
234
+ }}
235
+ >
236
+ <Icon name={uploadSVG} size="24px" />
237
+ </Button>
238
+ <input
239
+ {...getInputProps({
240
+ type: 'file',
241
+ ref: imageUploadInputRef,
242
+ onChange: handleUpload,
243
+ style: { display: 'none' },
244
+ })}
245
+ />
246
+ </Button.Group>
247
+ )}
248
+
249
+ {hideLinkPicker === false && (
250
+ <Button.Group>
251
+ <Button
252
+ icon
253
+ basic
254
+ aria-label={intl.formatMessage(messages.linkAnImage)}
255
+ onClick={(e) => {
256
+ !props.selected && onFocus && onFocus();
257
+ linkEditor.show();
258
+ }}
259
+ >
260
+ <Icon name={linkSVG} circled size="24px" />
261
+ </Button>
262
+ </Button.Group>
263
+ )}
264
+ </div>
265
+ {linkEditor.anchorNode && (
266
+ <linkEditor.LinkEditor
267
+ value={value}
268
+ placeholder={
269
+ placeholderLinkInput ||
270
+ intl.formatMessage(messages.linkAnImage)
271
+ }
272
+ objectBrowserPickerType={objectBrowserPickerType}
273
+ onChange={(_, e) =>
274
+ onChange(
275
+ props.id,
276
+ isInternalURL(e) ? flattenToAppURL(e) : e,
277
+ {},
278
+ )
279
+ }
280
+ id={id}
281
+ />
282
+ )}
283
+ </div>
284
+ </Message>
285
+ </div>
286
+ )}
287
+ </Dropzone>
288
+ </div>
289
+ );
290
+ };
291
+
292
+ export const ImageInput = compose(
293
+ connect(
294
+ (state, ownProps) => {
295
+ const requestId = `image-upload-${ownProps.id}`;
296
+ return {
297
+ request: state.content.subrequests[ownProps.block || requestId] || {},
298
+ content: state.content.subrequests[ownProps.block || requestId]?.data,
299
+ };
300
+ },
301
+ { createContent },
302
+ ),
303
+ )(withObjectBrowser(UnconnectedImageInput));
304
+
305
+ const ImageUploadWidget = (props) => (
306
+ <FormFieldWrapper {...props} className="image-upload-widget">
307
+ <ImageInput {...props} />
308
+ </FormFieldWrapper>
309
+ );
310
+
311
+ export default ImageUploadWidget;
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import ImageWidget from './ImageWidget';
3
+ import WidgetStory from './story';
4
+
5
+ export const Default = WidgetStory.bind({
6
+ props: { id: 'image_widget', title: 'Image', data: {}, properties: {} },
7
+ widget: ImageWidget,
8
+ });
9
+
10
+ export const Filled = WidgetStory.bind({
11
+ props: {
12
+ id: 'image_widget',
13
+ title: 'Image',
14
+ data: {},
15
+ properties: {},
16
+ value: 'halfdome2022.jpeg',
17
+ },
18
+ widget: ImageWidget,
19
+ });
20
+ Filled.args = {
21
+ value: 'halfdome2022.jpeg',
22
+ };
23
+
24
+ export const Inline = WidgetStory.bind({
25
+ props: {
26
+ id: 'image_widget',
27
+ title: 'Image',
28
+ data: {},
29
+ properties: {},
30
+ inline: true,
31
+ },
32
+ widget: ImageWidget,
33
+ });
34
+
35
+ export default {
36
+ title: 'Edit Widgets/Image',
37
+ component: ImageWidget,
38
+ decorators: [
39
+ (Story) => (
40
+ <div className="ui segment form attached" style={{ width: '400px' }}>
41
+ <Story />
42
+ </div>
43
+ ),
44
+ ],
45
+ argTypes: {},
46
+ };
@@ -9,7 +9,8 @@ export const nonContentRoutes = [
9
9
  '/aliases',
10
10
  '/contents',
11
11
  '/delete',
12
- '/diff',
12
+ /\/diff$/,
13
+ /\/diff\?/,
13
14
  /\/edit$/,
14
15
  '/historyview',
15
16
  '/links-to-item',
@@ -50,6 +50,7 @@ import TextViewWidget from '@plone/volto/components/theme/Widgets/TextWidget';
50
50
  import TitleViewWidget from '@plone/volto/components/theme/Widgets/TitleWidget';
51
51
  import TokenViewWidget from '@plone/volto/components/theme/Widgets/TokenWidget';
52
52
  import UrlViewWidget from '@plone/volto/components/theme/Widgets/UrlWidget';
53
+ import ImageWidget from '@plone/volto/components/manage/Widgets/ImageWidget';
53
54
 
54
55
  // Widgets mapping
55
56
  export const widgetMapping = {
@@ -68,6 +69,7 @@ export const widgetMapping = {
68
69
  date: DatetimeWidget,
69
70
  password: PasswordWidget,
70
71
  file: FileWidget,
72
+ image: ImageWidget,
71
73
  align: AlignWidget,
72
74
  buttons: ButtonsWidget,
73
75
  url: UrlWidget,
@@ -0,0 +1,54 @@
1
+ import { cloneDeep } from 'lodash';
2
+ import { cloneBlocks } from './cloneBlocks';
3
+
4
+ describe('cloneBlocks', () => {
5
+ it('cloneBlocks - basic', () => {
6
+ const blocksData = {
7
+ blocks: {
8
+ a: { '@type': 'slate', value: 1 },
9
+ b: { '@type': 'slate', value: 2 },
10
+ },
11
+ blocks_layout: { items: ['a', 'b'] },
12
+ };
13
+ const result = cloneBlocks(blocksData);
14
+
15
+ expect(result.blocks_layout.items[0]).not.toBe('a');
16
+ expect(result.blocks[result.blocks_layout.items[0]]).toStrictEqual({
17
+ '@type': 'slate',
18
+ value: 1,
19
+ });
20
+ });
21
+
22
+ it('cloneBlocks - nested', () => {
23
+ const blocksData = {
24
+ blocks: {
25
+ a: { '@type': 'slate', value: 1 },
26
+ b: { '@type': 'slate', value: 2 },
27
+ },
28
+ blocks_layout: { items: ['a', 'b'] },
29
+ };
30
+ blocksData.blocks.c = cloneDeep(blocksData);
31
+ blocksData.blocks_layout.items.push('c');
32
+
33
+ const result = cloneBlocks(blocksData);
34
+
35
+ expect(result.blocks_layout.items[0]).not.toBe('a');
36
+ expect(result.blocks[result.blocks_layout.items[0]]).toStrictEqual({
37
+ '@type': 'slate',
38
+ value: 1,
39
+ });
40
+
41
+ expect(
42
+ result.blocks[result.blocks_layout.items[2]].blocks_layout.items[0],
43
+ ).not.toBe('c');
44
+
45
+ expect(
46
+ result.blocks[result.blocks_layout.items[2]].blocks[
47
+ result.blocks[result.blocks_layout.items[2]].blocks_layout.items[0]
48
+ ],
49
+ ).toStrictEqual({
50
+ '@type': 'slate',
51
+ value: 1,
52
+ });
53
+ });
54
+ });
@@ -0,0 +1,43 @@
1
+ // @ts-nocheck
2
+ import { v4 as uuid } from 'uuid';
3
+ import {
4
+ getBlocksFieldname,
5
+ getBlocksLayoutFieldname,
6
+ hasBlocksData,
7
+ } from './Blocks';
8
+ import config from '@plone/registry';
9
+
10
+ export function cloneBlocks(blocksData) {
11
+ if (hasBlocksData(blocksData)) {
12
+ const blocksFieldname = getBlocksFieldname(blocksData);
13
+ const blocksLayoutFieldname = getBlocksLayoutFieldname(blocksData);
14
+
15
+ const cloneWithIds = Object.keys(blocksData.blocks)
16
+ .map((key) => {
17
+ const block = blocksData.blocks[key];
18
+ const blockConfig = config.blocks.blocksConfig[blocksData['@type']];
19
+ return blockConfig?.cloneData
20
+ ? blockConfig.cloneData(block)
21
+ : [uuid(), cloneBlocks(block)];
22
+ })
23
+ .filter((info) => !!info); // some blocks may refuse to be copied
24
+
25
+ const newBlockData = {
26
+ ...blocksData,
27
+ [blocksFieldname]: {
28
+ ...Object.assign(
29
+ {},
30
+ ...cloneWithIds.map(([id, data]) => ({ [id]: data })),
31
+ ),
32
+ },
33
+ [blocksLayoutFieldname]: {
34
+ ...blocksData[blocksLayoutFieldname],
35
+ items: [...cloneWithIds.map(([id]) => id)],
36
+ },
37
+ };
38
+
39
+ return newBlockData;
40
+ }
41
+
42
+ return blocksData;
43
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-box-arrow-up-right" viewBox="0 0 16 16">
2
+ <path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z"/>
3
+ <path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z"/>
4
+ </svg>
@@ -6,6 +6,10 @@
6
6
  margin: 0;
7
7
  }
8
8
 
9
+ .ui.form .inline.field.overwritten input {
10
+ background-color: rgb(#edc3c2);
11
+ }
12
+
9
13
  .ui.form .inline.field.help .wrapper {
10
14
  border-bottom: none;
11
15
  }
@@ -26,7 +26,8 @@
26
26
  }
27
27
  }
28
28
 
29
- #main {
29
+ #main,
30
+ .slate-inline-toolbar.slate-toolbar {
30
31
  .ui.basic.buttons .button,
31
32
  .ui.basic.button {
32
33
  -webkit-box-shadow: 0px 0px 0px @basicBorderSize transparent inset !important;
@@ -401,7 +401,6 @@ body.has-toolbar.has-sidebar-collapsed .ui.wrapper > .ui.inner.block.full {
401
401
  padding: 4px;
402
402
  border-radius: 1px;
403
403
  margin-left: 4px;
404
- color: @brown !important;
405
404
 
406
405
  &:hover {
407
406
  background-color: #edf1f2 !important;
@@ -414,13 +413,50 @@ body.has-toolbar.has-sidebar-collapsed .ui.wrapper > .ui.inner.block.full {
414
413
  }
415
414
  }
416
415
  }
416
+ }
417
+
418
+ .toolbar-buttons {
419
+ // used by the ImageUploadWidget
420
+ display: flex;
421
+ justify-content: center;
422
+ gap: 4px;
423
+ }
424
+
425
+ .image-upload-widget-toolbar {
426
+ position: absolute;
427
+ top: 1em;
428
+ right: 1em;
429
+
430
+ .icon.circled {
431
+ border: 1px solid #e4e8ec;
432
+ background: #f3f5f7;
433
+ box-shadow: inset 0 0 0 0.14285714rem transparent !important;
434
+ }
435
+ }
436
+
437
+ div.image-upload-widget-image {
438
+ position: relative;
439
+
440
+ img {
441
+ max-width: 100% !important;
442
+ }
443
+ }
444
+
445
+ .image-upload-widget {
446
+ text-align: center;
417
447
 
418
- .ui.active.basic.icon.button {
419
- border: 1px solid #2996da;
420
- background: transparent !important;
448
+ .toolbar-wrapper {
449
+ display: flex;
450
+ flex-direction: column;
451
+ justify-content: flex-end;
421
452
  }
422
453
  }
423
454
 
455
+ .ui.active.basic.icon.button {
456
+ border: 1px solid #2996da;
457
+ background: transparent !important;
458
+ }
459
+
424
460
  // Hero block
425
461
  .block.hero {
426
462
  clear: both;
@@ -396,9 +396,9 @@ body.has-toolbar.has-sidebar .drag.gridBlock.handle.wrapper {
396
396
 
397
397
  // Aspect ratio on all images
398
398
  .grid-image-wrapper img,
399
- .gridBlock .block.image img,
400
- .block-editor-gridBlock .block-editor-image img,
401
- .block-editor-image.contained img {
399
+ .gridBlock .block.image img:not(.placeholder),
400
+ .block-editor-gridBlock .block-editor-image img:not(.placeholder),
401
+ .block-editor-image.contained img:not(.placeholder) {
402
402
  width: 100%;
403
403
  aspect-ratio: @grid-images-aspect-ratio;
404
404
  object-fit: cover;
@@ -164,3 +164,56 @@
164
164
  }
165
165
  }
166
166
  }
167
+
168
+ #sidebar-properties .ui.form {
169
+ .teaser.block.overwrite.actions {
170
+ display: flex;
171
+ flex-direction: column;
172
+
173
+ > div {
174
+ display: flex;
175
+ justify-content: space-between;
176
+
177
+ .ui.label {
178
+ padding-left: 0;
179
+ background: unset;
180
+ font-size: 15px;
181
+ text-transform: capitalize;
182
+ }
183
+ }
184
+
185
+ h3 {
186
+ text-transform: capitalize;
187
+ }
188
+
189
+ .ui.button:hover {
190
+ background: none !important;
191
+ }
192
+ }
193
+
194
+ .ui.message.warning:has(.teaser.block.overwrite.actions) {
195
+ display: block;
196
+ }
197
+ }
198
+
199
+ #sidebar-properties .ui.segment.attached.actions {
200
+ display: flex;
201
+ justify-content: center;
202
+ padding: 1.5em 1em;
203
+ border-top: none;
204
+
205
+ .ui.buttons.refresh.teaser button {
206
+ display: flex;
207
+ align-items: center;
208
+ padding: 0.5833em 0.833em;
209
+ background-color: #e8e8e8 !important;
210
+ color: rgba(0, 0, 0, 0.6) !important;
211
+ font-size: 0.85714286rem;
212
+ font-weight: bold;
213
+
214
+ svg {
215
+ padding: 0 0 0 0.5em;
216
+ margin-right: 0;
217
+ }
218
+ }
219
+ }