@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
@@ -16,13 +16,7 @@
16
16
  */
17
17
 
18
18
  // Injected-config boundary.
19
- export {
20
- GcbFieldsProvider,
21
- GcbFieldsContext,
22
- useGcbFieldsConfig,
23
- useTokensConfig,
24
- useGoogleMapsEnabled,
25
- } from './provider';
19
+ export { GcbFieldsProvider, GcbFieldsContext, useGcbFieldsConfig, useTokensConfig, useGoogleMapsEnabled } from './provider';
26
20
 
27
21
  // Inspector renderer + the control registry.
28
22
  export { renderInspector } from './inspector';
@@ -35,4 +29,4 @@ export { ControlContext } from './control-context';
35
29
 
36
30
  // Token helpers (for custom token-aware UI outside the standard controls).
37
31
  export { useTokens, getTokensByGroup, generateMapFromTokens } from './hooks/useTokens';
38
- export { getAllTokenGroups } from './utils/token-helper';
32
+ export { getAllTokenGroups } from './utils/token-helper';
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Render an Inspector panel tree from a block's `gcb.controls` array.
3
+ *
4
+ * Controls are flat in the JSON; structure comes from `parentPanelId` references
5
+ * to `type: "group"` controls. We bucket controls under their group and emit one
6
+ * <PanelBody> per group, plus a default panel for anything ungrouped.
7
+ */
8
+
9
+ import { PanelBody } from '@wordpress/components';
10
+ import { Fragment, useContext } from '@wordpress/element';
11
+ import { __ } from '@wordpress/i18n';
12
+ import { controlComponents } from './controls';
13
+ import { ValidationContext } from './validation-context';
14
+ import { STRUCTURAL_TYPES, shouldRender, panelsContainingErrors } from './conditional-logic';
15
+
16
+ // Re-export so existing callers keep working (post-fields.js imports
17
+ // shouldRender + panelsContainingErrors from './inspector').
18
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
+ export { shouldRender, panelsContainingErrors };
20
+
21
+ /**
22
+ * @param {Array} controls
23
+ * @param {Object} attributes
24
+ * @param {Function} setAttributes
25
+ * @param {Object} [options]
26
+ * @param {boolean} [options.flatten] When true and no groups exist, render
27
+ * ungrouped controls flat (no outer "Settings" PanelBody). Used by the
28
+ * post-fields meta-box where the meta-box itself IS the panel — nesting
29
+ * another PanelBody would look like a redundant dropdown.
30
+ * @param {Set<string>} [options.forceOpenPanelIds] Panel ids to render
31
+ * with initialOpen=true. Used by the meta-box to auto-open any panel
32
+ * containing a field that just failed validation, so the user can see
33
+ * the offending field.
34
+ */
35
+ export function renderInspector(controls, attributes, setAttributes, options = {}) {
36
+ const {
37
+ groups,
38
+ ungrouped
39
+ } = bucketControls(controls);
40
+ const flatten = options.flatten === true && groups.length === 0;
41
+ const forceOpen = options.forceOpenPanelIds || new Set();
42
+ return /*#__PURE__*/_jsxs(Fragment, {
43
+ children: [ungrouped.length > 0 && (flatten ? ungrouped.map(control => renderControl(control, attributes, setAttributes)) : /*#__PURE__*/_jsx(PanelBody, {
44
+ title: __('Settings', 'gcblite'),
45
+ initialOpen: true,
46
+ children: ungrouped.map(control => renderControl(control, attributes, setAttributes))
47
+ })), groups.map(({
48
+ group,
49
+ children
50
+ }) => /*#__PURE__*/_jsx(PanelBody
51
+ // Remount the panel when its forced-open status changes so
52
+ // the new initialOpen value is honoured. (PanelBody only
53
+ // reads initialOpen on mount.)
54
+ , {
55
+ title: group.label,
56
+ initialOpen: forceOpen.has(group.id),
57
+ children: children.map(control => renderControl(control, attributes, setAttributes))
58
+ }, `${group.id}:${forceOpen.has(group.id) ? 'open' : 'closed'}`))]
59
+ });
60
+ }
61
+ function bucketControls(controls) {
62
+ const groupsById = new Map();
63
+ const groupOrder = [];
64
+ const ungrouped = [];
65
+
66
+ // First pass: register structural controls (group / panel / tools-panel)
67
+ // in declaration order.
68
+ controls.forEach(control => {
69
+ if (STRUCTURAL_TYPES.has(control.type) && control.id) {
70
+ groupsById.set(control.id, {
71
+ group: control,
72
+ children: []
73
+ });
74
+ groupOrder.push(control.id);
75
+ }
76
+ });
77
+
78
+ // Second pass: assign each non-structural control to a panel or to ungrouped.
79
+ controls.forEach(control => {
80
+ if (STRUCTURAL_TYPES.has(control.type)) return;
81
+ const parentId = control.parentPanelId;
82
+ if (parentId && groupsById.has(parentId)) {
83
+ groupsById.get(parentId).children.push(control);
84
+ } else {
85
+ ungrouped.push(control);
86
+ }
87
+ });
88
+ return {
89
+ groups: groupOrder.map(id => groupsById.get(id)),
90
+ ungrouped
91
+ };
92
+ }
93
+ function renderControl(control, attributes, setAttributes) {
94
+ if (!shouldRender(control, attributes)) {
95
+ return null;
96
+ }
97
+ const Component = controlComponents[control.type];
98
+ if (!Component) {
99
+ return /*#__PURE__*/_jsxs("div", {
100
+ style: {
101
+ padding: 8,
102
+ background: '#fff3cd',
103
+ border: '1px solid #ffeeba',
104
+ marginBottom: 8
105
+ },
106
+ children: [/*#__PURE__*/_jsx("strong", {
107
+ children: control.label
108
+ }), ": unknown control type ", /*#__PURE__*/_jsx("code", {
109
+ children: control.type
110
+ })]
111
+ }, control.id);
112
+ }
113
+ const value = attributes[control.attributeKey];
114
+ const onChange = next => setAttributes({
115
+ [control.attributeKey]: next
116
+ });
117
+
118
+ // Wrap each rendered control in a ValidationWrapper that overlays the
119
+ // required `*`, the inline error message, and the data-attribute used
120
+ // by the meta-box submit interceptor to scroll-into-view. The wrapper
121
+ // is invisible in surfaces without validation (sidebar default
122
+ // ValidationContext is { errors:{}, showErrors:false }) so blocks
123
+ // don't see any change.
124
+ return /*#__PURE__*/_jsx(ValidationWrapper, {
125
+ control: control,
126
+ children: /*#__PURE__*/_jsx(Component, {
127
+ control: control,
128
+ value: value,
129
+ onChange: onChange,
130
+ attributes: attributes
131
+ })
132
+ }, control.id);
133
+ }
134
+
135
+ /**
136
+ * Per-field decorator: stamps data-gcblite-field for scroll-to-error,
137
+ * shows the required asterisk, and surfaces the inline error message
138
+ * once the host has flipped showErrors on.
139
+ */
140
+ function ValidationWrapper({
141
+ control,
142
+ children
143
+ }) {
144
+ const {
145
+ errors,
146
+ showErrors
147
+ } = useContext(ValidationContext);
148
+ const key = control.attributeKey;
149
+ const required = !!control.validation?.required;
150
+ const errorMessage = showErrors && key ? errors[key] : null;
151
+ if (!required && !errorMessage && !key) {
152
+ return children;
153
+ }
154
+ return /*#__PURE__*/_jsxs("div", {
155
+ "data-gcblite-field": key || undefined,
156
+ className: ['gcblite-field', required ? 'gcblite-field--required' : '', errorMessage ? 'gcblite-field--has-error' : ''].filter(Boolean).join(' '),
157
+ children: [children, errorMessage && /*#__PURE__*/_jsx("p", {
158
+ className: "gcblite-field__error",
159
+ role: "alert",
160
+ children: errorMessage
161
+ })]
162
+ });
163
+ }
@@ -23,37 +23,38 @@
23
23
  */
