@siratik/wp-component-library 0.1.1 → 0.1.3

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 (96) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +43 -5
  3. package/components/attachment-image/README.md +118 -0
  4. package/components/attachment-image/index.js +105 -0
  5. package/components/attributes-panel/README.md +92 -0
  6. package/components/attributes-panel/index.js +324 -0
  7. package/components/badge/README.md +61 -0
  8. package/components/badge/index.js +105 -0
  9. package/components/block-appender/README.md +158 -0
  10. package/components/block-appender/index.js +135 -0
  11. package/components/block-extension/index.js +214 -0
  12. package/components/business-hours/SpecialHoursControl.js +211 -0
  13. package/components/business-hours/WeeklyHoursControl.js +419 -0
  14. package/components/business-hours/index.js +8 -0
  15. package/components/button/README.md +172 -0
  16. package/components/button/index.js +84 -0
  17. package/components/captcha-placeholder/CloudflareTurnstile.js +111 -0
  18. package/components/captcha-placeholder/GoogleRecaptcha.js +132 -0
  19. package/components/captcha-placeholder/HCaptcha.js +99 -0
  20. package/components/captcha-placeholder/README.md +228 -0
  21. package/components/captcha-placeholder/index.js +39 -0
  22. package/components/captcha-placeholder/styles.js +33 -0
  23. package/components/geocoder/AddressEntryControl.js +390 -0
  24. package/components/geocoder/GeocoderSearchControl.js +353 -0
  25. package/components/geocoder/countryOptions.js +66 -0
  26. package/components/geocoder/geocoder.js +400 -0
  27. package/components/geocoder/index.js +16 -0
  28. package/components/icon-picker/Icon.js +53 -0
  29. package/components/icon-picker/IconPicker.js +70 -0
  30. package/components/icon-picker/IconPickerModal.js +474 -0
  31. package/components/icon-picker/IconPickerToolbarButton.js +60 -0
  32. package/components/icon-picker/InlineIconPicker.js +92 -0
  33. package/components/icon-picker/api.js +181 -0
  34. package/components/icon-picker/config-bootstrap.js +108 -0
  35. package/components/icon-picker/hooks.js +157 -0
  36. package/components/icon-picker/index.js +39 -0
  37. package/components/icon-picker/load-script.js +33 -0
  38. package/components/icon-picker/loaders.js +79 -0
  39. package/components/icon-picker/store.js +223 -0
  40. package/components/icon-picker/styles.js +232 -0
  41. package/components/icon-picker/utils.js +138 -0
  42. package/components/layout/README.md +268 -0
  43. package/components/link-tools-panel/README.md +49 -0
  44. package/components/link-tools-panel/index.js +198 -0
  45. package/components/link-tools-panel/styles.js +90 -0
  46. package/components/media/README.md +288 -0
  47. package/components/media/SettingsMediaUpload.js +186 -0
  48. package/components/media/editor-media-upload.js +65 -0
  49. package/components/media/image-controls.js +196 -0
  50. package/components/media/index.js +13 -0
  51. package/components/media/inspector-media-upload.js +290 -0
  52. package/components/media/toolbar-media-upload.js +53 -0
  53. package/components/media/utils/aspect-ratios.js +118 -0
  54. package/components/meta/MetaPanel.js +75 -0
  55. package/components/meta/README.md +46 -0
  56. package/components/meta/index.js +6 -0
  57. package/components/meta/meta-gated-editor.js +282 -0
  58. package/components/query/README.md +340 -0
  59. package/components/query/cardTemplatePartPanel.js +69 -0
  60. package/components/query/cardTemplatePartSelect.js +165 -0
  61. package/components/query/cardTemplatePartUtils.js +119 -0
  62. package/components/query/columnCountControl.js +63 -0
  63. package/components/query/displayTypeSelect.js +52 -0
  64. package/components/query/examples.js +166 -0
  65. package/components/query/hideCurrentPostControl.js +47 -0
  66. package/components/query/index.js +20 -0
  67. package/components/query/manualPostSelector.js +125 -0
  68. package/components/query/manualTermSelector.js +141 -0
  69. package/components/query/orderBySelect.js +95 -0
  70. package/components/query/postTypeSelect.js +67 -0
  71. package/components/query/postsPerPageControl.js +51 -0
  72. package/components/query/selectionModeControl.js +61 -0
  73. package/components/query/taxonomySelect.js +139 -0
  74. package/components/query/utils.js +75 -0
  75. package/components/repeater/README.md +159 -0
  76. package/components/repeater/index.js +829 -0
  77. package/components/segmented-bar/README.md +30 -0
  78. package/components/segmented-bar/index.js +68 -0
  79. package/components/sortable-select/README.md +153 -0
  80. package/components/sortable-select/custom-sortable-token.js +127 -0
  81. package/components/sortable-select/index.js +693 -0
  82. package/components/style-picker/README.md +36 -0
  83. package/components/style-picker/index.js +92 -0
  84. package/components/variable-field/README.md +299 -0
  85. package/components/variable-field/index.js +592 -0
  86. package/components/variable-field/value-to-chip-html.js +38 -0
  87. package/components/variable-inserter/README.md +68 -0
  88. package/components/variable-inserter/index.js +168 -0
  89. package/constants/statusColors.js +1 -1
  90. package/editor.js +16 -0
  91. package/index.js +127 -0
  92. package/package.json +21 -13
  93. package/utils/kitaris-localize.js +128 -0
  94. package/utils/slug-aware-color-setter.js +27 -0
  95. package/build/index.asset.php +0 -20
  96. package/build/index.js +0 -1242
