@wordpress-gcb/fields 0.2.0 → 0.2.2

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