24
24
 
25
25
  import { createContext, useContext } from '@wordpress/element';
26
-
26
+ import { jsx as _jsx } from "react/jsx-runtime";
27
27
  export const GcbFieldsContext = createContext(null);
28
-
29
- export function GcbFieldsProvider({ value, children }) {
30
- return (
31
- <GcbFieldsContext.Provider value={value || {}}>
32
- {children}
33
- </GcbFieldsContext.Provider>
34
- );
28
+ export function GcbFieldsProvider({
29
+ value,
30
+ children
31
+ }) {
32
+ return /*#__PURE__*/_jsx(GcbFieldsContext.Provider, {
33
+ value: value || {},
34
+ children: children
35
+ });
35
36
  }
36
37
 
37
38
  /** Legacy global fallback so the plugin keeps working before it injects config. */
38
39
  function legacyGlobal() {
39
- return (typeof window !== 'undefined' && window.gcbLite) || {};
40
+ return typeof window !== 'undefined' && window.gcbLite || {};
40
41
  }
41
42
 
42
43
  /** Full resolved config (context first, window.gcbLite fallback). */
43
44
  export function useGcbFieldsConfig() {
44
- return useContext(GcbFieldsContext) || {};
45
+ return useContext(GcbFieldsContext) || {};
45
46
  }
46
47
 
47
48
  /** Design-token tree. */
48
49
  export function useTokensConfig() {
49
- const cfg = useContext(GcbFieldsContext) || {};
50
- if (cfg.tokens !== undefined) return cfg.tokens || {};
51
- return legacyGlobal().tokens || {};
50
+ const cfg = useContext(GcbFieldsContext) || {};
51
+ if (cfg.tokens !== undefined) return cfg.tokens || {};
52
+ return legacyGlobal().tokens || {};
52
53
  }
53
54
 
54
55
  /** Whether the google-map control should enable Maps features. */
55
56
  export function useGoogleMapsEnabled() {
56
- const cfg = useContext(GcbFieldsContext) || {};
57
- if (cfg.googleMapsEnabled !== undefined) return !!cfg.googleMapsEnabled;
58
- return !!legacyGlobal().googleMaps?.hasApiKey;
59
- }
57
+ const cfg = useContext(GcbFieldsContext) || {};
58
+ if (cfg.googleMapsEnabled !== undefined) return !!cfg.googleMapsEnabled;
59
+ return !!legacyGlobal().googleMaps?.hasApiKey;
60
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Map utilities for fields that translate user-friendly keys to backend tokens.
3
+ *
4
+ * simple: { sm: 'spacing-sm', md: 'spacing-md' }
5
+ * labelled:{ sm: { label: 'Small (16px)', token: 'spacing-sm' } }
6
+ */
7
+
8
+ export const parseMap = map => {
9
+ if (!map || typeof map !== 'object') return null;
10
+ const out = {};
11
+ Object.entries(map).forEach(([key, value]) => {
12
+ if (typeof value === 'string') {
13
+ out[key] = {
14
+ label: key,
15
+ token: value
16
+ };
17
+ } else if (typeof value === 'object' && value.token) {
18
+ out[key] = {
19
+ label: value.label || key,
20
+ token: value.token
21
+ };
22
+ }
23
+ });
24
+ return Object.keys(out).length > 0 ? out : null;
25
+ };
26
+ export const getTokenFromKey = (m, key) => m && key ? m[key]?.token || null : null;
27
+ export const getKeyFromToken = (m, token) => {
28
+ if (!m || !token) return null;
29
+ const entry = Object.entries(m).find(([, v]) => v.token === token);
30
+ return entry ? entry[0] : null;
31
+ };
32
+ export const mapToOptions = m => {
33
+ if (!m) return [];
34
+ return Object.entries(m).map(([key, v]) => ({
35
+ label: v.label,
36
+ value: key
37
+ }));
38
+ };
39
+ export const getMapKeys = m => {
40
+ if (!m) return [];
41
+ return Object.keys(m).map(k => Number(k)).filter(k => !Number.isNaN(k)).sort((a, b) => a - b);
42
+ };
43
+ export const mapToRangeMarks = m => {
44
+ if (!m) return null;
45
+ return Object.entries(m).map(([key, v]) => ({
46
+ value: Number(key),
47
+ label: v.label
48
+ }));
49
+ };
50
+ export const isValidMapValue = (m, value) => {
51
+ if (!m || value === null || value === undefined) return false;
52
+ if (m[value]) return true;
53
+ return Object.values(m).some(e => e.token === value);
54
+ };
@@ -0,0 +1,396 @@
1
+ /**
2
+ * Token helpers — ported from the original GCB.
3
+ *
4
+ * Two layers of tokens:
5
+ * - Built-in groups (this file): hard-coded design system tokens that ship
6
+ * with the plugin. Useful as a fallback when a theme has no theme.json.
7
+ * - Theme groups (window.gcbLite.tokens): parsed from theme.json by PHP.
8
+ * Always merged in by getTokenGroupTokens() below — theme tokens win on
9
+ * a path collision.
10
+ *
11
+ * Token path syntax: "category:subKey" or "category:subKey:tokenKey".
12
+ * "spacing:scale" — the group of tokens
13
+ * "spacing:scale:30" — a specific token
14
+ */
15
+
16
+ const builtInTokenGroups = {
17
+ color: {
18
+ label: 'Color',
19
+ children: {
20
+ palette: {
21
+ label: 'Palette',
22
+ tokens: [{
23
+ key: 'primary',
24
+ value: 'color-primary',
25
+ label: 'Primary'
26
+ }, {
27
+ key: 'secondary',
28
+ value: 'color-secondary',
29
+ label: 'Secondary'
30
+ }, {
31
+ key: 'accent',
32
+ value: 'color-accent',
33
+ label: 'Accent'
34
+ }, {
35
+ key: 'neutral',
36
+ value: 'color-neutral',
37
+ label: 'Neutral'
38
+ }, {
39
+ key: 'dark',
40
+ value: 'color-dark',
41
+ label: 'Dark'
42
+ }, {
43
+ key: 'light',
44
+ value: 'color-light',
45
+ label: 'Light'
46
+ }]
47
+ },
48
+ duotone: {
49
+ label: 'Duotone',
50
+ tokens: [{
51
+ key: 'blue',
52
+ value: 'duotone-blue',
53
+ label: 'Blue Duotone'
54
+ }, {
55
+ key: 'purple',
56
+ value: 'duotone-purple',
57
+ label: 'Purple Duotone'
58
+ }, {
59
+ key: 'green',
60
+ value: 'duotone-green',
61
+ label: 'Green Duotone'
62
+ }]
63
+ }
64
+ }
65
+ },
66
+ spacing: {
67
+ label: 'Spacing',
68
+ children: {
69
+ scale: {
70
+ label: 'Scale',
71
+ tokens: [{
72
+ key: '0',
73
+ value: 'spacing-none',
74
+ label: 'None (0)',
75
+ size: '0'
76
+ }, {
77
+ key: '10',
78
+ value: 'spacing-10',
79
+ label: 'Step 1 (0.25rem)',
80
+ size: '0.25rem'
81
+ }, {
82
+ key: '20',
83
+ value: 'spacing-20',
84
+ label: 'Step 2 (0.5rem)',
85
+ size: '0.5rem'
86
+ }, {
87
+ key: '30',
88
+ value: 'spacing-30',
89
+ label: 'Step 3 (1rem)',
90
+ size: '1rem'
91
+ }, {
92
+ key: '40',
93
+ value: 'spacing-40',
94
+ label: 'Step 4 (1.5rem)',
95
+ size: '1.5rem'
96
+ }, {
97
+ key: '50',
98
+ value: 'spacing-50',
99
+ label: 'Step 5 (2rem)',
100
+ size: '2rem'
101
+ }, {
102
+ key: '60',
103
+ value: 'spacing-60',
104
+ label: 'Step 6 (3rem)',
105
+ size: '3rem'
106
+ }, {
107
+ key: '70',
108
+ value: 'spacing-70',
109
+ label: 'Step 7 (4rem)',
110
+ size: '4rem'
111
+ }, {
112
+ key: '80',
113
+ value: 'spacing-80',
114
+ label: 'Step 8 (6rem)',
115
+ size: '6rem'
116
+ }]
117
+ },
118
+ presets: {
119
+ label: 'Presets',
120
+ tokens: [{
121
+ key: 'xs',
122
+ value: 'spacing-xs',
123
+ label: 'Extra Small (0.5rem)',
124
+ size: '0.5rem'
125
+ }, {
126
+ key: 'sm',
127
+ value: 'spacing-sm',
128
+ label: 'Small (1rem)',
129
+ size: '1rem'
130
+ }, {
131
+ key: 'md',
132
+ value: 'spacing-md',
133
+ label: 'Medium (1.5rem)',
134
+ size: '1.5rem'
135
+ }, {
136
+ key: 'lg',
137
+ value: 'spacing-lg',
138
+ label: 'Large (2rem)',
139
+ size: '2rem'
140
+ }, {
141
+ key: 'xl',
142
+ value: 'spacing-xl',
143
+ label: 'Extra Large (3rem)',
144
+ size: '3rem'
145
+ }, {
146
+ key: '2xl',
147
+ value: 'spacing-2xl',
148
+ label: '2X Large (4rem)',
149
+ size: '4rem'
150
+ }, {
151
+ key: '3xl',
152
+ value: 'spacing-3xl',
153
+ label: '3X Large (6rem)',
154
+ size: '6rem'
155
+ }]
156
+ },
157
+ semantic: {
158
+ label: 'Semantic',
159
+ tokens: [{
160
+ key: 'content',
161
+ value: 'spacing-content',
162
+ label: 'Content',
163
+ size: 'var(--wp--style--block-gap, 1.5rem)'
164
+ }, {
165
+ key: 'section',
166
+ value: 'spacing-section',
167
+ label: 'Section',
168
+ size: 'clamp(2rem, 5vw, 4rem)'
169
+ }, {
170
+ key: 'container',
171
+ value: 'spacing-container',
172
+ label: 'Container',
173
+ size: 'clamp(1rem, 3vw, 2rem)'
174
+ }]
175
+ }
176
+ }
177
+ },
178
+ typography: {
179
+ label: 'Typography',
180
+ children: {
181
+ fontSize: {
182
+ label: 'Font Sizes',
183
+ tokens: [{
184
+ key: 'xs',
185
+ value: 'text-xs',
186
+ label: 'Extra Small (12px)'
187
+ }, {
188
+ key: 'sm',
189
+ value: 'text-sm',
190
+ label: 'Small (14px)'
191
+ }, {
192
+ key: 'base',
193
+ value: 'text-base',
194
+ label: 'Base (16px)'
195
+ }, {
196
+ key: 'lg',
197
+ value: 'text-lg',
198
+ label: 'Large (18px)'
199
+ }, {
200
+ key: 'xl',
201
+ value: 'text-xl',
202
+ label: 'Extra Large (20px)'
203
+ }, {
204
+ key: '2xl',
205
+ value: 'text-2xl',
206
+ label: '2X Large (24px)'
207
+ }]
208
+ },
209
+ fontWeight: {
210
+ label: 'Font Weights',
211
+ tokens: [{
212
+ key: 'light',
213
+ value: 'font-light',
214
+ label: 'Light (300)'
215
+ }, {
216
+ key: 'normal',
217
+ value: 'font-normal',
218
+ label: 'Normal (400)'
219
+ }, {
220
+ key: 'medium',
221
+ value: 'font-medium',
222
+ label: 'Medium (500)'
223
+ }, {
224
+ key: 'semibold',
225
+ value: 'font-semibold',
226
+ label: 'Semibold (600)'
227
+ }, {
228
+ key: 'bold',
229
+ value: 'font-bold',
230
+ label: 'Bold (700)'
231
+ }]
232
+ }
233
+ }
234
+ },
235
+ sizing: {
236
+ label: 'Sizing',
237
+ children: {
238
+ containers: {
239
+ label: 'Container Widths',
240
+ tokens: [{
241
+ key: 'narrow',
242
+ value: 'container-narrow',
243
+ label: 'Narrow (600px)'
244
+ }, {
245
+ key: 'normal',
246
+ value: 'container-normal',
247
+ label: 'Normal (1200px)'
248
+ }, {
249
+ key: 'wide',
250
+ value: 'container-wide',
251
+ label: 'Wide (1600px)'
252
+ }, {
253
+ key: 'full',
254
+ value: 'container-full',
255
+ label: 'Full Width'
256
+ }]
257
+ },
258
+ borderRadius: {
259
+ label: 'Border Radius',
260
+ tokens: [{
261
+ key: 'none',
262
+ value: 'radius-none',
263
+ label: 'None'
264
+ }, {
265
+ key: 'sm',
266
+ value: 'radius-sm',
267
+ label: 'Small'
268
+ }, {
269
+ key: 'md',
270
+ value: 'radius-md',
271
+ label: 'Medium'
272
+ }, {
273
+ key: 'lg',
274
+ value: 'radius-lg',
275
+ label: 'Large'
276
+ }, {
277
+ key: 'full',
278
+ value: 'radius-full',
279
+ label: 'Full (Pill)'
280
+ }]
281
+ }
282
+ }
283
+ }
284
+ };
285
+
286
+ /**
287
+ * Get all token groups, merging built-ins with theme.json tokens.
288
+ * Theme tokens override built-ins on a path collision.
289
+ *
290
+ * @param {object} [themeTokens] Theme token tree. Defaults to the legacy
291
+ * `window.gcbLite.tokens` global so non-React callers (and the unmigrated
292
+ * plugin) keep working; the SDK's useTokens() hook passes the value from
293
+ * GcbFieldsProvider instead.
294
+ */
295
+ export function getAllTokenGroups(themeTokens) {
296
+ if (themeTokens === undefined) {
297
+ themeTokens = typeof window !== 'undefined' && window.gcbLite?.tokens || {};
298
+ }
299
+ const merged = {
300
+ ...builtInTokenGroups
301
+ };
302
+ Object.keys(themeTokens).forEach(category => {
303
+ if (merged[category]) {
304
+ merged[category] = {
305
+ ...merged[category],
306
+ children: {
307
+ ...(merged[category].children || {}),
308
+ ...(themeTokens[category].children || {})
309
+ }
310
+ };
311
+ } else {
312
+ merged[category] = themeTokens[category];
313
+ }
314
+ });
315
+ return merged;
316
+ }
317
+
318
+ /**
319
+ * Resolve a token group path to its token list.
320
+ *
321
+ * @param {string} tokenGroup e.g. "spacing:scale", "custom:gap"
322
+ * @returns {Array|null}
323
+ */
324
+ export function getTokenGroupTokens(tokenGroup) {
325
+ if (!tokenGroup || typeof tokenGroup !== 'string') return null;
326
+ const [categoryKey, subKey] = tokenGroup.split(':');
327
+ if (!categoryKey || !subKey) return null;
328
+ const all = getAllTokenGroups();
329
+ const category = all[categoryKey];
330
+ if (!category?.children) return null;
331
+ return category.children[subKey]?.tokens || null;
332
+ }
333
+
334
+ /**
335
+ * Convert a token list to {label, value} options for a select/radio control.
336
+ */
337
+ export function tokensToOptions(tokens) {
338
+ if (!Array.isArray(tokens)) return [];
339
+ return tokens.map(t => ({
340
+ label: t.label,
341
+ value: t.key
342
+ }));
343
+ }
344
+
345
+ /**
346
+ * For spacing tokens: resolve the raw size value (e.g. "1rem").
347
+ * Accepts a full path ("spacing:scale:30"), a partial ("scale:30"),
348
+ * or just a key ("30") — searches in priority order.
349
+ */
350
+ export function getSpacingSize(tokenKey) {
351
+ if (!tokenKey) return null;
352
+ const parts = tokenKey.split(':');
353
+ let categoryKey, subKey, key;
354
+ if (parts.length === 3) {
355
+ [categoryKey, subKey, key] = parts;
356
+ } else if (parts.length === 2) {
357
+ categoryKey = 'spacing';
358
+ [subKey, key] = parts;
359
+ } else {
360
+ // Bare key — search every spacing subcategory.
361
+ const spacing = getAllTokenGroups().spacing;
362
+ if (spacing?.children) {
363
+ for (const subCat of Object.values(spacing.children)) {
364
+ const t = subCat.tokens?.find(tok => tok.key === tokenKey);
365
+ if (t?.size) return t.size;
366
+ if (t?.value) return t.value; // theme.json tokens use `value`
367
+ }
368
+ }
369
+ return null;
370
+ }
371
+ const cat = getAllTokenGroups()[categoryKey];
372
+ const tok = cat?.children?.[subKey]?.tokens?.find(t => t.key === key);
373
+ return tok?.size || tok?.value || null;
374
+ }
375
+
376
+ /**
377
+ * Convert a spacing token to the WP CSS custom property.
378
+ * Falls back to var(--wp--preset--spacing--{key}) for unknown tokens.
379
+ */
380
+ export function spacingTokenToCSSVar(tokenKey) {
381
+ if (!tokenKey) return null;
382
+ const parts = tokenKey.split(':');
383
+ const key = parts.length > 1 ? parts[parts.length - 1] : tokenKey;
384
+ return `var(--wp--preset--spacing--${key})`;
385
+ }
386
+
387
+ /**
388
+ * Resolve a token to its cssVar, preferring an explicit cssVar property
389
+ * (theme.json tokens have one), falling back to the spacing convention.
390
+ */
391
+ export function tokenToCSSVar(token) {
392
+ if (!token) return null;
393
+ if (token.cssVar) return token.cssVar;
394
+ if (token.slug) return `var(--wp--preset--spacing--${token.slug})`;
395
+ return null;
396
+ }
@@ -12,8 +12,8 @@
12
12
  */
13
13
 
14
14
  import { createContext } from '@wordpress/element';
15
-
16
15
  export const ValidationContext = createContext({
17
- errors: {}, // { attributeKey: 'human message' }
18
- showErrors: false, // becomes true after the first failed save attempt
19
- });
16
+ errors: {},
17
+ // { attributeKey: 'human message' }
18
+ showErrors: false // becomes true after the first failed save attempt
19
+ });