@secretstache/wordpress-gutenberg 0.3.0 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. package/README.md +4 -0
  2. package/build/index.js +8 -2
  3. package/build/index.js.map +1 -1
  4. package/build/{index.css → styles.css} +2 -0
  5. package/package.json +5 -3
  6. package/src/components/ColorPaletteControl.js +24 -0
  7. package/src/components/DataQueryControls.js +51 -0
  8. package/src/components/DividersControl.js +74 -0
  9. package/src/components/IconPicker.js +73 -0
  10. package/src/components/ImageActions.js +59 -0
  11. package/src/components/LinkControl.js +30 -0
  12. package/src/components/MediaControl.js +192 -0
  13. package/src/components/MediaTypeControl.js +54 -0
  14. package/src/components/ResourcesWrapper.js +46 -0
  15. package/src/components/ResponsiveSpacingControl.js +74 -0
  16. package/src/components/SortableSelect.js +60 -0
  17. package/src/components/SpacingControl.js +119 -0
  18. package/src/components/index.js +12 -0
  19. package/src/hooks/index.js +11 -0
  20. package/src/hooks/useAccordionItem.js +51 -0
  21. package/src/hooks/useAllowedBlocks.js +25 -0
  22. package/src/hooks/useBlockTabsData.js +90 -0
  23. package/src/hooks/useChildBlockPosition.js +31 -0
  24. package/src/hooks/useColorChange.js +12 -0
  25. package/src/hooks/useDataQuery.js +45 -0
  26. package/src/hooks/useParentBlock.js +57 -0
  27. package/src/hooks/usePreviewToggle.js +32 -0
  28. package/src/hooks/useSlider.js +24 -0
  29. package/src/hooks/useThemeColors.js +19 -0
  30. package/src/hooks/useUpdateAttribute.js +4 -0
  31. package/src/index.js +6 -0
  32. package/src/styles/_animation-file-renderer.scss +11 -0
  33. package/src/styles/_editor-base.scss +56 -0
  34. package/src/styles/_icon-picker.scss +4 -0
  35. package/src/styles/_image-wrapper.scss +59 -0
  36. package/src/styles/_link-control.scss +6 -0
  37. package/src/styles/_media-picker.scss +20 -0
  38. package/src/styles/_new-child-btn.scss +15 -0
  39. package/src/styles/_responsive-spacing.scss +34 -0
  40. package/src/styles/_root-block-appender.scss +40 -0
  41. package/src/styles/_sortable-select.scss +5 -0
  42. package/src/styles/styles.scss +12 -0
  43. package/src/utils/attributes.js +224 -0
  44. package/src/utils/constants.js +17 -0
  45. package/src/utils/helpers.js +175 -0
  46. package/src/utils/index.js +6 -0
  47. package/src/utils/rootBlock/README.md +71 -0
  48. package/src/utils/rootBlock/hideRootBlockForInlineInserter.js +13 -0
  49. package/src/utils/rootBlock/hideRootBlockForOtherBlocks.js +32 -0
  50. package/src/utils/rootBlock/index.js +4 -0
  51. package/src/utils/rootBlock/initRootBlockAppender.js +45 -0
  52. package/src/utils/rootBlock/setRootBlock.js +32 -0
  53. package/src/utils/waitForContainer/README.md +40 -0
  54. package/src/utils/waitForContainer/index.js +25 -0
@@ -128,6 +128,8 @@
128
128
  .block-editor__container .bc-spacing-range-control .components-range-control__root .components-range-control__wrapper .components-range-control__marks .components-range-control__mark-label {
129
129
  left: 8px !important;
130
130
  margin-top: 7px !important; }
131
+ .block-editor__container .bc-spacing-range-control .components-range-control__root .components-base-control__help {
132
+ margin-left: -11px; }
131
133
 
