@wordpress-gcb/fields 0.2.1 → 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 (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 +241 -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 +141 -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,494 @@
1
+ /**
2
+ * RichtextField — modern alternative to the `wysiwyg` (TinyMCE) control.
3
+ *
4
+ * Built on Tiptap (ProseMirror under the hood). Lighter, no jQuery, no
5
+ * global state — fits the rest of the Inspector model where every
6
+ * control is a self-contained React component.
7
+ *
8
+ * Stored shape: HTML string (e.g. `<p>Hello <strong>world</strong></p>`).
9
+ * Compatible with WP's standard wp_kses() flow and any frontend that
10
+ * renders HTML via dangerouslySetInnerHTML.
11
+ *
12
+ * Why TWO rich-text controls? The existing `wysiwyg` control wraps
13
+ * wp_editor() / TinyMCE — it's what authors expect on every WordPress
14
+ * install and we keep it for backwards compat. `richtext` is the new
15
+ * default for themes that don't need TinyMCE-era plugin compatibility
16
+ * and want a smaller, more modern editor.
17
+ *
18
+ * Toolbar uses @wordpress/components so it visually matches the rest
19
+ * of our Inspector. Image button opens wp.media() and inserts an <img>
20
+ * referencing the existing media library — same lower-level API as
21
+ * MediaPicker, no @wordpress/block-editor dependency required (works
22
+ * on any admin screen).
23
+ */
24
+
25
+ import { __ } from '@wordpress/i18n';
26
+ import { useState, useEffect, useContext } from '@wordpress/element';
27
+ import { Button, Icon, Modal, Popover, TextControl } from '@wordpress/components';
28
+ import { ControlContext } from '../control-context';
29
+ import PopoverOrModal from './PopoverOrModal';
30
+ import { formatBold, formatItalic, formatStrikethrough, formatListBullets, formatListNumbered, code as codeIcon, quote as quoteIcon, link as linkIcon, image as imageIcon, undo as undoIcon, redo as redoIcon, pencil as editIcon } from '@wordpress/icons';
31
+ import { useEditor, EditorContent } from '@tiptap/react';
32
+ import StarterKit from '@tiptap/starter-kit';
33
+ import Link from '@tiptap/extension-link';
34
+ import Image from '@tiptap/extension-image';
35
+
36
+ /**
37
+ * Open wp.media() and resolve with the picked image. Same wp.media
38
+ * API our MediaPicker shim uses — no @wordpress/block-editor required.
39
+ */
40
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
41
+ function pickImageFromMedia() {
42
+ return new Promise(resolve => {
43
+ if (typeof window === 'undefined' || !window.wp?.media) {
44
+ // eslint-disable-next-line no-console
45
+ console.warn('gcb-lite: wp.media not loaded — call wp_enqueue_media() on this screen.');
46
+ resolve(null);
47
+ return;
48
+ }
49
+ const frame = window.wp.media({
50
+ title: __('Insert image', 'gcblite'),
51
+ button: {
52
+ text: __('Insert image', 'gcblite')
53
+ },
54
+ library: {
55
+ type: ['image']
56
+ },
57
+ multiple: false
58
+ });
59
+ frame.on('select', () => {
60
+ const a = frame.state().get('selection').first()?.toJSON();
61
+ resolve(a || null);
62
+ });
63
+ frame.on('close', () => resolve(null));
64
+ frame.open();
65
+ });
66
+ }
67
+ function ToolbarSeparator() {
68
+ return /*#__PURE__*/_jsx("span", {
69
+ "aria-hidden": true,
70
+ style: {
71
+ display: 'inline-block',
72
+ width: 1,
73
+ height: 24,
74
+ background: '#ddd',
75
+ margin: '0 4px',
76
+ alignSelf: 'center'
77
+ }
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Toolbar button. Uses @wordpress/icons so the toolbar visually
83
+ * matches the rest of WP admin (block-editor toolbar, post-meta
84
+ * controls, etc.). The icon prop on @wordpress/components Button
85
+ * renders the SVG at WP's standard 24×24.
86
+ */
87
+ function TbButton({
88
+ icon,
89
+ label,
90
+ isPressed,
91
+ disabled,
92
+ onClick
93
+ }) {
94
+ return /*#__PURE__*/_jsx(Button, {
95
+ icon: icon,
96
+ label: label,
97
+ showTooltip: true,
98
+ onClick: onClick,
99
+ disabled: disabled,
100
+ "aria-pressed": isPressed,
101
+ className: "gcb-richtext-toolbar__btn"
102
+ });
103
+ }
104
+ function LinkPopover({
105
+ editor,
106
+ anchor,
107
+ onClose
108
+ }) {
109
+ const initial = editor.getAttributes('link').href || '';
110
+ const [url, setUrl] = useState(initial);
111
+ const apply = () => {
112
+ if (!url) {
113
+ editor.chain().focus().unsetLink().run();
114
+ } else {
115
+ editor.chain().focus().extendMarkRange('link').setLink({
116
+ href: url
117
+ }).run();
118
+ }
119
+ onClose();
120
+ };
121
+ return /*#__PURE__*/_jsx(Popover, {
122
+ anchor: anchor,
123
+ onClose: onClose,
124
+ placement: "bottom-start",
125
+ children: /*#__PURE__*/_jsxs("div", {
126
+ style: {
127
+ padding: 12,
128
+ minWidth: 280
129
+ },
130
+ children: [/*#__PURE__*/_jsx(TextControl, {
131
+ label: __('Link URL', 'gcblite'),
132
+ value: url,
133
+ onChange: setUrl,
134
+ placeholder: "https://example.com",
135
+ __nextHasNoMarginBottom: true,
136
+ __next40pxDefaultSize: true
137
+ }), /*#__PURE__*/_jsxs("div", {
138
+ style: {
139
+ display: 'flex',
140
+ gap: 8,
141
+ marginTop: 8
142
+ },
143
+ children: [/*#__PURE__*/_jsx(Button, {
144
+ variant: "primary",
145
+ onClick: apply,
146
+ children: __('Apply', 'gcblite')
147
+ }), initial && /*#__PURE__*/_jsx(Button, {
148
+ variant: "tertiary",
149
+ onClick: () => {
150
+ editor.chain().focus().unsetLink().run();
151
+ onClose();
152
+ },
153
+ children: __('Remove', 'gcblite')
154
+ })]
155
+ })]
156
+ })
157
+ });
158
+ }
159
+ function Toolbar({
160
+ editor,
161
+ control
162
+ }) {
163
+ const [linkPopoverAnchor, setLinkPopoverAnchor] = useState(null);
164
+ if (!editor) return null;
165
+ const headingLevels = control.headingLevels || [2, 3, 4];
166
+ // Build heading select options. 'p' is paragraph (no heading).
167
+ const headingValue = editor.isActive('heading', {
168
+ level: headingLevels[0]
169
+ }) ? `h${headingLevels[0]}` : editor.isActive('heading', {
170
+ level: headingLevels[1]
171
+ }) ? `h${headingLevels[1]}` : editor.isActive('heading', {
172
+ level: headingLevels[2]
173
+ }) ? `h${headingLevels[2]}` : 'p';
174
+ const setHeading = val => {
175
+ if (val === 'p') {
176
+ editor.chain().focus().setParagraph().run();
177
+ } else {
178
+ const level = parseInt(val.slice(1), 10);
179
+ editor.chain().focus().toggleHeading({
180
+ level
181
+ }).run();
182
+ }
183
+ };
184
+ const insertImage = async () => {
185
+ const img = await pickImageFromMedia();
186
+ if (!img?.url) return;
187
+ editor.chain().focus().setImage({
188
+ src: img.url,
189
+ alt: img.alt || ''
190
+ }).run();
191
+ };
192
+ return /*#__PURE__*/_jsxs("div", {
193
+ className: "gcb-richtext-toolbar",
194
+ style: {
195
+ display: 'flex',
196
+ flexWrap: 'wrap',
197
+ alignItems: 'center',
198
+ gap: 2,
199
+ padding: 6,
200
+ // Light border — the surface should feel calm. Matches the
201
+ // editor body below; their shared edge (toolbar-bottom +
202
+ // body-top) collapses to one line.
203
+ border: '1px solid #ddd',
204
+ borderBottom: '1px solid #ddd',
205
+ background: '#fff',
206
+ borderRadius: '8px 8px 0 0'
207
+ },
208
+ children: [/*#__PURE__*/_jsxs("select", {
209
+ value: headingValue,
210
+ onChange: e => setHeading(e.target.value),
211
+ className: "gcb-richtext-block-select",
212
+ "aria-label": __('Text style', 'gcblite'),
213
+ children: [/*#__PURE__*/_jsx("option", {
214
+ value: "p",
215
+ children: __('Paragraph', 'gcblite')
216
+ }), headingLevels.map(l => /*#__PURE__*/_jsx("option", {
217
+ value: `h${l}`,
218
+ children: `Heading ${l}`
219
+ }, l))]
220
+ }), /*#__PURE__*/_jsx(ToolbarSeparator, {}), /*#__PURE__*/_jsx(TbButton, {
221
+ icon: formatBold,
222
+ label: __('Bold', 'gcblite'),
223
+ isPressed: editor.isActive('bold'),
224
+ onClick: () => editor.chain().focus().toggleBold().run()
225
+ }), /*#__PURE__*/_jsx(TbButton, {
226
+ icon: formatItalic,
227
+ label: __('Italic', 'gcblite'),
228
+ isPressed: editor.isActive('italic'),
229
+ onClick: () => editor.chain().focus().toggleItalic().run()
230
+ }), /*#__PURE__*/_jsx(TbButton, {
231
+ icon: formatStrikethrough,
232
+ label: __('Strikethrough', 'gcblite'),
233
+ isPressed: editor.isActive('strike'),
234
+ onClick: () => editor.chain().focus().toggleStrike().run()
235
+ }), /*#__PURE__*/_jsx(TbButton, {
236
+ icon: codeIcon,
237
+ label: __('Inline code', 'gcblite'),
238
+ isPressed: editor.isActive('code'),
239
+ onClick: () => editor.chain().focus().toggleCode().run()
240
+ }), /*#__PURE__*/_jsx(ToolbarSeparator, {}), /*#__PURE__*/_jsx(TbButton, {
241
+ icon: formatListBullets,
242
+ label: __('Bulleted list', 'gcblite'),
243
+ isPressed: editor.isActive('bulletList'),
244
+ onClick: () => editor.chain().focus().toggleBulletList().run()
245
+ }), /*#__PURE__*/_jsx(TbButton, {
246
+ icon: formatListNumbered,
247
+ label: __('Numbered list', 'gcblite'),
248
+ isPressed: editor.isActive('orderedList'),
249
+ onClick: () => editor.chain().focus().toggleOrderedList().run()
250
+ }), /*#__PURE__*/_jsx(TbButton, {
251
+ icon: quoteIcon,
252
+ label: __('Blockquote', 'gcblite'),
253
+ isPressed: editor.isActive('blockquote'),
254
+ onClick: () => editor.chain().focus().toggleBlockquote().run()
255
+ }), /*#__PURE__*/_jsx(ToolbarSeparator, {}), /*#__PURE__*/_jsx(TbButton, {
256
+ icon: linkIcon,
257
+ label: __('Link', 'gcblite'),
258
+ isPressed: editor.isActive('link'),
259
+ onClick: e => setLinkPopoverAnchor(e.currentTarget)
260
+ }), control.allowImages !== false && /*#__PURE__*/_jsx(TbButton, {
261
+ icon: imageIcon,
262
+ label: __('Insert image', 'gcblite'),
263
+ onClick: insertImage
264
+ }), /*#__PURE__*/_jsx(ToolbarSeparator, {}), /*#__PURE__*/_jsx(TbButton, {
265
+ icon: undoIcon,
266
+ label: __('Undo', 'gcblite'),
267
+ onClick: () => editor.chain().focus().undo().run(),
268
+ disabled: !editor.can().undo()
269
+ }), /*#__PURE__*/_jsx(TbButton, {
270
+ icon: redoIcon,
271
+ label: __('Redo', 'gcblite'),
272
+ onClick: () => editor.chain().focus().redo().run(),
273
+ disabled: !editor.can().redo()
274
+ }), linkPopoverAnchor && /*#__PURE__*/_jsx(LinkPopover, {
275
+ editor: editor,
276
+ anchor: linkPopoverAnchor,
277
+ onClose: () => setLinkPopoverAnchor(null)
278
+ })]
279
+ });
280
+ }
281
+
282
+ /**
283
+ * Strip a chunk of HTML down to a plain-text preview for the
284
+ * collapsed button in sidebar variant. No DOMParser — we run inside
285
+ * the block-editor JS bundle which has access to document, but a
286
+ * regex-based strip is plenty for a short summary line.
287
+ */
288
+ function htmlToPreview(html, limit = 80) {
289
+ if (!html) return '';
290
+ const text = String(html).replace(/<[^>]+>/g, ' ').replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim();
291
+ if (text.length <= limit) return text;
292
+ return text.slice(0, limit - 1) + '…';
293
+ }
294
+
295
+ /**
296
+ * The actual Tiptap editor surface — toolbar + EditorContent. Used
297
+ * inline on wide surfaces (meta-box, options page) and inside a Modal
298
+ * on the narrow block Inspector sidebar.
299
+ */
300
+ function RichtextEditorSurface({
301
+ editor,
302
+ control
303
+ }) {
304
+ if (!editor) return null;
305
+ return /*#__PURE__*/_jsxs(_Fragment, {
306
+ children: [/*#__PURE__*/_jsx(Toolbar, {
307
+ editor: editor,
308
+ control: control
309
+ }), /*#__PURE__*/_jsx(EditorContent, {
310
+ editor: editor,
311
+ className: "gcb-richtext-editor"
312
+ })]
313
+ });
314
+ }
315
+
316
+ /**
317
+ * Resolve the display mode for the field. Authors can pin a specific
318
+ * mode via control.display ('inline' | 'popover' | 'modal'); otherwise
319
+ * we pick the best fit for the surface:
320
+ * - sidebar variant (narrow ~280px block Inspector) → 'popover' so
321
+ * the editor hangs off to the side, leaving the canvas visible
322
+ * for live preview of edits
323
+ * - metabox/wide variants → 'inline' (toolbar + editor fit fine)
324
+ */
325
+ function resolveDisplay(control, ctx) {
326
+ if (control.display && ['inline', 'popover', 'modal'].includes(control.display)) {
327
+ return control.display;
328
+ }
329
+ if (ctx?.variant === 'sidebar') return 'popover';
330
+ return 'inline';
331
+ }
332
+ export default function RichtextField({
333
+ control,
334
+ value,
335
+ onChange
336
+ }) {
337
+ const ctx = useContext(ControlContext);
338
+ const display = resolveDisplay(control, ctx);
339
+ const headingLevels = control.headingLevels || [2, 3, 4];
340
+ const [modalOpen, setModalOpen] = useState(false);
341
+ const editor = useEditor({
342
+ extensions: [StarterKit.configure({
343
+ // Disable headings here; we'll re-add only the levels the
344
+ // theme has allowed so the toolbar select stays in sync.
345
+ heading: {
346
+ levels: headingLevels
347
+ }
348
+ }), Link.configure({
349
+ openOnClick: false,
350
+ HTMLAttributes: {
351
+ rel: 'noopener noreferrer'
352
+ }
353
+ }), Image.configure({
354
+ HTMLAttributes: {
355
+ class: 'gcb-richtext-image'
356
+ }
357
+ })],
358
+ content: value || '',
359
+ onUpdate: ({
360
+ editor: ed
361
+ }) => {
362
+ const html = ed.getHTML();
363
+ // Tiptap returns "<p></p>" for an empty document. Normalise
364
+ // to '' so validation's required-check still triggers on
365
+ // truly-empty input.
366
+ onChange(html === '<p></p>' ? '' : html);
367
+ }
368
+ });
369
+
370
+ // Sync external value changes (e.g. value reset, conditional logic
371
+ // swap) without re-triggering onUpdate.
372
+ useEffect(() => {
373
+ if (!editor) return;
374
+ const current = editor.getHTML();
375
+ const incoming = value || '';
376
+ if (current === incoming) return;
377
+ if (current === '<p></p>' && incoming === '') return;
378
+ editor.commands.setContent(incoming, false);
379
+ }, [value, editor]);
380
+
381
+ // Popover or Modal: collapsed button + opens the editor in an
382
+ // overlay. Popover (default for sidebar) anchors to the side of
383
+ // the field, leaving the canvas visible for live-update editing.
384
+ // Modal is for surfaces where you want focus-mode authoring and
385
+ // don't mind covering the canvas.
386
+ if (display === 'popover' || display === 'modal') {
387
+ const preview = htmlToPreview(value);
388
+ const triggerButton = ({
389
+ onToggle,
390
+ isOpen
391
+ }) => /*#__PURE__*/_jsxs(Button, {
392
+ onClick: onToggle,
393
+ "aria-expanded": isOpen,
394
+ className: "gcb-richtext-control__open",
395
+ children: [/*#__PURE__*/_jsx("span", {
396
+ className: "gcb-richtext-control__open-text",
397
+ children: preview || control.placeholder || __('Edit rich text…', 'gcblite')
398
+ }), /*#__PURE__*/_jsx(Icon, {
399
+ icon: editIcon,
400
+ size: 20,
401
+ className: "gcb-richtext-control__open-icon"
402
+ })]
403
+ });
404
+ const editorPanel = ({
405
+ close,
406
+ variant
407
+ }) => /*#__PURE__*/_jsx("div", {
408
+ className: `gcb-richtext-control__panel gcb-richtext-control__panel--${variant}`,
409
+ children: /*#__PURE__*/_jsx(RichtextEditorSurface, {
410
+ editor: editor,
411
+ control: control
412
+ })
413
+ });
414
+
415
+ // For 'modal' mode, force PopoverOrModal into modal by mounting
416
+ // inside a metabox-flavoured ControlContext. (PopoverOrModal
417
+ // dispatches purely on ctx.variant.) Pinning it explicitly
418
+ // keeps the per-field opt-in honest regardless of where the
419
+ // control is used.
420
+ if (display === 'modal') {
421
+ return /*#__PURE__*/_jsxs("div", {
422
+ className: "components-base-control gcb-richtext-control gcb-richtext-control--popout",
423
+ children: [/*#__PURE__*/_jsxs("div", {
424
+ className: "components-base-control__field",
425
+ children: [control.label && /*#__PURE__*/_jsx("label", {
426
+ className: "components-base-control__label",
427
+ children: control.label
428
+ }), /*#__PURE__*/_jsx(ControlContext.Provider, {
429
+ value: {
430
+ variant: 'metabox'
431
+ },
432
+ children: /*#__PURE__*/_jsx(PopoverOrModal, {
433
+ modalTitle: control.label || __('Edit rich text', 'gcblite'),
434
+ modalSize: "large",
435
+ renderToggle: triggerButton,
436
+ renderContent: editorPanel
437
+ })
438
+ })]
439
+ }), control.helpText && /*#__PURE__*/_jsx("p", {
440
+ className: "components-base-control__help",
441
+ children: control.helpText
442
+ })]
443
+ });
444
+ }
445
+
446
+ // Popover variant — used by default in sidebar. PopoverOrModal
447
+ // reads the surrounding variant, which is 'sidebar' here, so
448
+ // it'll render as a Dropdown popover anchored to the trigger.
449
+ return /*#__PURE__*/_jsxs("div", {
450
+ className: "components-base-control gcb-richtext-control gcb-richtext-control--popout",
451
+ children: [/*#__PURE__*/_jsxs("div", {
452
+ className: "components-base-control__field",
453
+ children: [control.label && /*#__PURE__*/_jsx("label", {
454
+ className: "components-base-control__label",
455
+ children: control.label
456
+ }), /*#__PURE__*/_jsx(PopoverOrModal, {
457
+ modalTitle: control.label || __('Edit rich text', 'gcblite'),
458
+ modalSize: "large",
459
+ dropdownProps: {
460
+ popoverProps: {
461
+ placement: 'left-start',
462
+ className: 'gcb-richtext-control__popover'
463
+ }
464
+ },
465
+ renderToggle: triggerButton,
466
+ renderContent: editorPanel
467
+ })]
468
+ }), control.helpText && /*#__PURE__*/_jsx("p", {
469
+ className: "components-base-control__help",
470
+ children: control.helpText
471
+ })]
472
+ });
473
+ }
474
+
475
+ // Inline variant (default on wide surfaces: meta-box, options,
476
+ // taxonomy, user-profile). Toolbar + editor render directly into
477
+ // the field — no overlay.
478
+ return /*#__PURE__*/_jsxs("div", {
479
+ className: "components-base-control gcb-richtext-control",
480
+ children: [/*#__PURE__*/_jsxs("div", {
481
+ className: "components-base-control__field",
482
+ children: [control.label && /*#__PURE__*/_jsx("label", {
483
+ className: "components-base-control__label",
484
+ children: control.label
485
+ }), /*#__PURE__*/_jsx(RichtextEditorSurface, {
486
+ editor: editor,
487
+ control: control
488
+ })]
489
+ }), control.helpText && /*#__PURE__*/_jsx("p", {
490
+ className: "components-base-control__help",
491
+ children: control.helpText
492
+ })]
493
+ });
494
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * SelectField — ported from the original GCB SelectField.js verbatim.
3
+ *
4
+ * Supports plain {label, value} options, legacy `map` configs, and full
5
+ * `tokenGroup` binding to theme.json tokens (with optional tokenKeys filter
6
+ * and defaultOptionKey). When tokenGroup is set, the saved value is the
7
+ * full token object { key, slug, value, cssVar } rather than a string.
8
+ */
9
+
10
+ import { __ } from '@wordpress/i18n';
11
+ import { SelectControl, Spinner } from '@wordpress/components';
12
+ import { parseMap, getTokenFromKey, getKeyFromToken, mapToOptions } from '../utils/map-utils';
13
+ import { useTokens, getTokensByGroup, generateMapFromTokens } from '../hooks/useTokens';
14
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
+ function SelectFieldImpl({
16
+ label,
17
+ value,
18
+ options = [],
19
+ onChange,
20
+ help,
21
+ className = '',
22
+ map,
23
+ tokenGroup,
24
+ tokenKeys,
25
+ defaultOptionKey,
26
+ placeholder
27
+ }) {
28
+ const {
29
+ tokens,
30
+ loading
31
+ } = useTokens();
32
+ let groupTokens = null;
33
+ let normalizedMap = null;
34
+ if (tokenGroup && tokens) {
35
+ groupTokens = getTokensByGroup(tokens, tokenGroup);
36
+ if (groupTokens && tokenKeys && Array.isArray(tokenKeys) && tokenKeys.length > 0) {
37
+ groupTokens = groupTokens.filter(t => tokenKeys.includes(t.key));
38
+ }
39
+ if (groupTokens) {
40
+ const generatedMap = generateMapFromTokens(groupTokens);
41
+ normalizedMap = generatedMap ? parseMap(generatedMap) : null;
42
+ }
43
+ } else if (map) {
44
+ normalizedMap = parseMap(map);
45
+ }
46
+ if (tokenGroup && loading) {
47
+ return /*#__PURE__*/_jsxs("div", {
48
+ style: {
49
+ padding: '12px 0'
50
+ },
51
+ children: [/*#__PURE__*/_jsx("div", {
52
+ style: {
53
+ fontWeight: 500,
54
+ marginBottom: '8px'
55
+ },
56
+ children: label
57
+ }), /*#__PURE__*/_jsx(Spinner, {})]
58
+ });
59
+ }
60
+ let effectiveOptions = normalizedMap ? mapToOptions(normalizedMap) : options;
61
+
62
+ // Add a clearing/placeholder option if no default is set, so users can pick blank.
63
+ if (!defaultOptionKey && !effectiveOptions.some(o => o.value === '')) {
64
+ effectiveOptions = [{
65
+ label: '— ' + (placeholder || __('Select', 'gcblite')) + ' —',
66
+ value: ''
67
+ }, ...effectiveOptions];
68
+ }
69
+
70
+ // Apply default if value is empty.
71
+ if ((value === undefined || value === null || value === '') && defaultOptionKey && tokenGroup && groupTokens) {
72
+ const defaultToken = groupTokens.find(t => t.key === defaultOptionKey);
73
+ if (defaultToken && onChange) {
74
+ const defaultValue = {
75
+ key: defaultToken.key,
76
+ slug: defaultToken.slug,
77
+ value: defaultToken.value,
78
+ cssVar: defaultToken.cssVar
79
+ };
80
+ onChange(defaultValue);
81
+ }
82
+ }
83
+ let displayValue = value;
84
+ if (typeof value === 'object' && value !== null) {
85
+ displayValue = value.key !== undefined ? value.key : '';
86
+ } else if (normalizedMap && value !== undefined && value !== null && value !== '') {
87
+ displayValue = getKeyFromToken(normalizedMap, value) || value;
88
+ }
89
+ if ((displayValue === undefined || displayValue === null || displayValue === '') && defaultOptionKey) {
90
+ displayValue = defaultOptionKey;
91
+ }
92
+ const handleChange = newOptionKey => {
93
+ let finalValue = newOptionKey;
94
+ if (tokenGroup && groupTokens) {
95
+ const selected = groupTokens.find(t => t.key === newOptionKey);
96
+ if (selected) {
97
+ finalValue = {
98
+ key: selected.key,
99
+ slug: selected.slug,
100
+ value: selected.value,
101
+ cssVar: selected.cssVar
102
+ };
103
+ }
104
+ } else if (normalizedMap) {
105
+ finalValue = getTokenFromKey(normalizedMap, newOptionKey);
106
+ }
107
+ if (onChange) onChange(finalValue);
108
+ };
109
+ return /*#__PURE__*/_jsx(SelectControl, {
110
+ label: label,
111
+ value: displayValue ?? '',
112
+ options: effectiveOptions,
113
+ onChange: handleChange,
114
+ help: help,
115
+ className: className,
116
+ __nextHasNoMarginBottom: true
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Adapter: maps the registry's `{ control, value, onChange }` contract onto
122
+ * SelectFieldImpl's flat-prop contract.
123
+ */
124
+ export default function SelectField({
125
+ control,
126
+ value,
127
+ onChange
128
+ }) {
129
+ return /*#__PURE__*/_jsx(SelectFieldImpl, {
130
+ label: control.label,
131
+ value: value,
132
+ options: (control.options || []).map(o => ({
133
+ label: o.label,
134
+ value: o.value
135
+ })),
136
+ onChange: onChange,
137
+ help: control.helpText,
138
+ placeholder: control.placeholder,
139
+ map: control.map,
140
+ tokenGroup: control.tokenGroup,
141
+ tokenKeys: control.tokenKeys,
142
+ defaultOptionKey: control.defaultOptionKey ?? control.default
143
+ });
144
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Size — dimension with a unit selector.
3
+ *
4
+ * Stored shape: a CSS length string with unit, e.g. `'16px'`, `'1.5rem'`,
5
+ * `'100%'`. Empty string when not set.
6
+ *
7
+ * Supported units: px, em, rem, %, vh, vw.
8
+ *
9
+ * In a React component you can pass the value directly to `style.width`,
10
+ * `style.height`, `style.padding`, etc. — it's already a valid CSS length.
11
+ */
12
+ import { __experimentalUnitControl as UnitControl, TextControl } from '@wordpress/components';
13
+ import { __ } from '@wordpress/i18n';
14
+ import { jsx as _jsx } from "react/jsx-runtime";
15
+ const UNITS = [{
16
+ value: 'px',
17
+ label: 'px'
18
+ }, {
19
+ value: 'em',
20
+ label: 'em'
21
+ }, {
22
+ value: 'rem',
23
+ label: 'rem'
24
+ }, {
25
+ value: '%',
26
+ label: '%'
27
+ }, {
28
+ value: 'vh',
29
+ label: 'vh'
30
+ }, {
31
+ value: 'vw',
32
+ label: 'vw'
33
+ }];
34
+ export default function SizeField({
35
+ control,
36
+ value,
37
+ onChange
38
+ }) {
39
+ if (UnitControl) {
40
+ return /*#__PURE__*/_jsx(UnitControl, {
41
+ label: control.label,
42
+ value: value || '',
43
+ onChange: onChange,
44
+ units: UNITS,
45
+ help: control.helpText,
46
+ __nextHasNoMarginBottom: true
47
+ });
48
+ }
49
+
50
+ // Fallback when UnitControl isn't available — manual entry.
51
+ return /*#__PURE__*/_jsx(TextControl, {
52
+ label: control.label,
53
+ value: value || '',
54
+ onChange: onChange,
55
+ placeholder: "e.g. 16px, 1em, 100%",
56
+ help: control.helpText || __('Enter a size with unit (px, em, rem, %, vh, vw)', 'gcblite'),
57
+ __nextHasNoMarginBottom: true
58
+ });
59
+ }