@plone-collective/volto-image-editor 1.0.0-alpha.1

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 (57) hide show
  1. package/.changelog.draft +19 -0
  2. package/.release-it.json +34 -0
  3. package/CHANGELOG.md +27 -0
  4. package/README.md +209 -0
  5. package/babel.config.js +17 -0
  6. package/locales/de/LC_MESSAGES/volto.po +187 -0
  7. package/locales/en/LC_MESSAGES/volto.po +187 -0
  8. package/locales/es/LC_MESSAGES/volto.po +194 -0
  9. package/locales/pt_BR/LC_MESSAGES/volto.po +192 -0
  10. package/locales/volto.pot +189 -0
  11. package/news/.gitkeep +0 -0
  12. package/package.json +50 -0
  13. package/public/.gitkeep +0 -0
  14. package/src/components/.gitkeep +0 -0
  15. package/src/components/ImageEditor/ImageEditor.scss +59 -0
  16. package/src/components/ImageEditor/ImageEditor.tsx +290 -0
  17. package/src/components/ImageEditor/ImageEditorWrapper.scss +13 -0
  18. package/src/components/ImageEditor/ImageEditorWrapper.tsx +91 -0
  19. package/src/components/ImageEditor/components/AdjustableCropperBackground.tsx +60 -0
  20. package/src/components/ImageEditor/components/AdjustableImage.scss +3 -0
  21. package/src/components/ImageEditor/components/AdjustableImage.tsx +85 -0
  22. package/src/components/ImageEditor/components/AdjustablePreviewBackground.tsx +59 -0
  23. package/src/components/ImageEditor/components/Button.scss +30 -0
  24. package/src/components/ImageEditor/components/Button.tsx +27 -0
  25. package/src/components/ImageEditor/components/Navigation.scss +38 -0
  26. package/src/components/ImageEditor/components/Navigation.tsx +153 -0
  27. package/src/components/ImageEditor/components/SettingsModal.scss +369 -0
  28. package/src/components/ImageEditor/components/SettingsModal.tsx +582 -0
  29. package/src/components/ImageEditor/components/Slider.scss +231 -0
  30. package/src/components/ImageEditor/components/Slider.tsx +322 -0
  31. package/src/components/ImageEditor/types/ImageSettings.ts +23 -0
  32. package/src/components/Widgets/FileWidget.scss +14 -0
  33. package/src/components/Widgets/FileWidget.tsx +239 -0
  34. package/src/config/settings.ts +5 -0
  35. package/src/customizations/volto/components/manage/Widgets/FileWidget.jsx +3 -0
  36. package/src/icons/AspectRatioIcons.tsx +39 -0
  37. package/src/icons/BrightnessIcon.tsx +19 -0
  38. package/src/icons/ContrastIcon.tsx +19 -0
  39. package/src/icons/CropIcon.tsx +19 -0
  40. package/src/icons/DownloadIcon.tsx +20 -0
  41. package/src/icons/FlipHorizontalIcon.tsx +21 -0
  42. package/src/icons/FlipVerticalIcon.tsx +27 -0
  43. package/src/icons/HueIcon.tsx +20 -0
  44. package/src/icons/ResetIcon.tsx +19 -0
  45. package/src/icons/ResetRotationIcon.tsx +20 -0
  46. package/src/icons/RestrictionIcons.tsx +19 -0
  47. package/src/icons/RotateLeftIcon.tsx +21 -0
  48. package/src/icons/RotateRightIcon.tsx +21 -0
  49. package/src/icons/SaturationIcon.tsx +20 -0
  50. package/src/icons/SettingsIcon.tsx +21 -0
  51. package/src/icons/StencilIcons.tsx +8 -0
  52. package/src/index.ts +10 -0
  53. package/src/theme/constants.scss +13 -0
  54. package/src/theme/custom.css +82 -0
  55. package/src/theme/mixins.scss +91 -0
  56. package/towncrier.toml +33 -0
  57. package/tsconfig.json +30 -0
