@wordpress-gcb/fields 0.2.1 → 0.2.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 (91) hide show
  1. package/dist/conditional-logic.js +83 -0
  2. package/{src → dist}/control-context.js +3 -2
  3. package/{src → dist}/controls/MediaCapabilityGate.js +12 -8
  4. package/dist/controls/MediaPicker.js +149 -0
  5. package/dist/controls/MediaTriggerBadges.js +35 -0
  6. package/{src → dist}/controls/PopoverOrModal.js +49 -43
  7. package/dist/controls/SortableItem.js +126 -0
  8. package/dist/controls/button-group.js +46 -0
  9. package/dist/controls/checkbox-group.js +65 -0
  10. package/dist/controls/checkbox.js +15 -0
  11. package/dist/controls/code.js +24 -0
  12. package/dist/controls/color.js +249 -0
  13. package/dist/controls/date.js +55 -0
  14. package/dist/controls/datetime.js +61 -0
  15. package/dist/controls/email.js +17 -0
  16. package/dist/controls/file.js +163 -0
  17. package/dist/controls/gallery.js +371 -0
  18. package/dist/controls/google-map.js +143 -0
  19. package/dist/controls/heading-level.js +93 -0
  20. package/dist/controls/icon.js +292 -0
  21. package/dist/controls/image.js +360 -0
  22. package/dist/controls/index.js +88 -0
  23. package/dist/controls/message.js +86 -0
  24. package/dist/controls/number.js +19 -0
  25. package/dist/controls/oembed.js +42 -0
  26. package/{src → dist}/controls/page-link.js +1 -2
  27. package/dist/controls/post-object.js +913 -0
  28. package/dist/controls/radio.js +19 -0
  29. package/dist/controls/range.js +108 -0
  30. package/{src → dist}/controls/relationship.js +12 -7
  31. package/dist/controls/repeater.js +277 -0
  32. package/dist/controls/richtext.js +494 -0
  33. package/dist/controls/select.js +144 -0
  34. package/dist/controls/size.js +59 -0
  35. package/dist/controls/spacing.js +172 -0
  36. package/dist/controls/taxonomy.js +569 -0
  37. package/dist/controls/text.js +16 -0
  38. package/dist/controls/textarea.js +17 -0
  39. package/dist/controls/toggle-group.js +28 -0
  40. package/dist/controls/toggle.js +15 -0
  41. package/dist/controls/url.js +235 -0
  42. package/dist/controls/user.js +383 -0
  43. package/{src → dist}/controls/wysiwyg.js +1 -1
  44. package/{src → dist}/hooks/useTokens.js +25 -21
  45. package/{src → dist}/index.js +2 -8
  46. package/dist/inspector.js +163 -0
  47. package/{src → dist}/provider.js +18 -17
  48. package/dist/utils/map-utils.js +54 -0
  49. package/dist/utils/token-helper.js +396 -0
  50. package/{src → dist}/validation-context.js +4 -4
  51. package/package.json +20 -13
  52. package/src/conditional-logic.js +0 -77
  53. package/src/controls/MediaPicker.js +0 -139
  54. package/src/controls/MediaTriggerBadges.js +0 -31
  55. package/src/controls/SortableItem.js +0 -110
  56. package/src/controls/button-group.js +0 -49
  57. package/src/controls/checkbox-group.js +0 -55
  58. package/src/controls/checkbox.js +0 -13
  59. package/src/controls/code.js +0 -21
  60. package/src/controls/color.js +0 -235
  61. package/src/controls/date.js +0 -37
  62. package/src/controls/datetime.js +0 -54
  63. package/src/controls/email.js +0 -15
  64. package/src/controls/file.js +0 -134
  65. package/src/controls/gallery.js +0 -338
  66. package/src/controls/google-map.js +0 -117
  67. package/src/controls/heading-level.js +0 -99
  68. package/src/controls/icon.js +0 -301
  69. package/src/controls/image.js +0 -334
  70. package/src/controls/index.js +0 -95
  71. package/src/controls/message.js +0 -56
  72. package/src/controls/number.js +0 -17
  73. package/src/controls/oembed.js +0 -32
  74. package/src/controls/post-object.js +0 -788
  75. package/src/controls/radio.js +0 -18
  76. package/src/controls/range.js +0 -110
  77. package/src/controls/repeater.js +0 -290
  78. package/src/controls/richtext.js +0 -505
  79. package/src/controls/select.js +0 -141
  80. package/src/controls/size.js +0 -49
  81. package/src/controls/spacing.js +0 -141
  82. package/src/controls/taxonomy.js +0 -488
  83. package/src/controls/text.js +0 -14
  84. package/src/controls/textarea.js +0 -15
  85. package/src/controls/toggle-group.js +0 -34
  86. package/src/controls/toggle.js +0 -13
  87. package/src/controls/url.js +0 -164
  88. package/src/controls/user.js +0 -343
  89. package/src/inspector.js +0 -174
  90. package/src/utils/map-utils.js +0 -51
  91. package/src/utils/token-helper.js +0 -243