@@ -0,0 +1,196 @@
1
+ import {
2
+ Flex,
3
+ FlexBlock,
4
+ SelectControl,
5
+ ToggleControl,
6
+ __experimentalToggleGroupControl as ToggleGroupControl,
7
+ __experimentalToggleGroupControlOption as ToggleGroupControlOption,
8
+ __experimentalToolsPanel as ToolsPanel,
9
+ __experimentalToolsPanelItem as ToolsPanelItem,
10
+ __experimentalUnitControl as UnitControl,
11
+ } from "@wordpress/components";
12
+ import { useSelect } from "@wordpress/data";
13
+ import { __ } from "@wordpress/i18n";
14
+
15
+ import { useAspectRatioOptions } from "./utils/aspect-ratios";
16
+
17
+ /**
18
+ * Reusable image controls for aspect ratio, width, height, scale, and caption.
19
+ * @param {object} props
20
+ * @param {string} props.aspectRatio
21
+ * @param {string} props.width
22
+ * @param {string} props.height
23
+ * @param {boolean} props.showCaption
24
+ * @param {string} props.scale - Object-fit value: "cover" or "contain".
25
+ * @param {function} props.setAttributes
26
+ */
27
+ const SCALE_HELP = {
28
+ cover: __("Image covers the space evenly.", "wp-component-library"),
29
+ contain: __("Image is contained without cropping.", "wp-component-library"),
30
+ };
31
+
32
+ const ImageControls = ({
33
+ aspectRatio = "4/3",
34
+ width = "auto",
35
+ height = "auto",
36
+ showCaption = false,
37
+ scale = "cover",
38
+ imageSize = "",
39
+ setAttributes,
40
+ }) => {
41
+ // Get aspect ratio options from theme.json
42
+ const aspectRatioOptions = useAspectRatioOptions();
43
+
44
+ // Build resolution options from registered image sizes
45
+ const imageSizeOptions = useSelect((select) => {
46
+ const sizes = select("core/block-editor").getSettings().imageSizes || [];
47
+ return [
48
+ { value: "", label: __("Auto", "wp-component-library") },
49
+ ...sizes.map(({ name, slug }) => ({ value: slug, label: name })),
50
+ ];
51
+ }, []);
52
+
53
+ const resetAllFilter = (newAttributes) => {
54
+ return {
55
+ ...newAttributes,
56
+ aspectRatio: "4/3",
57
+ width: "auto",
58
+ height: "auto",
59
+ showCaption: false,
60
+ scale: "cover",
61
+ imageSize: "",
62
+ };
63
+ };
64
+
65
+ return (
66
+ <ToolsPanel
67
+ label={__("Image Settings", "wp-component-library")}
68
+ resetAllFilter={resetAllFilter}
69
+ panelId="image-settings"
70
+ >
71
+ <ToolsPanelItem
72
+ hasValue={() => aspectRatio !== "4/3"}
73
+ label={__("Aspect Ratio", "wp-component-library")}
74
+ onDeselect={() => setAttributes({ aspectRatio: "4/3" })}
75
+ resetAllFilter={resetAllFilter}
76
+ isShownByDefault
77
+ panelId="image-settings"
78
+ >
79
+ <SelectControl
80
+ __nextHasNoMarginBottom={true}
81
+ __next40pxDefaultSize
82
+ label={__("Aspect Ratio", "wp-component-library")}
83
+ value={aspectRatio}
84
+ onChange={(value) => setAttributes({ aspectRatio: value })}
85
+ options={aspectRatioOptions}
86
+ />
87
+ </ToolsPanelItem>
88
+
89
+ <ToolsPanelItem
90
+ hasValue={() => scale !== "cover"}
91
+ label={__("Scale", "wp-component-library")}
92
+ onDeselect={() => setAttributes({ scale: "cover" })}
93
+ resetAllFilter={resetAllFilter}
94
+ isShownByDefault
95
+ panelId="image-settings"
96
+ >
97
+ <ToggleGroupControl
98
+ __nextHasNoMarginBottom={true}
99
+ __next40pxDefaultSize
100
+ label={__("Scale", "wp-component-library")}
101
+ value={scale}
102
+ onChange={(value) => setAttributes({ scale: value })}
103
+ isBlock
104
+ help={SCALE_HELP[scale] ?? ""}
105
+ >
106
+ <ToggleGroupControlOption
107
+ value="cover"
108
+ label={__("Cover", "wp-component-library")}
109
+ />
110
+ <ToggleGroupControlOption
111
+ value="contain"
112
+ label={__("Contain", "wp-component-library")}
113
+ />
114
+ </ToggleGroupControl>
115
+ </ToolsPanelItem>
116
+
117
+ <Flex gap={4} style={{ minWidth: 250 }}>
118
+ <FlexBlock>
119
+ <ToolsPanelItem
120
+ hasValue={() => width !== "auto"}
121
+ label={__("Width", "wp-component-library")}
122
+ onDeselect={() => setAttributes({ width: "auto" })}
123
+ resetAllFilter={resetAllFilter}
124
+ panelId="image-settings"
125
+ >
126
+ <UnitControl
127
+ __next40pxDefaultSize
128
+ label={__("Width", "wp-component-library")}
129
+ onChange={(value) =>
130
+ setAttributes({ width: value })
131
+ }
132
+ value={width}
133
+ placeholder={__("Auto", "wp-component-library")}
134
+ />
135
+ </ToolsPanelItem>
136
+ </FlexBlock>
137
+ <FlexBlock>
138
+ <ToolsPanelItem
139
+ hasValue={() => height !== "auto"}
140
+ label={__("Height", "wp-component-library")}
141
+ onDeselect={() => setAttributes({ height: "auto" })}
142
+ resetAllFilter={resetAllFilter}
143
+ panelId="image-settings"
144
+ >
145
+ <UnitControl
146
+ __next40pxDefaultSize
147
+ label={__("Height", "wp-component-library")}
148
+ onChange={(value) =>
149
+ setAttributes({ height: value })
150
+ }
151
+ value={height}
152
+ placeholder={__("Auto", "wp-component-library")}
153
+ />
154
+ </ToolsPanelItem>
155
+ </FlexBlock>
156
+ </Flex>
157
+
158
+ <ToolsPanelItem
159
+ hasValue={() => showCaption !== false}
160
+ label={__("Show Caption", "wp-component-library")}
161
+ onDeselect={() => setAttributes({ showCaption: false })}
162
+ resetAllFilter={resetAllFilter}
163
+ isShownByDefault={false}
164
+ panelId="image-settings"
165
+ >
166
+ <ToggleControl
167
+ __nextHasNoMarginBottom={true}
168
+ label={__("Show Caption", "wp-component-library")}
169
+ checked={showCaption}
170
+ onChange={(value) => setAttributes({ showCaption: value })}
171
+ />
172
+ </ToolsPanelItem>
173
+
174
+ <ToolsPanelItem
175
+ hasValue={() => !!imageSize}
176
+ label={__("Resolution", "wp-component-library")}
177
+ onDeselect={() => setAttributes({ imageSize: "" })}
178
+ resetAllFilter={resetAllFilter}
179
+ isShownByDefault={false}
180
+ panelId="image-settings"
181
+ >
182
+ <SelectControl
183
+ __nextHasNoMarginBottom={true}
184
+ __next40pxDefaultSize
185
+ label={__("Resolution", "wp-component-library")}
186
+ value={imageSize}
187
+ options={imageSizeOptions}
188
+ onChange={(value) => setAttributes({ imageSize: value })}
189
+ help={__("Override the default image resolution. Leave as Auto for smart sizing.", "wp-component-library")}
190
+ />
191
+ </ToolsPanelItem>
192
+ </ToolsPanel>
193
+ );
194
+ };
195
+
196
+ export { ImageControls };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Media Upload Components
3
+ *
4
+ * A collection of media upload components for different contexts
5
+ * within the WordPress block editor and admin settings.
6
+ */
7
+
8
+ // Re-export individual media upload components
9
+ export { EditorMediaUpload } from "./editor-media-upload";
10
+ export { ImageControls } from "./image-controls";
11
+ export { InspectorMediaUpload } from "./inspector-media-upload";
12
+ export { SettingsMediaUpload } from "./SettingsMediaUpload";
13
+ export { ToolbarMediaUpload } from "./toolbar-media-upload";
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Inspector Media Upload Component
3
+ *
4
+ * Uses wp.media() directly instead of the block editor's MediaUpload
5
+ * slot, ensuring it works on both block editor and settings pages.
6
+ */
7
+ import styled from "@emotion/styled";
8
+ import { BaseControl, Button, Flex, Placeholder } from "@wordpress/components";
9
+ import { useCallback } from "@wordpress/element";
10
+ import { __ } from "@wordpress/i18n";
11
+ import { AttachmentImage } from "../attachment-image";
12
+
13
+ /**
14
+ * "auto", a number ("1.91") or a ratio ("16/9", "16 / 9").
15
+ */
16
+ const ASPECT_RATIO_PATTERN = /^(auto|\d+(\.\d+)?(\s*\/\s*\d+(\.\d+)?)?)$/;
17
+
18
+ /**
19
+ * A number with an optional length unit ("400", "64px", "20rem", "50%").
20
+ */
21
+ const MAX_WIDTH_PATTERN = /^\d+(\.\d+)?(px|em|rem|%|vw|ch)?$/;
22
+
23
+ /**
24
+ * CSS aspect-ratio accepts a number (e.g. 1.91) or a ratio string ("16/9").
25
+ * Anything else falls back to 16/9, so a prop value can never add CSS rules.
26
+ *
27
+ * @param {number|string} ratio
28
+ * @returns {string}
29
+ */
30
+ function toCssAspectRatio(ratio) {
31
+ if (typeof ratio === "number" && Number.isFinite(ratio) && ratio > 0) {
32
+ return String(ratio);
33
+ }
34
+ if (typeof ratio === "string" && ASPECT_RATIO_PATTERN.test(ratio.trim())) {
35
+ return ratio.trim();
36
+ }
37
+ return String(16 / 9);
38
+ }
39
+
40
+ /**
41
+ * CSS max-width for the preview (number = px; empty or invalid = none).
42
+ *
43
+ * @param {number|string|null} maxWidth
44
+ * @returns {string}
45
+ */
46
+ function toCssMaxWidth(maxWidth) {
47
+ if (typeof maxWidth === "number" && Number.isFinite(maxWidth) && maxWidth >= 0) {
48
+ return `${maxWidth}px`;
49
+ }
50
+ if (typeof maxWidth === "string" && MAX_WIDTH_PATTERN.test(maxWidth.trim())) {
51
+ return maxWidth.trim();
52
+ }
53
+ return "none";
54
+ }
55
+
56
+ // Styled components
57
+ const StyledWrapper = styled.div`
58
+ max-width: ${(props) => toCssMaxWidth(props.maxWidth)};
59
+
60
+ /* Shared frame so placeholder and selected image occupy the same box. */
61
+ .built-inspector-media-preview {
62
+ width: 100%;
63
+ aspect-ratio: ${(props) => props.$aspectRatio};
64
+ overflow: hidden;
65
+ box-sizing: border-box;
66
+ background: #f0f0f0;
67
+ }
68
+
69
+ .built-inspector-media-preview .components-placeholder {
70
+ width: 100%;
71
+ height: 100%;
72
+ min-height: 0 !important;
73
+ margin: 0;
74
+ padding: 0;
75
+ box-sizing: border-box;
76
+ }
77
+
78
+ .built-inspector-media-preview img {
79
+ width: 100%;
80
+ height: 100%;
81
+ object-fit: cover;
82
+ object-position: center;
83
+ display: block;
84
+ }
85
+ `;
86
+
87
+ const StyledImageContainer = styled.div`
88
+ width: 100%;
89
+ height: 100%;
90
+
91
+ .components-flex {
92
+ height: auto;
93
+ }
94
+ `;
95
+
96
+ const StyledActions = styled.div`
97
+ margin-top: 12px;
98
+ `;
99
+
100
+ const ALLOWED_MEDIA_TYPES = ["image"];
101
+
102
+ /**
103
+ * Hook to open the native wp.media frame.
104
+ */
105
+ function useMediaOpen({ onSelect, multiple, allowedTypes }) {
106
+ return useCallback(() => {
107
+ if (typeof wp === "undefined" || !wp.media) { return; }
108
+
109
+ const frame = wp.media({
110
+ title: __("Select or Upload Media", "wp-component-library"),
111
+ multiple: multiple || false,
112
+ library: { type: allowedTypes || ["image"] },
113
+ button: { text: __("Select", "wp-component-library") },
114
+ });
115
+
116
+ frame.on("select", () => {
117
+ const selection = frame.state().get("selection");
118
+ if (multiple) {
119
+ onSelect(selection.map((att) => att.toJSON()));
120
+ } else {
121
+ onSelect(selection.first().toJSON());
122
+ }
123
+ });
124
+
125
+ frame.open();
126
+ }, [onSelect, multiple, allowedTypes]);
127
+ }
128
+
129
+ /**
130
+ * Inspector Media Upload
131
+ *
132
+ * @param {object} props
133
+ * @param {number|string|null} [props.maxWidth] Optional max width for the
134
+ * preview/placeholder (number = px). Default: none (full width).
135
+ * @param {number|string} [props.aspectRatio=16/9] Locked preview box ratio;
136
+ * selected images use object-fit: cover inside the same box as the placeholder.
137
+ * @returns {JSX.Element}
138
+ */
139
+ function InspectorMediaUpload({
140
+ mediaIDs,
141
+ onSelect,
142
+ onRemove,
143
+ multiple,
144
+ buttonTitle,
145
+ variant,
146
+ showFeatureImage = false,
147
+ featureImage = null,
148
+ label = null,
149
+ help = null,
150
+ showImagePlaceholder,
151
+ aspectRatio = 16 / 9,
152
+ maxWidth = null,
153
+ }) {
154
+ // Handle ID, [id], and attachment object formats for mediaIDs.
155
+ const getMediaId = (mediaData) => {
156
+ if (!mediaData) { return null; }
157
+ if (typeof mediaData === "number") { return mediaData; }
158
+ if (typeof mediaData === "string" && /^\d+$/.test(mediaData)) {
159
+ return parseInt(mediaData, 10);
160
+ }
161
+ if (Array.isArray(mediaData)) {
162
+ return getMediaId(mediaData[0]);
163
+ }
164
+ if (typeof mediaData === "object" && mediaData.id) { return mediaData.id; }
165
+ return null;
166
+ };
167
+
168
+ const mediaId = getMediaId(mediaIDs);
169
+ const hasImage = !!mediaId;
170
+ const hasFeatureImage = !!(
171
+ showFeatureImage &&
172
+ featureImage &&
173
+ featureImage !== 0
174
+ );
175
+ const cssAspectRatio = toCssAspectRatio(aspectRatio);
176
+
177
+ // Generate a unique ID for BaseControl
178
+ const controlId = `inspector-media-upload-${mediaId || "new"}`;
179
+
180
+ // Open native wp.media frame
181
+ const openMedia = useMediaOpen({
182
+ onSelect,
183
+ multiple,
184
+ allowedTypes: ALLOWED_MEDIA_TYPES,
185
+ });
186
+
187
+ const imageDisplay = (
188
+ <>
189
+ {showImagePlaceholder && !hasImage && !hasFeatureImage && (
190
+ <div className="built-inspector-media-preview">
191
+ <Placeholder
192
+ withIllustration={true}
193
+ className="built-editor-panel-image placeholder-image placeholder-image--built"
194
+ />
195
+ </div>
196
+ )}
197
+
198
+ {showImagePlaceholder && hasImage && (
199
+ <div className="built-inspector-media-preview">
200
+ <StyledImageContainer className="built-editor-panel-image">
201
+ <AttachmentImage
202
+ className="built-editor-panel-image"
203
+ imageId={mediaId}
204
+ size="wide_medium"
205
+ includeFigure={false}
206
+ aspectRatio={cssAspectRatio}
207
+ />
208
+ </StyledImageContainer>
209
+ </div>
210
+ )}
211
+
212
+ {showImagePlaceholder && hasFeatureImage && !hasImage && (
213
+ <div className="built-inspector-media-preview">
214
+ <StyledImageContainer className="built-editor-panel-image">
215
+ <AttachmentImage
216
+ className="built-editor-panel-image"
217
+ imageId={featureImage.id || featureImage}
218
+ size="wide_medium"
219
+ includeFigure={false}
220
+ aspectRatio={cssAspectRatio}
221
+ />
222
+ </StyledImageContainer>
223
+ </div>
224
+ )}
225
+ </>
226
+ );
227
+
228
+ const buttons = (
229
+ <StyledActions>
230
+ {!hasImage ? (
231
+ <Button
232
+ __next40pxDefaultSize
233
+ className="is-full-width"
234
+ size="default"
235
+ variant={variant || "secondary"}
236
+ onClick={openMedia}
237
+ >
238
+ {buttonTitle ||
239
+ __("Select or Upload Media", "wp-component-library")}
240
+ </Button>
241
+ ) : (
242
+ <Flex>
243
+ <Button
244
+ __next40pxDefaultSize
245
+ size="default"
246
+ className="is-full-width"
247
+ variant="secondary"
248
+ onClick={openMedia}
249
+ >
250
+ {__("Edit/Replace", "wp-component-library")}
251
+ </Button>
252
+ <Button
253
+ __next40pxDefaultSize
254
+ size="default"
255
+ variant="secondary"
256
+ className="is-full-width"
257
+ onClick={onRemove}
258
+ isDestructive
259
+ >
260
+ {__("Remove", "wp-component-library")}
261
+ </Button>
262
+ </Flex>
263
+ )}
264
+ </StyledActions>
265
+ );
266
+
267
+ // If label or help text is provided, wrap in BaseControl
268
+ if (label || help) {
269
+ return (
270
+ <StyledWrapper maxWidth={maxWidth} $aspectRatio={cssAspectRatio}>
271
+ <BaseControl id={controlId} label={label} help={help}>
272
+ {imageDisplay}
273
+ </BaseControl>
274
+ {buttons}
275
+ </StyledWrapper>
276
+ );
277
+ }
278
+
279
+ // Otherwise, return controls without BaseControl wrapper
280
+ return (
281
+ <StyledWrapper maxWidth={maxWidth} $aspectRatio={cssAspectRatio}>
282
+ <Flex direction="column" expanded={true} style={{ flexGrow: 1 }}>
283
+ {imageDisplay}
284
+ {buttons}
285
+ </Flex>
286
+ </StyledWrapper>
287
+ );
288
+ }
289
+
290
+ export { InspectorMediaUpload };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Toolbar Media Upload Component
3
+ */
4
+ import { MediaUpload, MediaUploadCheck } from "@wordpress/block-editor";
5
+ import { ToolbarButton } from "@wordpress/components";
6
+ import { __ } from "@wordpress/i18n";
7
+ import { pencil } from "@wordpress/icons";
8
+
9
+ const ALLOWED_MEDIA_TYPES = ["image"];
10
+
11
+ /**
12
+ * Toolbar Media Upload
13
+ *
14
+ * @param {object} props
15
+ * @returns {JSX.Element}
16
+ */
17
+ function ToolbarMediaUpload({
18
+ mediaIDs,
19
+ onSelect,
20
+ gallery,
21
+ multiple,
22
+ }) {
23
+ // Handle both ID and object formats for mediaIDs
24
+ const getMediaId = (mediaData) => {
25
+ if (!mediaData) { return null; }
26
+ if (typeof mediaData === "number") { return mediaData; }
27
+ if (typeof mediaData === "object" && mediaData.id) { return mediaData.id; }
28
+ return null;
29
+ };
30
+
31
+ const mediaId = getMediaId(mediaIDs);
32
+
33
+ return (
34
+ <MediaUploadCheck>
35
+ <MediaUpload
36
+ onSelect={onSelect}
37
+ allowedTypes={ALLOWED_MEDIA_TYPES}
38
+ value={mediaId}
39
+ render={({ open }) => (
40
+ <ToolbarButton
41
+ icon={pencil}
42
+ label={__("Edit/Replace Media", "wp-component-library")}
43
+ onClick={open}
44
+ />
45
+ )}
46
+ gallery={gallery}
47
+ multiple={multiple}
48
+ />
49
+ </MediaUploadCheck>
50
+ );
51
+ }
52
+
53
+ export { ToolbarMediaUpload };
@@ -0,0 +1,118 @@
1
+ import { useSettings } from "@wordpress/block-editor";
2
+ import { useMemo } from "@wordpress/element";
3
+ import { __ } from "@wordpress/i18n";
4
+
5
+ /**
6
+ * WordPress core aspect ratio presets (when theme enables defaultAspectRatios).
7
+ * @see https://developer.wordpress.org/news/2024/08/registering-custom-aspect-ratios-in-wordpress-6-6/
8
+ */
9
+ const getDefaultCoreAspectRatios = () => [
10
+ { name: __("Square", "wp-component-library"), ratio: "1" },
11
+ { name: __("Standard", "wp-component-library"), ratio: "4/3" },
12
+ { name: __("Portrait", "wp-component-library"), ratio: "3/4" },
13
+ { name: __("Classic", "wp-component-library"), ratio: "3/2" },
14
+ { name: __("Classic Portrait", "wp-component-library"), ratio: "2/3" },
15
+ { name: __("Wide", "wp-component-library"), ratio: "16/9" },
16
+ { name: __("Extra Wide", "wp-component-library"), ratio: "21/9" },
17
+ { name: __("Tall", "wp-component-library"), ratio: "9/16" },
18
+ ];
19
+
20
+ /**
21
+ * Flatten aspect ratio presets from useSettings (array or { default, theme } object).
22
+ *
23
+ * @param {unknown} setting
24
+ * @return {Array<{ name: string, ratio: string }>}
25
+ */
26
+ function collectAspectRatioPresets(setting) {
27
+ if (!setting) {
28
+ return [];
29
+ }
30
+
31
+ if (Array.isArray(setting)) {
32
+ return setting;
33
+ }
34
+
35
+ if (typeof setting !== "object") {
36
+ return [];
37
+ }
38
+
39
+ const merged = [];
40
+
41
+ for (const key of ["default", "theme", "custom"]) {
42
+ if (Array.isArray(setting[key])) {
43
+ merged.push(...setting[key]);
44
+ }
45
+ }
46
+
47
+ return merged;
48
+ }
49
+
50
+ /**
51
+ * Hook to get aspect ratio options from theme.json
52
+ * Returns whatever aspect ratios are defined (default or custom)
53
+ * Includes "Original" option like core/image block
54
+ */
55
+ export function useAspectRatioOptions() {
56
+ const [
57
+ aspectRatiosSetting,
58
+ defaultPresets,
59
+ themePresets,
60
+ defaultAspectRatiosEnabled,
61
+ ] = useSettings([
62
+ "dimensions.aspectRatios",
63
+ "dimensions.aspectRatios.default",
64
+ "dimensions.aspectRatios.theme",
65
+ "dimensions.defaultAspectRatios",
66
+ ]);
67
+
68
+ return useMemo(() => {
69
+ const originalOption = {
70
+ label: __("Original", "wp-component-library"),
71
+ value: "original",
72
+ };
73
+
74
+ let presets = [
75
+ ...collectAspectRatioPresets(aspectRatiosSetting),
76
+ ...collectAspectRatioPresets(defaultPresets),
77
+ ...collectAspectRatioPresets(themePresets),
78
+ ];
79
+
80
+ // Deduplicate by ratio value.
81
+ const seen = new Set();
82
+ presets = presets.filter((ratio) => {
83
+ if (
84
+ !ratio ||
85
+ typeof ratio !== "object" ||
86
+ !ratio.name ||
87
+ !ratio.ratio
88
+ ) {
89
+ return false;
90
+ }
91
+ if (seen.has(ratio.ratio)) {
92
+ return false;
93
+ }
94
+ seen.add(ratio.ratio);
95
+ return true;
96
+ });
97
+
98
+ if (presets.length === 0 && defaultAspectRatiosEnabled !== false) {
99
+ presets = getDefaultCoreAspectRatios();
100
+ }
101
+
102
+ const ratioOptions = presets
103
+ .filter(
104
+ (ratio) => ratio.ratio !== "original" && ratio.ratio !== "auto",
105
+ )
106
+ .map((ratio) => ({
107
+ label: ratio.name,
108
+ value: ratio.ratio,
109
+ }));
110
+
111
+ return [originalOption, ...ratioOptions];
112
+ }, [
113
+ aspectRatiosSetting,
114
+ defaultPresets,
115
+ themePresets,
116
+ defaultAspectRatiosEnabled,
117
+ ]);
118
+ }