132
134
  .block-editor__container .bc-add-new-child-btn {
133
135
  display: flex;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@secretstache/wordpress-gutenberg",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "",
5
5
  "author": "Secret Stache",
6
6
  "license": "GPL-2.0-or-later",
@@ -14,7 +14,8 @@
14
14
  "main": "build/index.js",
15
15
  "module": "build/index.es.js",
16
16
  "files": [
17
- "build"
17
+ "build",
18
+ "src"
18
19
  ],
19
20
  "type": "module",
20
21
  "scripts": {
@@ -50,18 +51,19 @@
50
51
  "rollup-plugin-postcss": "^4.0.2"
51
52
  },
52
53
  "dependencies": {
54
+ "classnames": "^2.5.1",
53
55
  "es-toolkit": "^1.12.0",
54
56
  "react-select": "5.7.5",
55
57
  "react-sortable-hoc": "2.0.0",
56
58
  "slugify": "^1.6.6"
57
59
  },
58
60
  "peerDependencies": {
59
- "@wordpress/dom-ready": "^4.3.0",
60
61
  "@wordpress/api-fetch": "^6.52.0",
61
62
  "@wordpress/block-editor": "^12.22.0",
62
63
  "@wordpress/blocks": "^12.34.0",
63
64
  "@wordpress/components": "^27.2.0",
64
65
  "@wordpress/data": "^9.24.0",
66
+ "@wordpress/dom-ready": "^4.3.0",
65
67
  "@wordpress/element": "^5.32.0",
66
68
  "@wordpress/hooks": "^3.57.0",
67
69
  "@wordpress/icons": "^9.45.0",
@@ -0,0 +1,24 @@
1
+ import { BaseControl, ColorPalette } from '@wordpress/components';
2
+ import { useThemeColors, useColorChange } from '../hooks';
3
+
4
+ export const ColorPaletteControl = ({
5
+ label = 'Color',
6
+ value,
7
+ attributeName,
8
+ setAttributes,
9
+ allowedColors,
10
+ }) => {
11
+ const colors = useThemeColors(allowedColors);
12
+ const onColorChange = useColorChange(colors, setAttributes);
13
+
14
+ return (
15
+ <BaseControl label={label}>
16
+ <ColorPalette
17
+ colors={colors}
18
+ value={value}
19
+ disableCustomColors={true}
20
+ onChange={(colorValue) => onColorChange(colorValue, attributeName)}
21
+ />
22
+ </BaseControl>
23
+ );
24
+ };
@@ -0,0 +1,51 @@
1
+ import { RadioControl } from '@wordpress/components';
2
+
3
+ // TODO: add support of curated posts, categories; consider merging with the useDataQuery hook
4
+ export const DataQueryControls = (props) => {
5
+ const {
6
+ dataSourceLabel = 'Data Source',
7
+ dataSource,
8
+ onDataSourceChange,
9
+
10
+ queryTypeLabel = 'Query',
11
+ queryType,
12
+ onQueryTypeChange,
13
+
14
+ settings,
15
+ } = props;
16
+
17
+ const sourcesList = settings
18
+ .filter((source) => source?.value && source?.label)
19
+ .map((source) => ({ label: source.label, value: source.value }));
20
+
21
+ const queriesList = settings.find((source) => source.value === dataSource)?.queries || [];
22
+
23
+ const hasSources = sourcesList && sourcesList?.length > 0;
24
+ const hasQueries = queriesList && queriesList?.length > 0;
25
+
26
+ return (
27
+ <>
28
+ {
29
+ hasSources && (
30
+ <RadioControl
31
+ label={dataSourceLabel}
32
+ selected={dataSource}
33
+ options={sourcesList}
34
+ onChange={onDataSourceChange}
35
+ />
36
+ )
37
+ }
38
+
39
+ {
40
+ hasQueries && (
41
+ <RadioControl
42
+ label={queryTypeLabel}
43
+ selected={queryType}
44
+ options={queriesList}
45
+ onChange={onQueryTypeChange}
46
+ />
47
+ )
48
+ }
49
+ </>
50
+ )
51
+ }
@@ -0,0 +1,74 @@
1
+ import { SelectControl, ToggleControl } from '@wordpress/components';
2
+ import { useEffect, useState } from '@wordpress/element';
3
+
4
+ export const DividersControl = ({
5
+ topDividers = [],
6
+ bottomDividers = [],
7
+ value = { topDivider: '', bottomDivider: '', isIncludeLine: false },
8
+ hasLine = true,
9
+ onChange,
10
+ }) => {
11
+ const hasTop = topDividers && topDividers?.length > 0;
12
+ const hasBottom = bottomDividers && bottomDividers?.length > 0;
13
+
14
+ const [ topDivider, setTopDivider ] = useState(value?.topDivider || '');
15
+ const [ bottomDivider, setBottomDivider ] = useState(value?.bottomDivider || '');
16
+ const [ isIncludeLine, setIsIncludeLine ] = useState(value?.isIncludeLine || false);
17
+
18
+ useEffect(() => {
19
+ onChange({
20
+ topDivider,
21
+ bottomDivider,
22
+ isIncludeLine,
23
+ });
24
+ }, [ topDivider, bottomDivider, isIncludeLine ]);
25
+
26
+ return (
27
+ <>
28
+ {
29
+ hasTop && (
30
+ <SelectControl
31
+ label="Top Divider"
32
+ value={topDivider}
33
+ onChange={(topDivider) => setTopDivider(topDivider)}
34
+ options={[
35
+ {
36
+ label: 'None',
37
+ value: ''
38
+ },
39
+ ...topDividers,
40
+ ]}
41
+
42
+ />
43
+ )
44
+ }
45
+
46
+ {
47
+ hasBottom && (
48
+ <SelectControl
49
+ label="Bottom Divider"
50
+ value={bottomDivider}
51
+ onChange={(bottomDivider) => setBottomDivider(bottomDivider)}
52
+ options={[
53
+ {
54
+ label: 'None',
55
+ value: ''
56
+ },
57
+ ...bottomDividers,
58
+ ]}
59
+ />
60
+ )
61
+ }
62
+
63
+ {
64
+ hasLine && (
65
+ <ToggleControl
66
+ label="Include Vertical Line leading from this block to the next"
67
+ checked={isIncludeLine}
68
+ onChange={() => setIsIncludeLine((prevState) => !prevState)}
69
+ />
70
+ )
71
+ }
72
+ </>
73
+ );
74
+ }
@@ -0,0 +1,73 @@
1
+ import {
2
+ MediaPlaceholder,
3
+ MediaUpload,
4
+ MediaUploadCheck,
5
+ } from '@wordpress/block-editor';
6
+ import { Button, Icon } from '@wordpress/components';
7
+ import { edit as editIcon, trash as trashIcon } from '@wordpress/icons';
8
+
9
+ export const IconPicker = ({ imageId, imageUrl, imageAlt, svgCode, onSelect, onRemove }) => {
10
+ const hasImage = imageId && imageUrl;
11
+ const isSvg = hasImage && svgCode;
12
+
13
+ return (
14
+ <MediaUploadCheck>
15
+ <MediaUpload
16
+ onSelect={onSelect}
17
+ allowedTypes={['image']}
18
+ value={imageId}
19
+ render={({ open }) => {
20
+ return hasImage ? (
21
+ <div className="bc-image-wrapper">
22
+ {hasImage && (
23
+ isSvg ? (
24
+ <div
25
+ className="svg-container"
26
+ dangerouslySetInnerHTML={{ __html: svgCode }}
27
+ />
28
+ ) : (
29
+ <img src={imageUrl} alt={imageAlt || 'icon'} />
30
+ )
31
+ )}
32
+
33
+ <div className="bc-image-wrapper__actions">
34
+ <Button
35
+ className="bc-image-wrapper__btn bc-image-wrapper__replace-btn"
36
+ type="button"
37
+ onClick={open}
38
+ >
39
+ <Icon
40
+ icon={editIcon}
41
+ size={20}
42
+ className="bc-image-wrapper__btn-icon"
43
+ />
44
+ </Button>
45
+
46
+ <Button
47
+ className="bc-image-wrapper__btn bc-image-wrapper__remove-btn"
48
+ type="button"
49
+ onClick={onRemove}
50
+ >
51
+ <Icon
52
+ icon={trashIcon}
53
+ size={20}
54
+ className="bc-image-wrapper__btn-icon"
55
+ />
56
+ </Button>
57
+ </div>
58
+
59
+ <div className="bc-image-wrapper__overlay" />
60
+ </div>
61
+ ) : (
62
+ <MediaPlaceholder
63
+ icon="format-image"
64
+ onSelect={onSelect}
65
+ allowedTypes={['image', 'image/svg+xml']}
66
+ labels={{ title: 'Icon Image' }}
67
+ />
68
+ );
69
+ }}
70
+ />
71
+ </MediaUploadCheck>
72
+ );
73
+ };
@@ -0,0 +1,59 @@
1
+ import { MediaUpload, MediaUploadCheck, MediaPlaceholder } from '@wordpress/block-editor';
2
+ import { Button } from '@wordpress/components';
3
+
4
+ export const ImageActions = ({
5
+ imageId,
6
+ imageUrl,
7
+ imageAlt,
8
+ placeholder = false,
9
+ onSelectImage,
10
+ onRemoveImage,
11
+ className = '',
12
+ }) => {
13
+ const hasImage = imageId && imageUrl;
14
+
15
+ return (
16
+ <MediaUploadCheck>
17
+ <MediaUpload
18
+ onSelect={onSelectImage}
19
+ allowedTypes={['image']}
20
+ value={imageId}
21
+ render={({ open }) => {
22
+ return hasImage ? (
23
+ <div className={`bc-image-wrapper ${className}`}>
24
+ <img src={imageUrl} alt={imageAlt} onClick={open} />
25
+ <div className="bc-image-wrapper__actions">
26
+ <Button
27
+ className="bc-image-wrapper__btn bc-image-wrapper__replace-btn"
28
+ type="button"
29
+ onClick={open}
30
+ >
31
+ Replace
32
+ </Button>
33
+ <Button
34
+ className="bc-image-wrapper__btn bc-image-wrapper__remove-btn"
35
+ type="button"
36
+ onClick={onRemoveImage}
37
+ >
38
+ Remove
39
+ </Button>
40
+ </div>
41
+ <div className="bc-image-wrapper__overlay" />
42
+ </div>
43
+ ) : placeholder ? (
44
+ <MediaPlaceholder
45
+ className="media-placeholder"
46
+ icon="format-image"
47
+ onSelect={onSelectImage}
48
+ allowedTypes={['image']}
49
+ labels={{
50
+ title: 'Image',
51
+ instructions: 'Upload an image file or pick one from your media library.',
52
+ }}
53
+ />
54
+ ) : null;
55
+ }}
56
+ />
57
+ </MediaUploadCheck>
58
+ );
59
+ };
@@ -0,0 +1,30 @@
1
+ import { BaseControl, CheckboxControl } from '@wordpress/components';
2
+ import { URLInput } from '@wordpress/block-editor';
3
+ import { useUpdateAttribute } from '../hooks';
4
+
5
+ export const LinkControl = ({
6
+ url = { value: '#', attrName: 'linkSource' },
7
+ isOpenInNewTab = { value: false, attrName: 'linkIsOpenInNewTab'},
8
+ setAttributes,
9
+ label = 'Source',
10
+ }) => {
11
+ const updateAttribute = useUpdateAttribute(setAttributes);
12
+
13
+ return (
14
+ <>
15
+ <BaseControl label={label}>
16
+ <URLInput
17
+ className="bc-url-input"
18
+ value={url.value}
19
+ onChange={(newUrl) => setAttributes({ [url.attrName]: newUrl })}
20
+ />
21
+ </BaseControl>
22
+
23
+ <CheckboxControl
24
+ checked={isOpenInNewTab.value}
25
+ label="Open in a new tab"
26
+ onChange={(newIsOpenInNewTab) => setAttributes({ [isOpenInNewTab.attrName]: newIsOpenInNewTab })}
27
+ />
28
+ </>
29
+ );
30
+ };
@@ -0,0 +1,192 @@
1
+ import { Button, Icon as WPIcon } from '@wordpress/components';
2
+ import { MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
3
+ import { page as pageIcon } from '@wordpress/icons';
4
+
5
+ import { MEDIA_TYPES } from '../utils/index.js';
6
+
7
+ export const ImageRenderer = ({
8
+ imageId,
9
+ imageUrl,
10
+ onImageClick,
11
+ onRemoveClick,
12
+ onSelectClick,
13
+ selectButtonLabel = "Select Image",
14
+ removeButtonLabel = "Remove Image",
15
+ }) => {
16
+ return imageId && imageUrl ? (
17
+ <>
18
+ <div className="bc-selected-media-wrapper">
19
+ <img
20
+ src={imageUrl}
21
+ className="bc-selected-media bc-selected-media--image"
22
+ alt="Selected Image"
23
+ onClick={onImageClick}
24
+ />
25
+ </div>
26
+
27
+ <Button
28
+ className="bc-remove-btn"
29
+ onClick={onRemoveClick}
30
+ isSecondary
31
+ isDestructive
32
+ >
33
+ {removeButtonLabel}
34
+ </Button>
35
+ </>
36
+ ) : (
37
+ <Button
38
+ variant="secondary"
39
+ onClick={onSelectClick}
40
+ className="bc-select-btn"
41
+ >
42
+ {selectButtonLabel}
43
+ </Button>
44
+ );
45
+ };
46
+
47
+ export const VideoRenderer = ({
48
+ videoId,
49
+ videoUrl,
50
+ onRemoveClick,
51
+ onSelectClick,
52
+ selectButtonLabel = "Select Video",
53
+ removeButtonLabel = "Remove Video",
54
+ }) => {
55
+ return videoId && videoUrl ? (
56
+ <>
57
+ <div className="bc-selected-media-wrapper">
58
+ <video src={videoUrl} className="bc-selected-media bc-selected-media--video" controls />
59
+ </div>
60
+
61
+ <Button
62
+ className="bc-remove-btn"
63
+ onClick={onRemoveClick}
64
+ isSecondary
65
+ isDestructive
66
+ >
67
+ {removeButtonLabel}
68
+ </Button>
69
+ </>
70
+ ) : (
71
+ <Button
72
+ variant="secondary"
73
+ onClick={onSelectClick}
74
+ className="bc-select-btn"
75
+ >
76
+ {selectButtonLabel}
77
+ </Button>
78
+ );
79
+ };
80
+
81
+ export const AnimationRenderer = ({
82
+ animationFileId,
83
+ animationFileUrl,
84
+ animationFileName,
85
+ onSelectClick,
86
+ onRemoveClick,
87
+ selectButtonLabel = "Select File",
88
+ removeButtonLabel = "Remove File",
89
+ }) => {
90
+ return animationFileId && animationFileUrl ? (
91
+ <>
92
+ <div className="bc-animation-block-json-file" onClick={onSelectClick}>
93
+ <WPIcon icon={pageIcon} size={36} />
94
+ <span>{animationFileName}</span>
95
+ </div>
96
+ <Button
97
+ variant="secondary"
98
+ isDestructive
99
+ className="bc-remove-btn"
100
+ onClick={onRemoveClick}
101
+ >
102
+ {removeButtonLabel}
103
+ </Button>
104
+ </>
105
+ ) : (
106
+ <Button variant="secondary" onClick={onSelectClick}>
107
+ {selectButtonLabel}
108
+ </Button>
109
+ )
110
+ };
111
+
112
+ export const MediaControl = ({
113
+ mediaId,
114
+ mediaUrl,
115
+ mediaFileName = '',
116
+ onSelect,
117
+ onRemove,
118
+ type = MEDIA_TYPES.IMAGE,
119
+ selectButtonLabel,
120
+ removeButtonLabel,
121
+ ...other
122
+ }) => {
123
+ if (type === MEDIA_TYPES.IMAGE) {
124
+ return (
125
+ <MediaUploadCheck>
126
+ <MediaUpload
127
+ onSelect={onSelect}
128
+ allowedTypes={['image', 'image/svg+xml']}
129
+ accept="image/*"
130
+ value={mediaId}
131
+ render={({ open }) => (
132
+ <ImageRenderer
133
+ imageId={mediaId}
134
+ imageUrl={mediaUrl}
135
+ onImageClick={open}
136
+ onSelectClick={open}
137
+ onRemoveClick={onRemove}
138
+ selectButtonLabel={selectButtonLabel}
139
+ removeButtonLabel={removeButtonLabel}
140
+ />
141
+ )}
142
+ {...other}
143
+ />
144
+ </MediaUploadCheck>
145
+ );
146
+ } else if (type === MEDIA_TYPES.VIDEO) {
147
+ return (
148
+ <MediaUploadCheck>
149
+ <MediaUpload
150
+ onSelect={onSelect}
151
+ allowedTypes={['video']}
152
+ value={mediaId}
153
+ render={({ open }) => (
154
+ <VideoRenderer
155
+ videoId={mediaId}
156
+ videoUrl={mediaUrl}
157
+ onSelectClick={open}
158
+ onRemoveClick={onRemove}
159
+ selectButtonLabel={selectButtonLabel}
160
+ removeButtonLabel={removeButtonLabel}
161
+ />
162
+ )}
163
+ {...other}
164
+ />
165
+ </MediaUploadCheck>
166
+ );
167
+ } else if (type === MEDIA_TYPES.ANIMATION) {
168
+ return (
169
+ <MediaUploadCheck>
170
+ <MediaUpload
171
+ onSelect={onSelect}
172
+ allowedTypes={['application/json', 'text/plain', 'application/lottie']}
173
+ value={mediaId}
174
+ render={({ open }) => (
175
+ <AnimationRenderer
176
+ animationFileId={mediaId}
177
+ animationFileUrl={mediaUrl}
178
+ animationFileName={mediaFileName}
179
+ onSelectClick={open}
180
+ onRemoveClick={onRemove}
181
+ selectButtonLabel={selectButtonLabel}
182
+ removeButtonLabel={removeButtonLabel}
183
+ />
184
+ )}
185
+ {...other}
186
+ />
187
+ </MediaUploadCheck>
188
+ );
189
+ } else {
190
+ throw new Error('Unrecognized media type.');
191
+ }
192
+ };
@@ -0,0 +1,54 @@
1
+ import { useState } from '@wordpress/element';
2
+ import { SelectControl } from '@wordpress/components';
3
+
4
+ import { MediaControl } from './MediaControl.js';
5
+ import { MEDIA_TYPE_LABELS } from '../utils/index.js';
6
+
7
+ export const MediaTypeControl = (props) => {
8
+ const {
9
+ mediaTypes = [],
10
+ mediaId,
11
+ mediaUrl,
12
+ mediaFileName = '',
13
+ mediaOnSelect,
14
+ mediaOnRemove,
15
+ } = props;
16
+
17
+ const [selectedMediaType, setSelectedMediaType] = useState(mediaTypes?.[0]);
18
+
19
+ const mediaTypesOptions = mediaTypes
20
+ ?.filter((type) => MEDIA_TYPE_LABELS[type]) // Ensure it's an allowed type
21
+ ?.map((type) => ({
22
+ label: MEDIA_TYPE_LABELS[type],
23
+ value: type,
24
+ }));
25
+
26
+ return (
27
+ <>
28
+ {
29
+ // TODO: add custom label
30
+ mediaTypes && (
31
+ <SelectControl
32
+ label="Media Type"
33
+ value={selectedMediaType}
34
+ onChange={(mediaType) => setSelectedMediaType(mediaType)}
35
+ options={mediaTypesOptions}
36
+ />
37
+ )
38
+ }
39
+
40
+ {
41
+ selectedMediaType && (
42
+ <MediaControl
43
+ mediaId={mediaId}
44
+ mediaUrl={mediaUrl}
45
+ mediaFileName={mediaFileName}
46
+ type={selectedMediaType}
47
+ onSelect={mediaOnSelect}
48
+ onRemove={mediaOnRemove}
49
+ />
50
+ )
51
+ }
52
+ </>
53
+ );
54
+ }
@@ -0,0 +1,46 @@
1
+ import { Notice, Placeholder, Spinner } from '@wordpress/components';
2
+
3
+ const EmptyNotice = ({
4
+ message = 'No resources were found matching your criteria. Please try to adjust the query.'
5
+ }) => (
6
+ <Notice status="info" isDismissible={false}>
7
+ <p>{message}</p>
8
+ </Notice>
9
+ );
10
+
11
+ const LoadingSpinner = () => <Spinner className="bc-spinner" />;
12
+
13
+ const PlaceholderContent = ({
14
+ icon = 'info-outline',
15
+ instructions = 'Please configure the block in the sidebar.',
16
+ ...restProps
17
+ }) => (
18
+ <Placeholder
19
+ icon={icon}
20
+ instructions={instructions}
21
+ {...restProps}
22
+ />
23
+ );
24
+
25
+ export const ResourcesWrapper = ({
26
+ isLoading,
27
+ isEmpty,
28
+ isPlaceholder,
29
+ emptyMessage,
30
+ placeholderProps = {},
31
+ children,
32
+ }) => {
33
+ if (isLoading) {
34
+ return <LoadingSpinner />;
35
+ }
36
+
37
+ if (isEmpty) {
38
+ return <EmptyNotice message={emptyMessage} />;
39
+ }
40
+
41
+ if (isPlaceholder) {
42
+ return <PlaceholderContent {...placeholderProps} />;
43
+ }
44
+
45
+ return children;
46
+ };