@@ -0,0 +1,371 @@
1
+ /**
2
+ * GalleryField — ported verbatim from the original GCB GalleryControlComponent.
3
+ *
4
+ * Stored shape: array of image objects (same shape as ImageField), with
5
+ * focalPoint / size / customWidth / repeat / isFixed preserved per item across
6
+ * media-library reselects.
7
+ *
8
+ * Each row is independently draggable (dnd-kit) and edits open the same
9
+ * focal-point/size panel as the single-image control.
10
+ */
11
+
12
+ import { __ } from '@wordpress/i18n';
13
+ import { useState } from '@wordpress/element';
14
+ import { Button, __experimentalHStack as HStack, __experimentalTruncate as Truncate } from '@wordpress/components';
15
+ import MediaPicker from './MediaPicker';
16
+ import MediaCapabilityGate from './MediaCapabilityGate';
17
+ import PopoverOrModal from './PopoverOrModal';
18
+ import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, DragOverlay } from '@dnd-kit/core';
19
+ import { SortableContext, verticalListSortingStrategy, useSortable, arrayMove } from '@dnd-kit/sortable';
20
+ import { CSS } from '@dnd-kit/utilities';
21
+ import { ImageControlContent } from './image';
22
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
23
+ const TOGGLE_BUTTON_STYLE = {
24
+ width: '100%',
25
+ height: 'auto',
26
+ padding: '12px',
27
+ justifyContent: 'flex-start',
28
+ border: '1px solid #ddd',
29
+ borderRadius: '2px',
30
+ backgroundColor: '#fff'
31
+ };
32
+
33
+ /**
34
+ * Single gallery row — drag handle + thumbnail dropdown trigger + remove button.
35
+ */
36
+ function SortableGalleryImage({
37
+ image,
38
+ onUpdate,
39
+ onRemove,
40
+ control
41
+ }) {
42
+ const {
43
+ attributes,
44
+ listeners,
45
+ setNodeRef,
46
+ transform,
47
+ transition,
48
+ isDragging
49
+ } = useSortable({
50
+ id: image.id
51
+ });
52
+ const style = {
53
+ transform: CSS.Transform.toString(transform),
54
+ transition,
55
+ opacity: isDragging ? 0.5 : 1,
56
+ marginBottom: 8
57
+ };
58
+ const displayTitle = image.title || image.filename || image.alt || __('(no description)', 'gcblite');
59
+ return /*#__PURE__*/_jsx("div", {
60
+ ref: setNodeRef,
61
+ style: style,
62
+ className: "gcb-gallery-image-item",
63
+ children: /*#__PURE__*/_jsxs("div", {
64
+ style: {
65
+ display: 'flex',
66
+ alignItems: 'center',
67
+ gap: 8
68
+ },
69
+ children: [/*#__PURE__*/_jsx("div", {
70
+ ...attributes,
71
+ ...listeners,
72
+ style: {
73
+ cursor: 'grab',
74
+ display: 'flex',
75
+ alignItems: 'center',
76
+ justifyContent: 'center',
77
+ padding: 4,
78
+ flexShrink: 0
79
+ },
80
+ "aria-label": __('Drag to reorder', 'gcblite'),
81
+ children: /*#__PURE__*/_jsx("svg", {
82
+ viewBox: "0 0 20 20",
83
+ width: "16",
84
+ height: "16",
85
+ style: {
86
+ fill: '#666'
87
+ },
88
+ children: /*#__PURE__*/_jsx("path", {
89
+ d: "M7 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 2zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 14zm6-8a2 2 0 1 0-.001-4.001A2 2 0 0 0 13 6zm0 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 14z"
90
+ })
91
+ })
92
+ }), /*#__PURE__*/_jsx(MediaPicker, {
93
+ onSelect: media => onUpdate(image.id, {
94
+ ...image,
95
+ id: media.id,
96
+ url: media.url,
97
+ alt: media.alt || '',
98
+ title: media.title || media.filename || '',
99
+ filename: media.filename || '',
100
+ width: media.width,
101
+ height: media.height,
102
+ filesize: media.filesizeInBytes
103
+ }),
104
+ allowedTypes: ['image'],
105
+ value: image.id,
106
+ render: ({
107
+ open
108
+ }) => /*#__PURE__*/_jsx(PopoverOrModal, {
109
+ modalTitle: displayTitle || __('Gallery image', 'gcblite'),
110
+ dropdownProps: {
111
+ popoverProps: {
112
+ placement: 'left-start'
113
+ }
114
+ },
115
+ renderToggle: ({
116
+ isOpen,
117
+ onToggle
118
+ }) => /*#__PURE__*/_jsx(Button, {
119
+ onClick: onToggle,
120
+ "aria-expanded": isOpen,
121
+ "aria-label": __('Image settings', 'gcblite'),
122
+ className: "gcb-modal-toggle-button gcb-image-control-toggle",
123
+ style: {
124
+ ...TOGGLE_BUTTON_STYLE,
125
+ flex: 1
126
+ },
127
+ children: /*#__PURE__*/_jsxs(HStack, {
128
+ spacing: 3,
129
+ justify: "flex-start",
130
+ children: [/*#__PURE__*/_jsx("span", {
131
+ "aria-hidden": true,
132
+ style: {
133
+ width: 32,
134
+ height: 32,
135
+ borderRadius: '100%',
136
+ backgroundImage: `url(${image.url})`,
137
+ backgroundSize: 'cover',
138
+ backgroundPosition: 'center',
139
+ flexShrink: 0,
140
+ border: '1px solid #ddd',
141
+ display: 'block'
142
+ }
143
+ }), /*#__PURE__*/_jsx(Truncate, {
144
+ numberOfLines: 1,
145
+ children: displayTitle
146
+ })]
147
+ })
148
+ }),
149
+ renderContent: ({
150
+ close
151
+ }) => /*#__PURE__*/_jsxs("div", {
152
+ style: {
153
+ padding: 16,
154
+ minWidth: 280
155
+ },
156
+ children: [/*#__PURE__*/_jsx(ImageControlContent, {
157
+ control: control,
158
+ value: image,
159
+ onChange: newValue => onUpdate(image.id, newValue),
160
+ onReplace: () => {
161
+ close();
162
+ open();
163
+ }
164
+ }), /*#__PURE__*/_jsx("div", {
165
+ style: {
166
+ marginTop: 16,
167
+ paddingTop: 16,
168
+ borderTop: '1px solid #ddd'
169
+ },
170
+ children: /*#__PURE__*/_jsx(Button, {
171
+ onClick: () => {
172
+ onRemove(image.id);
173
+ close();
174
+ },
175
+ variant: "link",
176
+ isDestructive: true,
177
+ style: {
178
+ width: '100%'
179
+ },
180
+ children: __('Remove from gallery', 'gcblite')
181
+ })
182
+ })]
183
+ })
184
+ })
185
+ })]
186
+ })
187
+ });
188
+ }
189
+ export default function GalleryField({
190
+ control,
191
+ value,
192
+ onChange
193
+ }) {
194
+ const [activeId, setActiveId] = useState(null);
195
+ const images = Array.isArray(value) ? value : [];
196
+ const sensors = useSensors(useSensor(PointerSensor, {
197
+ activationConstraint: {
198
+ distance: 8
199
+ }
200
+ }));
201
+ const handleDragStart = event => setActiveId(event.active.id);
202
+ const handleDragEnd = event => {
203
+ const {
204
+ active,
205
+ over
206
+ } = event;
207
+ setActiveId(null);
208
+ if (over && active.id !== over.id) {
209
+ const oldIndex = images.findIndex(img => img.id === active.id);
210
+ const newIndex = images.findIndex(img => img.id === over.id);
211
+ onChange(arrayMove(images, oldIndex, newIndex));
212
+ }
213
+ };
214
+ const handleSelect = media => {
215
+ const newImages = Array.isArray(media) ? media : [media];
216
+ const existingMap = new Map(images.map(img => [img.id, img]));
217
+ const formatted = newImages.map(img => {
218
+ const existing = existingMap.get(img.id);
219
+ if (existing) {
220
+ return {
221
+ ...existing,
222
+ url: img.url,
223
+ alt: img.alt || existing.alt,
224
+ title: img.title || img.filename || existing.title,
225
+ filename: img.filename || existing.filename,
226
+ width: img.width,
227
+ height: img.height,
228
+ filesize: img.filesizeInBytes
229
+ };
230
+ }
231
+ return {
232
+ id: img.id,
233
+ url: img.url,
234
+ alt: img.alt || '',
235
+ title: img.title || img.filename || '',
236
+ filename: img.filename || '',
237
+ width: img.width,
238
+ height: img.height,
239
+ filesize: img.filesizeInBytes,
240
+ focalPoint: {
241
+ x: 0.5,
242
+ y: 0.5
243
+ },
244
+ size: 'cover',
245
+ customWidth: '',
246
+ repeat: true,
247
+ isFixed: false
248
+ };
249
+ });
250
+ onChange(formatted);
251
+ };
252
+ const removeImage = imageId => onChange(images.filter(img => img.id !== imageId));
253
+ const updateImage = (imageId, updates) => onChange(images.map(img => img.id === imageId ? {
254
+ ...img,
255
+ ...updates
256
+ } : img));
257
+ return /*#__PURE__*/_jsxs("div", {
258
+ className: "components-base-control gcb-gallery-control",
259
+ children: [/*#__PURE__*/_jsx("div", {
260
+ className: "components-base-control__field",
261
+ children: /*#__PURE__*/_jsx("label", {
262
+ className: "components-base-control__label",
263
+ children: control.label
264
+ })
265
+ }), control.helpText && /*#__PURE__*/_jsx("p", {
266
+ className: "components-base-control__help",
267
+ children: control.helpText
268
+ }), /*#__PURE__*/_jsx(MediaCapabilityGate, {
269
+ children: /*#__PURE__*/_jsx(MediaPicker, {
270
+ onSelect: handleSelect,
271
+ allowedTypes: ['image'],
272
+ multiple: true,
273
+ gallery: true,
274
+ value: images.map(img => img.id),
275
+ render: ({
276
+ open
277
+ }) => /*#__PURE__*/_jsx("div", {
278
+ className: "gcb-gallery-control-content",
279
+ children: images.length === 0 ? /*#__PURE__*/_jsx(Button, {
280
+ onClick: open,
281
+ variant: "secondary",
282
+ style: {
283
+ marginBottom: 8
284
+ },
285
+ children: __('Add images', 'gcblite')
286
+ }) : /*#__PURE__*/_jsxs(_Fragment, {
287
+ children: [/*#__PURE__*/_jsxs(DndContext, {
288
+ sensors: sensors,
289
+ collisionDetection: closestCenter,
290
+ onDragStart: handleDragStart,
291
+ onDragEnd: handleDragEnd,
292
+ children: [/*#__PURE__*/_jsx(SortableContext, {
293
+ items: images.map(img => img.id),
294
+ strategy: verticalListSortingStrategy,
295
+ children: /*#__PURE__*/_jsx("div", {
296
+ className: "gcb-gallery-items",
297
+ children: images.map(image => /*#__PURE__*/_jsx(SortableGalleryImage, {
298
+ image: image,
299
+ onUpdate: updateImage,
300
+ onRemove: removeImage,
301
+ control: control
302
+ }, image.id))
303
+ })
304
+ }), /*#__PURE__*/_jsx(DragOverlay, {
305
+ children: activeId ? (() => {
306
+ const active = images.find(img => img.id === activeId);
307
+ const title = active?.title || active?.filename || active?.alt || __('(no description)', 'gcblite');
308
+ return /*#__PURE__*/_jsxs("div", {
309
+ style: {
310
+ display: 'flex',
311
+ alignItems: 'center',
312
+ gap: 8,
313
+ padding: '8px 12px',
314
+ background: 'white',
315
+ borderRadius: 2,
316
+ boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
317
+ opacity: 0.9,
318
+ maxWidth: 400,
319
+ border: '1px solid #ddd'
320
+ },
321
+ children: [/*#__PURE__*/_jsx("svg", {
322
+ viewBox: "0 0 20 20",
323
+ width: "16",
324
+ height: "16",
325
+ style: {
326
+ fill: '#666'
327
+ },
328
+ children: /*#__PURE__*/_jsx("path", {
329
+ d: "M7 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 2zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 14zm6-8a2 2 0 1 0-.001-4.001A2 2 0 0 0 13 6zm0 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 14z"
330
+ })
331
+ }), /*#__PURE__*/_jsx("span", {
332
+ "aria-hidden": true,
333
+ style: {
334
+ width: 32,
335
+ height: 32,
336
+ borderRadius: '100%',
337
+ backgroundImage: `url(${active?.url})`,
338
+ backgroundSize: 'cover',
339
+ backgroundPosition: 'center',
340
+ flexShrink: 0,
341
+ border: '1px solid #ddd',
342
+ display: 'block'
343
+ }
344
+ }), /*#__PURE__*/_jsx("span", {
345
+ style: {
346
+ flex: 1,
347
+ overflow: 'hidden',
348
+ textOverflow: 'ellipsis',
349
+ whiteSpace: 'nowrap',
350
+ fontSize: 13
351
+ },
352
+ children: title
353
+ })]
354
+ });
355
+ })() : null
356
+ })]
357
+ }), /*#__PURE__*/_jsx(Button, {
358
+ onClick: open,
359
+ variant: "secondary",
360
+ style: {
361
+ width: '100%',
362
+ marginTop: 12
363
+ },
364
+ children: __('Add more images', 'gcblite')
365
+ })]
366
+ })
367
+ })
368
+ })
369
+ })]
370
+ });
371
+ }
@@ -0,0 +1,143 @@
1
+ import { BaseControl, Notice, TextControl } from '@wordpress/components';
2
+ import { useState, useEffect, useRef, useCallback } from '@wordpress/element';
3
+ import { __ } from '@wordpress/i18n';
4
+ import { useGoogleMapsEnabled } from '../provider';
5
+
6
+ /**
7
+ * Google Map control — address search with autocomplete + interactive map.
8
+ *
9
+ * Stored shape: { address, lat, lng, zoom }
10
+ *
11
+ * Maps features are gated on the host signalling a configured API key —
12
+ * `googleMapsEnabled` from GcbFieldsProvider, falling back to
13
+ * `window.gcbLite.googleMaps.hasApiKey`. With no key, falls back to a plain
14
+ * address input that sets `address` only. (The Maps JS SDK itself is enqueued
15
+ * by the host when a key exists; this control just reads window.google.)
16
+ */
17
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
+ export default function GoogleMapField({
19
+ control,
20
+ value,
21
+ onChange
22
+ }) {
23
+ const location = value && typeof value === 'object' ? value : {
24
+ address: '',
25
+ lat: null,
26
+ lng: null,
27
+ zoom: 14
28
+ };
29
+ const hasApiKey = useGoogleMapsEnabled();
30
+ const [address, setAddress] = useState(location.address || '');
31
+ useEffect(() => {
32
+ setAddress(location.address || '');
33
+ }, [location.address]);
34
+ const inputRef = useRef(null);
35
+ const mapRef = useRef(null);
36
+ const mapInstance = useRef(null);
37
+ const markerRef = useRef(null);
38
+ const update = useCallback(next => {
39
+ onChange({
40
+ ...location,
41
+ ...next
42
+ });
43
+ }, [onChange, location]);
44
+
45
+ // Wire up Places autocomplete on the input.
46
+ useEffect(() => {
47
+ if (!hasApiKey || !inputRef.current || !window.google?.maps?.places) return;
48
+ const ac = new window.google.maps.places.Autocomplete(inputRef.current, {
49
+ fields: ['formatted_address', 'geometry']
50
+ });
51
+ const listener = ac.addListener('place_changed', () => {
52
+ const place = ac.getPlace();
53
+ if (!place.geometry) return;
54
+ const lat = place.geometry.location.lat();
55
+ const lng = place.geometry.location.lng();
56
+ update({
57
+ address: place.formatted_address || '',
58
+ lat,
59
+ lng
60
+ });
61
+ });
62
+ return () => window.google.maps.event.removeListener(listener);
63
+ }, [hasApiKey, update]);
64
+
65
+ // Wire up the map preview when we have coords.
66
+ useEffect(() => {
67
+ if (!hasApiKey || !mapRef.current || !window.google?.maps) return;
68
+ if (location.lat == null || location.lng == null) return;
69
+ const center = {
70
+ lat: Number(location.lat),
71
+ lng: Number(location.lng)
72
+ };
73
+ if (!mapInstance.current) {
74
+ mapInstance.current = new window.google.maps.Map(mapRef.current, {
75
+ center,
76
+ zoom: location.zoom || 14,
77
+ disableDefaultUI: true,
78
+ clickableIcons: false
79
+ });
80
+ markerRef.current = new window.google.maps.Marker({
81
+ position: center,
82
+ map: mapInstance.current,
83
+ draggable: true
84
+ });
85
+ markerRef.current.addListener('dragend', e => {
86
+ update({
87
+ lat: e.latLng.lat(),
88
+ lng: e.latLng.lng()
89
+ });
90
+ });
91
+ } else {
92
+ mapInstance.current.setCenter(center);
93
+ markerRef.current.setPosition(center);
94
+ }
95
+ }, [hasApiKey, location.lat, location.lng, location.zoom, update]);
96
+ if (!hasApiKey) {
97
+ return /*#__PURE__*/_jsxs(BaseControl, {
98
+ label: control.label,
99
+ help: control.helpText,
100
+ __nextHasNoMarginBottom: true,
101
+ children: [/*#__PURE__*/_jsx(Notice, {
102
+ status: "warning",
103
+ isDismissible: false,
104
+ children: __('No Google Maps API key configured. Set one with the `gcb_google_maps_api_key` filter to enable autocomplete and the map preview.', 'gcblite')
105
+ }), /*#__PURE__*/_jsx(TextControl, {
106
+ label: "",
107
+ hideLabelFromVision: true,
108
+ value: address,
109
+ onChange: next => {
110
+ setAddress(next);
111
+ update({
112
+ address: next
113
+ });
114
+ },
115
+ placeholder: __('Enter an address…', 'gcblite'),
116
+ __nextHasNoMarginBottom: true
117
+ })]
118
+ });
119
+ }
120
+ return /*#__PURE__*/_jsxs(BaseControl, {
121
+ label: control.label,
122
+ help: control.helpText,
123
+ __nextHasNoMarginBottom: true,
124
+ children: [/*#__PURE__*/_jsx(TextControl, {
125
+ label: "",
126
+ hideLabelFromVision: true,
127
+ ref: inputRef,
128
+ value: address,
129
+ onChange: setAddress,
130
+ placeholder: __('Start typing an address…', 'gcblite'),
131
+ __nextHasNoMarginBottom: true
132
+ }), location.lat != null && location.lng != null && /*#__PURE__*/_jsx("div", {
133
+ ref: mapRef,
134
+ style: {
135
+ width: '100%',
136
+ height: 200,
137
+ marginTop: 8,
138
+ borderRadius: 4,
139
+ overflow: 'hidden'
140
+ }
141
+ })]
142
+ });
143
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * HeadingLevel — compound input: a text field for the heading content
3
+ * and an inline dropdown for the semantic level. Visually mirrors WP's
4
+ * UnitControl (the "10 px" combo): input on the left, suffix selector
5
+ * on the right, single 40px-tall row.
6
+ *
7
+ * Stored shape:
8
+ * { text: 'Section title', level: 'h2' }
9
+ *
10
+ * React frontend usage:
11
+ * const { text, level } = heading || {};
12
+ * if (!text) return null;
13
+ * const Tag = level || 'h2';
14
+ * return <Tag className="...">{text}</Tag>;
15
+ *
16
+ * Config:
17
+ * levels ['h1','h2','h3','h4','h5','h6','p','div','span'] (default)
18
+ * — set to your own subset to restrict choice.
19
+ * default { text, level } — initial value when the attribute is empty
20
+ * placeholder string — placeholder for the text input
21
+ *
22
+ * Accessibility: `div` and `span` are non-semantic; the helper text turns
23
+ * red when one is selected so authors see the trade-off before shipping.
24
+ *
25
+ * Implementation note: uses WP's @experimental `InputControl` primitive
26
+ * directly — same component UnitControl is built on top of. The `suffix`
27
+ * slot is exactly the "input + dropdown" pattern; the component handles
28
+ * the row height, focus ring, and emotion class plumbing so we don't.
29
+ */
30
+
31
+ import { __ } from '@wordpress/i18n';
32
+ import { __experimentalInputControl as InputControl, Notice } from '@wordpress/components';
33
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
34
+ const ALL_LEVELS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div', 'span'];
35
+ const HEADING_LEVELS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']);
36
+ function resolveLevels(control) {
37
+ if (Array.isArray(control.levels) && control.levels.length > 0) {
38
+ return control.levels.filter(l => ALL_LEVELS.includes(l));
39
+ }
40
+ return ALL_LEVELS;
41
+ }
42
+ export default function HeadingLevelField({
43
+ control,
44
+ value,
45
+ onChange
46
+ }) {
47
+ const levels = resolveLevels(control);
48
+ const heading = value && typeof value === 'object' ? value : {};
49
+ const text = heading.text ?? control.default?.text ?? '';
50
+ const level = heading.level ?? control.default?.level ?? levels[0] ?? 'h2';
51
+ const isNonSemantic = !HEADING_LEVELS.has(level);
52
+ const update = patch => onChange({
53
+ text,
54
+ level,
55
+ ...patch
56
+ });
57
+ const levelSelect = /*#__PURE__*/_jsx("select", {
58
+ className: "components-unit-control__select",
59
+ "aria-label": __('Heading level', 'gcblite'),
60
+ value: level,
61
+ onChange: e => update({
62
+ level: e.target.value
63
+ }),
64
+ children: levels.map(lvl => /*#__PURE__*/_jsx("option", {
65
+ value: lvl,
66
+ children: lvl.toUpperCase()
67
+ }, lvl))
68
+ });
69
+ return /*#__PURE__*/_jsxs("div", {
70
+ className: "components-base-control gcb-heading-level-control",
71
+ children: [/*#__PURE__*/_jsxs("div", {
72
+ className: "components-base-control__field",
73
+ children: [/*#__PURE__*/_jsx(InputControl, {
74
+ label: control.label,
75
+ value: text,
76
+ placeholder: control.placeholder || __('Heading text', 'gcblite'),
77
+ onChange: next => update({
78
+ text: next ?? ''
79
+ }),
80
+ suffix: levelSelect,
81
+ __next40pxDefaultSize: true
82
+ }), isNonSemantic && /*#__PURE__*/_jsx(Notice, {
83
+ status: "warning",
84
+ isDismissible: false,
85
+ className: "gcb-heading-level-control__warning",
86
+ children: __('Non-heading elements are skipped by screen-reader heading navigation. Prefer H1–H6 for content titles.', 'gcblite')
87
+ })]
88
+ }), control.helpText && /*#__PURE__*/_jsx("p", {
89
+ className: "components-base-control__help",
90
+ children: control.helpText
91
+ })]
92
+ });
93
+ }