@@ -0,0 +1,239 @@
1
+ /**
2
+ * FileWidget component.
3
+ * @module components/manage/Widgets/FileWidget
4
+ */
5
+
6
+ import React from 'react';
7
+ import { Button, Dimmer, Grid } from 'semantic-ui-react';
8
+ import { readAsDataURL } from 'promise-file-reader';
9
+ import { injectIntl } from 'react-intl';
10
+ import deleteSVG from '@plone/volto/icons/delete.svg';
11
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
12
+ import Toast from '@plone/volto/components/manage/Toast/Toast';
13
+ import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink';
14
+ import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
15
+ import loadable from '@loadable/component';
16
+ import { validateFileUploadSize } from '@plone/volto/helpers/FormValidation/FormValidation';
17
+ import { defineMessages, useIntl } from 'react-intl';
18
+ import Image from '@plone/volto/components/theme/Image/Image';
19
+ import { toast } from 'react-toastify';
20
+ import ImageEditorWrapper from '../ImageEditor/ImageEditorWrapper';
21
+ import './FileWidget.scss';
22
+ import { useUpdateEffect } from 'react-advanced-cropper';
23
+
24
+ const imageMimetypes = [
25
+ 'image/png',
26
+ 'image/jpeg',
27
+ 'image/webp',
28
+ 'image/jpg',
29
+ 'image/gif',
30
+ 'image/svg+xml',
31
+ ];
32
+ const Dropzone = loadable(() => import('react-dropzone'));
33
+
34
+ const messages = defineMessages({
35
+ releaseDrag: {
36
+ id: 'Drop files here ...',
37
+ defaultMessage: 'Drop files here ...',
38
+ },
39
+ editFile: {
40
+ id: 'Drop file here to replace the existing file',
41
+ defaultMessage: 'Drop file here to replace the existing file',
42
+ },
43
+ fileDrag: {
44
+ id: 'Drop file here to upload a new file',
45
+ defaultMessage: 'Drop file here to upload a new file',
46
+ },
47
+ replaceFile: {
48
+ id: 'Replace existing file',
49
+ defaultMessage: 'Replace existing file',
50
+ },
51
+ addNewFile: {
52
+ id: 'Choose a file',
53
+ defaultMessage: 'Choose a file',
54
+ },
55
+ maxSizeError: {
56
+ id: 'The file you uploaded exceeded the maximum allowed size of {size} bytes',
57
+ defaultMessage:
58
+ 'The file you uploaded exceeded the maximum allowed size of {size} bytes',
59
+ },
60
+ acceptError: {
61
+ id: 'File is not of the accepted type {accept}',
62
+ defaultMessage: 'File is not of the accepted type {accept}',
63
+ },
64
+ });
65
+
66
+ const FileWidget = (props) => {
67
+ const { id, value, onChange, isDisabled } = props;
68
+ const [data, setData] = React.useState(null);
69
+ const isImage = value && imageMimetypes.includes(value['content-type']);
70
+ const mainColumnWidth = isImage ? 10 : 12;
71
+ const intl = useIntl();
72
+
73
+ const imgAttrs = React.useMemo(() => {
74
+ const imgObj = {};
75
+ if (value?.download) {
76
+ imgObj.item = {
77
+ '@id': value.download.substring(0, value.download.indexOf('/@@images')),
78
+ image: value,
79
+ };
80
+ } else if (value?.data) {
81
+ imgObj.src = `data:${value['content-type']};${value.encoding},${value.data}`;
82
+ }
83
+ return imgObj;
84
+ }, [value]);
85
+
86
+ useUpdateEffect(() => {
87
+ if (data) {
88
+ onChange(id, data);
89
+ }
90
+ }, [data]);
91
+
92
+ /**
93
+ * Drop handler
94
+ * @method onDrop
95
+ * @param {array} files File objects
96
+ * @returns {undefined}
97
+ */
98
+ const onDrop = (files, rejectedFiles) => {
99
+ rejectedFiles.forEach((file) => {
100
+ file.errors.forEach((err) => {
101
+ if (err.code === 'file-too-large') {
102
+ toast.error(
103
+ <Toast
104
+ error
105
+ title={intl.formatMessage(messages.maxSizeError, {
106
+ size: props.size,
107
+ })}
108
+ />,
109
+ );
110
+ }
111
+
112
+ if (err.code === 'file-invalid-type') {
113
+ toast.error(
114
+ <Toast
115
+ error
116
+ title={intl.formatMessage(messages.acceptError, {
117
+ accept: props.accept,
118
+ })}
119
+ />,
120
+ );
121
+ }
122
+ });
123
+ });
124
+ if (files.length < 1) return;
125
+ const file = files[0];
126
+ if (!validateFileUploadSize(file, intl.formatMessage)) return;
127
+ readAsDataURL(file).then((data) => {
128
+ const fields = data.match(/^data:(.*);(.*),(.*)$/);
129
+ onChange(id, {
130
+ data: fields[3],
131
+ encoding: fields[2],
132
+ 'content-type': fields[1],
133
+ filename: file.name,
134
+ });
135
+ });
136
+
137
+ let reader = new FileReader();
138
+ reader.onload = function () {
139
+ const fields = reader.result.match(/^data:(.*);(.*),(.*)$/);
140
+ if (imageMimetypes.includes(fields[1])) {
141
+ let imagePreview = document.getElementById(`field-${id}-image`);
142
+ if (imagePreview) imagePreview.src = reader.result;
143
+ }
144
+ };
145
+ reader.readAsDataURL(files[0]);
146
+ };
147
+
148
+ return (
149
+ <FormFieldWrapper {...props}>
150
+ <Grid className="image-widget-grid">
151
+ <Grid.Row stretched>
152
+ <Grid.Column stretched width={mainColumnWidth}>
153
+ <Dropzone
154
+ onDrop={onDrop}
155
+ {...(props.size ? { maxSize: props.size } : {})}
156
+ {...(props.accept ? { accept: props.accept } : {})}
157
+ >
158
+ {({ getRootProps, getInputProps, isDragActive }) => (
159
+ <div className="file-widget-dropzone" {...getRootProps()}>
160
+ {isDragActive && <Dimmer active></Dimmer>}
161
+ {isImage ? (
162
+ <Image
163
+ className="image-preview small ui image"
164
+ id={`field-${id}-image`}
165
+ {...imgAttrs}
166
+ />
167
+ ) : (
168
+ <div className="dropzone-placeholder">
169
+ {isDragActive ? (
170
+ <p className="dropzone-text">
171
+ {intl.formatMessage(messages.releaseDrag)}
172
+ </p>
173
+ ) : value ? (
174
+ <p className="dropzone-text">
175
+ {intl.formatMessage(messages.editFile)}
176
+ </p>
177
+ ) : (
178
+ <p className="dropzone-text">
179
+ {intl.formatMessage(messages.fileDrag)}
180
+ </p>
181
+ )}
182
+ </div>
183
+ )}
184
+
185
+ <label className="label-file-widget-input">
186
+ {value
187
+ ? intl.formatMessage(messages.replaceFile)
188
+ : intl.formatMessage(messages.addNewFile)}
189
+ </label>
190
+ <input
191
+ {...getInputProps({
192
+ type: 'file',
193
+ style: { display: 'none' },
194
+ })}
195
+ id={`field-${id}`}
196
+ name={id}
197
+ type="file"
198
+ disabled={isDisabled}
199
+ />
200
+ </div>
201
+ )}
202
+ </Dropzone>
203
+ <div className="field-file-name">
204
+ {value && (
205
+ <UniversalLink href={value.download} download={true}>
206
+ {value.filename}
207
+ </UniversalLink>
208
+ )}
209
+ {value && (
210
+ <>
211
+ <Button
212
+ type="button"
213
+ icon
214
+ basic
215
+ className="delete-button"
216
+ aria-label="delete file"
217
+ disabled={isDisabled}
218
+ onClick={() => {
219
+ onChange(id, null);
220
+ }}
221
+ >
222
+ <Icon name={deleteSVG} size="20px" />
223
+ </Button>
224
+ </>
225
+ )}
226
+ </div>
227
+ </Grid.Column>
228
+ {isImage && (
229
+ <Grid.Column stretched width="2" className={'image-editor-cta'}>
230
+ <ImageEditorWrapper value={value} setData={setData} />
231
+ </Grid.Column>
232
+ )}
233
+ </Grid.Row>
234
+ </Grid>
235
+ </FormFieldWrapper>
236
+ );
237
+ };
238
+
239
+ export default injectIntl(FileWidget);
@@ -0,0 +1,5 @@
1
+ import type { ConfigType } from '@plone/registry';
2
+
3
+ export default function install(config: ConfigType) {
4
+ return config;
5
+ }
@@ -0,0 +1,3 @@
1
+ import FileWidget from '@plone-collective/volto-image-editor/components/Widgets/FileWidget';
2
+
3
+ export default FileWidget;
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import {
3
+ TbAspectRatio,
4
+ TbSquare,
5
+ TbRectangleVertical,
6
+ TbRectangle,
7
+ TbPhoto,
8
+ TbDeviceDesktop,
9
+ TbDeviceTv,
10
+ TbDeviceMobile,
11
+ TbRectangleFilled,
12
+ } from 'react-icons/tb';
13
+
14
+ // Free aspect ratio icon
15
+ export const FreeIcon = () => <TbAspectRatio />;
16
+
17
+ // Square 1:1
18
+ export const SquareIcon = () => <TbSquare />;
19
+
20
+ // Portrait 1:2
21
+ export const PortraitIcon = () => <TbRectangleVertical />;
22
+
23
+ // Landscape 2:1
24
+ export const LandscapeIcon = () => <TbRectangle />;
25
+
26
+ // Photo 3:4
27
+ export const PhotoIcon = () => <TbPhoto />;
28
+
29
+ // Monitor 4:3
30
+ export const MonitorIcon = () => <TbDeviceDesktop />;
31
+
32
+ // Widescreen 16:9
33
+ export const WidescreenIcon = () => <TbDeviceTv />;
34
+
35
+ // Mobile 9:16
36
+ export const MobileIcon = () => <TbDeviceMobile />;
37
+
38
+ // Ultra-wide 21:9
39
+ export const UltraWideIcon = () => <TbRectangleFilled />;
@@ -0,0 +1,19 @@
1
+ import React, { FC } from 'react';
2
+
3
+ interface Props {
4
+ className?: string;
5
+ }
6
+
7
+ export const BrightnessIcon: FC<Props> = ({ className }) => {
8
+ return (
9
+ <svg
10
+ className={className}
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ width="24"
13
+ height="24"
14
+ xmlSpace="preserve"
15
+ >
16
+ <path d="M12 15.3c-1.8 0-3.3-1.5-3.3-3.3 0-1.8 1.5-3.3 3.3-3.3 1.8 0 3.3 1.5 3.3 3.3 0 1.8-1.5 3.3-3.3 3.3zm0-5.1c-1 0-1.8.8-1.8 1.8s.8 1.8 1.8 1.8 1.8-.8 1.8-1.8-.8-1.8-1.8-1.8zM12 7.7c-.4 0-.8-.3-.8-.8V5.2c0-.4.3-.8.8-.8s.8.3.8.8V7c0 .4-.4.7-.8.7zM9 9c-.3.3-.8.3-1.1 0L6.6 7.7c-.3-.3-.3-.8 0-1.1.3-.3.8-.3 1.1 0L9 7.9c.3.3.3.8 0 1.1zM7.7 12c0 .4-.3.8-.8.8H5.2c-.4 0-.8-.3-.8-.8s.3-.8.8-.8H7c.4.1.7.4.7.8zM9 15c.3.3.3.8 0 1.1l-1.3 1.3c-.3.3-.8.3-1.1 0-.3-.3-.3-.8 0-1.1L7.9 15c.3-.3.8-.3 1.1 0zM12 16.3c.4 0 .8.3.8.8v1.8c0 .4-.3.8-.8.8s-.8-.3-.8-.8V17c.1-.4.4-.7.8-.7zM15 15c.3-.3.8-.3 1.1 0l1.3 1.3c.3.3.3.8 0 1.1s-.8.3-1.1 0L15 16.1c-.3-.3-.3-.8 0-1.1zM16.3 12c0-.4.3-.8.8-.8h1.8c.4 0 .8.3.8.8s-.3.8-.8.8H17c-.4 0-.7-.4-.7-.8zM15 9c-.3-.3-.3-.8 0-1.1l1.3-1.3c.3-.3.8-.3 1.1 0 .3.3.3.8 0 1.1L16.1 9c-.3.3-.8.3-1.1 0z" />
17
+ </svg>
18
+ );
19
+ };
@@ -0,0 +1,19 @@
1
+ import React, { FC } from 'react';
2
+
3
+ interface Props {
4
+ className?: string;
5
+ }
6
+
7
+ export const ContrastIcon: FC<Props> = ({ className }) => {
8
+ return (
9
+ <svg
10
+ className={className}
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ width="24"
13
+ height="24"
14
+ xmlSpace="preserve"
15
+ >
16
+ <path d="M12 4.4c-4.2 0-7.6 3.4-7.6 7.6s3.4 7.6 7.6 7.6 7.6-3.4 7.6-7.6-3.4-7.6-7.6-7.6zM5.9 12c0-3.3 2.7-6.1 6.1-6.1V18c-3.3.1-6.1-2.7-6.1-6z" />
17
+ </svg>
18
+ );
19
+ };
@@ -0,0 +1,19 @@
1
+ import React, { FC } from 'react';
2
+
3
+ interface Props {
4
+ className?: string;
5
+ }
6
+
7
+ export const CropIcon: FC<Props> = ({ className }) => {
8
+ return (
9
+ <svg
10
+ className={className}
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ width="24"
13
+ height="24"
14
+ xmlSpace="preserve"
15
+ >
16
+ <path d="M18.2 15.2h-1.5V7.3H8.8V5.8c0-.4-.3-.8-.8-.8s-.7.4-.7.8v1.5H5.8c-.4 0-.8.3-.8.8s.3.8.8.8h1.5v7.8h7.8v1.5c0 .4.3.8.8.8s.8-.3.8-.8v-1.5h1.5c.4 0 .8-.3.8-.8s-.4-.7-.8-.7zM8.8 8.8h6.3v6.3H8.8V8.8z" />
17
+ </svg>
18
+ );
19
+ };
@@ -0,0 +1,20 @@
1
+ import React, { FC } from 'react';
2
+
3
+ interface Props {
4
+ className?: string;
5
+ }
6
+
7
+ export const DownloadIcon: FC<Props> = ({ className }) => {
8
+ return (
9
+ <svg
10
+ className={className}
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ width="24"
13
+ height="24"
14
+ xmlSpace="preserve"
15
+ >
16
+ <path d="M17.2 12.1c-.4 0-.8.3-.8.8v2.8c0 .6-.4 1-1 1H8.6c-.6 0-1-.4-1-1v-2.8c0-.4-.3-.8-.8-.8s-.8.3-.8.8v2.8c0 1.4 1.1 2.5 2.5 2.5h6.8c1.4 0 2.5-1.1 2.5-2.5v-2.8c.1-.5-.2-.8-.6-.8z" />
17
+ <path d="M11.5 14.6c.1.1.3.2.5.2s.4-.1.5-.2l2.4-2.3c.3-.3.3-.8 0-1.1-.3-.3-.8-.3-1.1 0l-1.1 1.1V6.6c0-.4-.3-.8-.8-.8s-.8.3-.8.8v5.6l-1.1-1c-.3-.3-.8-.3-1.1 0-.3.3-.3.8 0 1.1l2.6 2.3z" />
18
+ </svg>
19
+ );
20
+ };
@@ -0,0 +1,21 @@
1
+ import React, { FC } from 'react';
2
+
3
+ interface Props {
4
+ className?: string;
5
+ }
6
+
7
+ export const FlipHorizontalIcon: FC<Props> = ({ className }) => {
8
+ return (
9
+ <svg
10
+ className={className}
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ width="16"
13
+ height="16"
14
+ >
15
+ <path
16
+ fill="#FFF"
17
+ d="M8 15.9c-.6 0-1-.4-1-1V1c0-.6.4-1 1-1s1 .4 1 1v13.9c0 .6-.4 1-1 1zm4-2.4h-.5c-.4 0-.8-.3-.8-.8s.3-.8.8-.8h.5c.4 0 .8.3.8.8s-.4.8-.8.8zm2.7 0h-.5c-.4 0-.8-.3-.8-.8 0-.3.2-.6.5-.7.1-.3.4-.5.7-.5.4 0 .8.3.8.8v.5c.1.4-.3.7-.7.7zm0-2.8c-.4 0-.8-.3-.8-.8v-.8c0-.4.3-.8.8-.8s.8.3.8.8v.8c0 .4-.4.8-.8.8zm0-3.3c-.4 0-.8-.3-.8-.8v-.7c0-.4.3-.8.8-.8s.8.3.8.8v.8c0 .4-.4.7-.8.7zm0-3.3c-.3 0-.6-.2-.7-.5-.3-.1-.5-.4-.5-.7 0-.4.3-.8.8-.8h.5c.4 0 .8.3.8.8v.5c-.1.4-.5.7-.9.7zM12 3.6h-.5c-.4 0-.8-.3-.8-.8s.3-.8.8-.8h.5c.4 0 .8.3.8.8s-.4.8-.8.8zM4.5 13.5H1.3c-.4 0-.8-.3-.8-.8V2.9c0-.4.3-.8.8-.8h3.2v1.5H2V12h2.5v1.5z"
18
+ />
19
+ </svg>
20
+ );
21
+ };
@@ -0,0 +1,27 @@
1
+ import React, { FC } from 'react';
2
+
3
+ interface Props {
4
+ className?: string;
5
+ }
6
+
7
+ export const FlipVerticalIcon: FC<Props> = ({ className }) => {
8
+ return (
9
+ <svg
10
+ className={className}
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ width="16"
13
+ height="16"
14
+ >
15
+ <path
16
+ fill="#FFF"
17
+ d="M15 9H1c-.5 0-1-.5-1-1s.4-1 1-1h14c.6 0 1 .4 1 1s-.5 1-1 1zM3.1 12.7c-.4 0-.8-.3-.8-.8v-.5c0-.4.3-.8.8-.8s.8.3.8.8v.6c0 .4-.3.7-.8.7zM3.6 15.4h-.5c-.4 0-.8-.3-.8-.8v-.5c0-.4.3-.8.8-.8.3 0 .6.2.7.5.3.1.5.4.5.7.1.6-.2.9-.7.9zM10.1 15.4h-.8c-.4 0-.8-.3-.8-.8s.3-.8.8-.8h.8c.4 0 .8.3.8.8s-.4.8-.8.8zm-3.2 0H6c-.4 0-.8-.3-.8-.8s.3-.8.8-.8h.8c.4 0 .8.3.8.8s-.3.8-.7.8zM13 15.4h-.5c-.4 0-.8-.3-.8-.8 0-.3.2-.6.5-.7.1-.3.4-.5.7-.5.4 0 .8.3.8.8v.5c.1.4-.2.7-.7.7zM13 12.7c-.4 0-.8-.3-.8-.8v-.5c0-.4.3-.8.8-.8s.8.3.8.8v.6c0 .4-.3.7-.8.7z"
18
+ />
19
+ <g>
20
+ <path
21
+ fill="#FFF"
22
+ d="M13.8 4.5h-1.5V2H3.9v2.5H2.4V1.3c0-.4.3-.8.8-.8H13c.4 0 .8.3.8.8v3.2z"
23
+ />
24
+ </g>
25
+ </svg>
26
+ );
27
+ };
@@ -0,0 +1,20 @@
1
+ import React, { FC } from 'react';
2
+
3
+ interface Props {
4
+ className?: string;
5
+ }
6
+
7
+ export const HueIcon: FC<Props> = ({ className }) => {
8
+ return (
9
+ <svg
10
+ className={className}
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ width="24"
13
+ height="24"
14
+ xmlSpace="preserve"
15
+ >
16
+ <path d="M12 19.6c-4.2 0-7.6-3.4-7.6-7.6S7.8 4.4 12 4.4s7.6 3.4 7.6 7.6-3.4 7.6-7.6 7.6zm0-13.7c-3.3 0-6.1 2.7-6.1 6.1s2.7 6.1 6.1 6.1 6.1-2.7 6.1-6.1-2.8-6.1-6.1-6.1z" />
17
+ <path d="M12 15.3c-1.8 0-3.3-1.5-3.3-3.3 0-1.8 1.5-3.3 3.3-3.3 1.8 0 3.3 1.5 3.3 3.3 0 1.8-1.5 3.3-3.3 3.3zm0-5.1c-1 0-1.8.8-1.8 1.8s.8 1.8 1.8 1.8 1.8-.8 1.8-1.8-.8-1.8-1.8-1.8z" />
18
+ </svg>
19
+ );
20
+ };
@@ -0,0 +1,19 @@
1
+ import React, { FC } from 'react';
2
+
3
+ interface Props {
4
+ className?: string;
5
+ }
6
+
7
+ export const ResetIcon: FC<Props> = ({ className }) => {
8
+ return (
9
+ <svg
10
+ className={className}
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ width="24"
13
+ height="24"
14
+ xmlSpace="preserve"
15
+ >
16
+ <path d="M21.2 9c-.3-.2-.8-.1-1 .2l-.8 1.3c-.7-3.4-3.8-6-7.4-6-4.2 0-7.6 3.4-7.6 7.6s3.4 7.6 7.6 7.6c2.3 0 4.5-1.1 6-2.9.3-.3.2-.8-.1-1.1-.3-.3-.8-.2-1.1.1-1.2 1.5-2.9 2.3-4.8 2.3-3.3 0-6.1-2.7-6.1-6.1S8.7 5.9 12 5.9c2.9 0 5.4 2.1 5.9 4.9l-1.3-.8c-.4-.2-.8-.1-1 .3-.2.4-.1.8.3 1l2.9 1.6c.1.1.2.1.4.1.3 0 .5-.1.6-.4l1.7-2.8c.2-.1.1-.6-.3-.8z" />
17
+ </svg>
18
+ );
19
+ };
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+
3
+ export const ResetRotationIcon = () => (
4
+ <svg
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ viewBox="0 0 24 24"
7
+ fill="none"
8
+ stroke="currentColor"
9
+ strokeWidth={2}
10
+ strokeLinecap="round"
11
+ strokeLinejoin="round"
12
+ width="18"
13
+ height="18"
14
+ >
15
+ <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
16
+ <path d="M21 3v5h-5" />
17
+ <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
18
+ <path d="M3 21v-5h5" />
19
+ </svg>
20
+ );
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import {
3
+ TbAdjustmentsHorizontal,
4
+ TbMaximize,
5
+ TbFocus2,
6
+ TbUnlink,
7
+ } from 'react-icons/tb';
8
+
9
+ // Fit Area - image fits inside boundaries
10
+ export const FitAreaIcon = () => <TbAdjustmentsHorizontal />;
11
+
12
+ // Fill Area - image fills the boundaries
13
+ export const FillAreaIcon = () => <TbMaximize />;
14
+
15
+ // Stencil - image restricted to stencil area
16
+ export const StencilIcon = () => <TbFocus2 />;
17
+
18
+ // None - no restrictions
19
+ export const NoRestrictionIcon = () => <TbUnlink />;
@@ -0,0 +1,21 @@
1
+ import React, { FC } from 'react';
2
+
3
+ interface Props {
4
+ className?: string;
5
+ }
6
+
7
+ export const RotateLeftIcon: FC<Props> = ({ className }) => {
8
+ return (
9
+ <svg
10
+ className={className}
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ width="16"
13
+ height="16"
14
+ >
15
+ <path
16
+ fill="#FFF"
17
+ d="M3.5 11.8c-.2.2-.2.6 0 .9 1.1 1.1 2.8 1.8 4.5 1.8 3.7 0 6.5-2.8 6.5-6.5s-3-6.5-6.5-6.5c-1.7 0-3.3.7-4.5 1.8l-1-1c-.4-.4-1-.1-1 .4v3.8c0 .3.3.6.6.6h3.7c.5 0 .8-.7.4-1L5 4.9c.9-.9 1.9-1.3 3-1.3 3 0 5.4 3.1 4 6.2-.7 1.6-2.3 2.6-4 2.6-1.2 0-2.2-.4-3-1.3-.2-.2-.6-.2-.8 0l-.7.7z"
18
+ />
19
+ </svg>
20
+ );
21
+ };
@@ -0,0 +1,21 @@
1
+ import React, { FC } from 'react';
2
+
3
+ interface Props {
4
+ className?: string;
5
+ }
6
+
7
+ export const RotateRightIcon: FC<Props> = ({ className }) => {
8
+ return (
9
+ <svg
10
+ className={className}
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ width="16"
13
+ height="16"
14
+ >
15
+ <path
16
+ fill="#FFF"
17
+ d="M12.5 11.8c.2.2.2.6 0 .9-1.1 1.1-2.8 1.8-4.5 1.8-3.7 0-6.5-2.8-6.5-6.5s3-6.5 6.5-6.5c1.7 0 3.3.7 4.5 1.8l1-1c.4-.4 1-.1 1 .4v3.8c0 .3-.3.6-.6.6h-3.7c-.5 0-.8-.7-.4-1L11 4.9c-.9-.9-1.9-1.3-3-1.3-3 0-5.4 3.1-4 6.2.7 1.6 2.3 2.6 4 2.6 1.2 0 2.2-.4 3-1.3.2-.2.6-.2.8 0l.7.7z"
18
+ />
19
+ </svg>
20
+ );
21
+ };
@@ -0,0 +1,20 @@
1
+ import React, { FC } from 'react';
2
+
3
+ interface Props {
4
+ className?: string;
5
+ }
6
+
7
+ export const SaturationIcon: FC<Props> = ({ className }) => {
8
+ return (
9
+ <svg
10
+ className={className}
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ width="24"
13
+ height="24"
14
+ xmlSpace="preserve"
15
+ >
16
+ <path d="M14.2 16c.5-.5.8-1.2.8-2 0-.3 0-.6-.1-.9-.3.1-.5.1-.8.1-1.3 0-2.2-.7-2.9-1.3-.2-.2-.8-.6-.9-.6 0 0-.1 0-.3.1-.5 1-.8 1.8-.8 2.5s.3 1.4.8 2c1 1.3 3 1.3 4.2.1z" />
17
+ <path d="M12 19.7c-1.5 0-3-.6-4.1-1.7-1.1-1.1-1.6-2.5-1.6-4 .1-2.7 1.8-5 3.6-7.4.5-.6 1-1.3 1.4-2l.7-.8.6.9c.5.7 1 1.4 1.5 2 1.8 2.4 3.5 4.6 3.6 7.3 0 1.5-.5 2.9-1.6 4-1.1 1-2.6 1.7-4.1 1.7zm0-13.3-.9 1.2C9.5 9.7 7.9 11.8 7.8 14c0 1.1.4 2.1 1.1 2.9.8.8 1.9 1.3 3 1.3s2.3-.5 3-1.3c.8-.8 1.2-1.8 1.1-2.9-.1-2.1-1.5-4.1-3.3-6.4-.1-.4-.4-.8-.7-1.2z" />
18
+ </svg>
19
+ );
20
+ };
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+
3
+ export const SettingsIcon = () => (
4
+ <svg
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ viewBox="0 0 24 24"
7
+ fill="none"
8
+ stroke="currentColor"
9
+ strokeWidth={2}
10
+ strokeLinecap="round"
11
+ strokeLinejoin="round"
12
+ width="18"
13
+ height="18"
14
+ >
15
+ <circle cx="12" cy="12" r="3" />
16
+ <path d="M12 1v6m0 6v6" />
17
+ <path d="M12 6.5a5.5 5.5 0 0 1 0 11" />
18
+ <path d="M1 12h6m6 0h6" />
19
+ <path d="M17.5 12a5.5 5.5 0 0 1-11 0" />
20
+ </svg>
21
+ );
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { TbRectangle, TbCircle } from 'react-icons/tb';
3
+
4
+ // Rectangle stencil
5
+ export const RectangleStencilIcon = () => <TbRectangle />;
6
+
7
+ // Circle stencil
8
+ export const CircleStencilIcon = () => <TbCircle />;
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { ConfigType } from '@plone/registry';
2
+ import installSettings from './config/settings';
3
+
4
+ function applyConfig(config: ConfigType) {
5
+ installSettings(config);
6
+
7
+ return config;
8
+ }
9
+
10
+ export default applyConfig;
@@ -0,0 +1,13 @@
1
+ $maximum: 1200px;
2
+ $desktop: 920px;
3
+ $tablet: 768px;
4
+ $mobile: 540px;
5
+
6
+ $container-padding: 24px;
7
+
8
+ $dark-theme-color: #6fcff0;
9
+ $theme-color: #61dafb;
10
+ $black-color: #212121;
11
+ $black-button-color: #36393f;
12
+ $black-button-color-hover: #20232a;
13
+ $real-theme-color: #1aa7f9;