@webikon/webentor-core 0.10.1 → 0.11.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Webentor Core Changelog
2
2
 
3
+ ## 0.11.0
4
+
5
+ - Add responsive spacing settings for WP Core blocks (`core/paragraph`, `core/heading`) — spacing panel appears automatically, classes rendered on frontend via `WP_HTML_Tag_Processor`
6
+ - Filterable block list: JS `webentor.core.wpCoreBlocksWithSpacing`, PHP `webentor/wp_core_blocks_with_spacing`
7
+ - Native WP spacing controls disabled on affected blocks to avoid duplication
8
+ - Fix React hooks warning ("Do not call Hooks inside useEffect") caused by `generateClassNames` being called outside React component context via `blocks.getSaveContent.extraProps`
9
+ - Fix WP Core blocks breaking after save/refresh — `classNameGenerator` no longer injects responsive classes into saved markup, preventing block validation failures on static blocks
10
+ - Fix `l-section` responsive settings not splitting classes between wrapper and inner container in editor (layout/flexbox/grid classes now correctly apply to the inner container, matching frontend behavior)
11
+ - Add `WebentorCoreServiceProvider` — Acorn auto-discovered service provider for webentor-core
12
+ - Blade directives (`@sliderContent`, `@enqueueScripts`, `@xdebugBreak`) moved from theme to core
13
+ - View Components (`Button`, `Slider`) moved from theme to core — themes can override by extending `Webentor\Core\View\Components\Button` etc.
14
+ - Core block `data.php` files now loaded by the service provider instead of `ThemeServiceProvider`
15
+
3
16
  ## 0.10.1
4
17
 
5
18
  - Fix `l-section` inner z-index
@@ -107,7 +107,9 @@ Key methods:
107
107
  - Each panel queries registry and renders SettingsComponents
108
108
 
109
109
  3. **Class generation** (`generateClassNames` in `utils.ts`):
110
- - Called by `registerBlockExtension` classNameGenerator hook
110
+ - Called via a dedicated `editor.BlockListBlock` HOC (not via `registerBlockExtension`'s
111
+ `classNameGenerator`, which is no-oped to prevent classes leaking into saved markup
112
+ and React hooks being called outside component context)
111
113
  - Collects breakpoints from all attribute values
112
114
  - Calls each module's `generateClasses(attributes, breakpoint, context)` per breakpoint
113
115
  - Concatenates results
@@ -16,6 +16,7 @@
16
16
  import { registerBlockExtension } from '@10up/block-components';
17
17
  import { BlockControls, InspectorControls } from '@wordpress/block-editor';
18
18
  import { ToolbarGroup } from '@wordpress/components';
19
+ import { createHigherOrderComponent } from '@wordpress/compose';
19
20
  import { Fragment } from '@wordpress/element';
20
21
  import { addFilter, applyFilters } from '@wordpress/hooks';
21
22
 
@@ -45,6 +46,43 @@ import './settings/sizing';
45
46
  import './settings/spacing';
46
47
 
47
48
  const initResponsiveSettings = () => {
49
+ /**
50
+ * WP Core block spacing support injection.
51
+ * Injects `supports.webentor.spacing` into configured WP Core blocks so the
52
+ * responsive spacing panel appears automatically. Runs at priority 5,
53
+ * before the attribute-merging filter at priority 10.
54
+ *
55
+ * The block list is filterable via `webentor.core.wpCoreBlocksWithSpacing`.
56
+ */
57
+ addFilter(
58
+ 'blocks.registerBlockType',
59
+ 'webentor/core/injectWpCoreBlockSpacingSupport',
60
+ (settings, name) => {
61
+ const wpCoreBlocks = applyFilters(
62
+ 'webentor.core.wpCoreBlocksWithSpacing',
63
+ ['core/paragraph', 'core/heading'],
64
+ ) as string[];
65
+
66
+ if (!wpCoreBlocks.includes(name)) return settings;
67
+
68
+ settings.supports = {
69
+ ...settings.supports,
70
+ webentor: {
71
+ ...settings.supports?.webentor,
72
+ spacing: true,
73
+ },
74
+ spacing: {
75
+ ...settings.supports?.spacing,
76
+ padding: false,
77
+ margin: false,
78
+ },
79
+ };
80
+
81
+ return settings;
82
+ },
83
+ 5,
84
+ );
85
+
48
86
  /**
49
87
  * Attribute registration filter.
50
88
  * Iterates over all registered setting modules and merges their
@@ -89,10 +127,46 @@ const initResponsiveSettings = () => {
89
127
  extensionName: 'webentor.core.addResponsiveSettings',
90
128
  attributes: {},
91
129
  inlineStyleGenerator,
92
- classNameGenerator: generateClassNames,
130
+ // No-op: prevents @10up/block-components from injecting responsive classes
131
+ // into saved markup via blocks.getSaveContent.extraProps, which breaks
132
+ // static core blocks (core/paragraph, core/heading) and triggers React
133
+ // hooks warnings (generateClassNames uses hooks but the save filter
134
+ // calls it outside React component context).
135
+ classNameGenerator: () => '',
93
136
  order: 'after',
94
137
  Edit: BlockEdit,
95
138
  });
139
+
140
+ /**
141
+ * Editor-only responsive classes via editor.BlockListBlock.
142
+ * Replaces the @10up classNameGenerator approach so that:
143
+ * 1. Hooks (useBlockProps, useBlockParent) are called in component context
144
+ * 2. Classes are only applied in the editor, not injected into saved HTML
145
+ */
146
+ const addResponsiveClassesHOC = createHigherOrderComponent(
147
+ (BlockListBlock) => (props) => {
148
+ const { attributes, className = '' } = props;
149
+ const responsiveClasses = generateClassNames(attributes);
150
+
151
+ if (!responsiveClasses) {
152
+ return <BlockListBlock {...props} />;
153
+ }
154
+
155
+ return (
156
+ <BlockListBlock
157
+ {...props}
158
+ className={`${className} ${responsiveClasses}`.trim()}
159
+ />
160
+ );
161
+ },
162
+ 'addResponsiveClasses',
163
+ );
164
+
165
+ addFilter(
166
+ 'editor.BlockListBlock',
167
+ 'webentor/core/addResponsiveClasses',
168
+ addResponsiveClassesHOC,
169
+ );
96
170
  };
97
171
 
98
172
  /**
@@ -142,26 +142,32 @@ export const applyResponsiveSettings = (attributes: any): boolean => {
142
142
  };
143
143
 
144
144
  /**
145
- * Generates Tailwind class names from block attributes using the SettingsRegistry.
146
- * Each registered setting's generateClasses() is called per breakpoint.
145
+ * Pure class generator. Walks the SettingsRegistry and runs each supported
146
+ * module's generateClasses() per breakpoint, grouping the results by
147
+ * SettingDefinition.attributeKey (e.g. 'layout', 'flexbox', 'grid', 'spacing').
148
+ *
149
+ * Mirrors the PHP classes_by_property map. Hook-free — caller passes
150
+ * blockName and parentBlockAttributes explicitly so this can be used from
151
+ * any component without duplicating useBlockProps / useBlockParent calls.
147
152
  *
148
- * This function is called as a classNameGenerator hook from registerBlockExtension.
149
- * useBlockParent/useBlockProps are called at top level to comply with Rules of Hooks.
153
+ * Consumers that need to split classes between different DOM elements
154
+ * (e.g. l-section puts flexbox/grid/layout.display on an inner container)
155
+ * can call this helper and pick the entries they want per element.
150
156
  */
151
- export const generateClassNames = (attributes: any): string => {
157
+ export const computeClassesByAttribute = (
158
+ attributes: Record<string, any>,
159
+ blockName: string,
160
+ parentBlockAttributes?: Record<string, any>,
161
+ ): Record<string, string[]> => {
162
+ const result: Record<string, string[]> = {};
163
+
152
164
  if (!applyResponsiveSettings(attributes)) {
153
- return '';
165
+ return result;
154
166
  }
155
167
 
156
- const blockProps = useBlockProps();
157
- const blockName = blockProps['data-type'];
158
168
  const blockSettings = getBlockType(blockName);
159
169
  const supports = blockSettings?.supports;
160
170
 
161
- // useBlockParent hoisted to top level (hook-safe)
162
- const parentBlock = useBlockParent();
163
-
164
- // Resolve ordered breakpoints for cascade logic
165
171
  const orderedBreakpoints: string[] = applyFilters(
166
172
  'webentor.core.twBreakpoints',
167
173
  ['basic'],
@@ -170,11 +176,10 @@ export const generateClassNames = (attributes: any): string => {
170
176
  const context: ClassGenContext = {
171
177
  blockName,
172
178
  supports,
173
- parentBlockAttributes: parentBlock?.attributes,
179
+ parentBlockAttributes,
174
180
  breakpoints: orderedBreakpoints,
175
181
  };
176
182
 
177
- const classes: string[] = [];
178
183
  const allSettings = registry.getAll();
179
184
 
180
185
  // Collect all breakpoints present in any attribute
@@ -182,7 +187,6 @@ export const generateClassNames = (attributes: any): string => {
182
187
  for (const def of allSettings) {
183
188
  if (!registry.isSupported(supports, def)) continue;
184
189
 
185
- // Check all attribute keys declared by this module
186
190
  for (const attrKey of Object.keys(def.attributeSchema)) {
187
191
  const attrGroup = attributes[attrKey];
188
192
  if (!attrGroup || typeof attrGroup !== 'object') continue;
@@ -198,15 +202,90 @@ export const generateClassNames = (attributes: any): string => {
198
202
  }
199
203
  }
200
204
 
201
- // Generate classes per breakpoint per registered setting
205
+ // Generate classes per breakpoint per registered setting, grouped by attributeKey
202
206
  for (const bp of breakpoints) {
203
207
  for (const def of allSettings) {
204
208
  if (!registry.isSupported(supports, def)) continue;
205
- classes.push(...def.generateClasses(attributes, bp, context));
209
+ const produced = def.generateClasses(attributes, bp, context);
210
+ if (produced.length === 0) continue;
211
+ const key = def.attributeKey;
212
+ if (!result[key]) result[key] = [];
213
+ result[key].push(...produced);
214
+ }
215
+ }
216
+
217
+ return result;
218
+ };
219
+
220
+ /**
221
+ * Generates Tailwind class names from block attributes using the SettingsRegistry.
222
+ * Each registered setting's generateClasses() is called per breakpoint.
223
+ *
224
+ * Called from the editor.BlockListBlock HOC (React component context).
225
+ * Hooks are called unconditionally before any early returns (Rules of Hooks).
226
+ *
227
+ * Delegates the per-module work to computeClassesByAttribute and flattens the
228
+ * result, preserving the original output shape (a single space-joined string).
229
+ */
230
+ export const generateClassNames = (attributes: any): string => {
231
+ // Hooks must be called unconditionally before any early return (Rules of Hooks)
232
+ const blockProps = useBlockProps();
233
+ const parentBlock = useBlockParent();
234
+
235
+ if (!applyResponsiveSettings(attributes)) {
236
+ return '';
237
+ }
238
+
239
+ const blockName = blockProps['data-type'];
240
+
241
+ const classesByAttribute = computeClassesByAttribute(
242
+ attributes,
243
+ blockName,
244
+ parentBlock?.attributes,
245
+ );
246
+
247
+ return Object.values(classesByAttribute).flat().join(' ');
248
+ };
249
+
250
+ /**
251
+ * Collect Tailwind class tokens from responsive setting attributes.
252
+ *
253
+ * Reads value entries from the given attribute keys (e.g. 'layout', 'flexbox',
254
+ * 'grid') and returns the set of class tokens that would be generated.
255
+ * Works directly from the block's attributes — no registry or cross-bundle
256
+ * state needed.
257
+ *
258
+ * Useful for blocks that need to split classes between elements (e.g. l-section
259
+ * moves layout/flexbox/grid classes from the wrapper to an inner container).
260
+ */
261
+ export const collectClassTokensFromAttributes = (
262
+ attributes: Record<string, any>,
263
+ attributeKeys: string[],
264
+ ): Set<string> => {
265
+ const tokens = new Set<string>();
266
+
267
+ for (const attrKey of attributeKeys) {
268
+ const attrGroup = attributes[attrKey];
269
+ if (!attrGroup || typeof attrGroup !== 'object') continue;
270
+
271
+ for (const prop of Object.values(attrGroup)) {
272
+ const propData = prop as any;
273
+ if (!propData?.value) continue;
274
+
275
+ for (const [bp, value] of Object.entries(propData.value)) {
276
+ if (!value || typeof value !== 'string') continue;
277
+ const prefix = bp === 'basic' ? '' : `${bp}:`;
278
+ tokens.add(`${prefix}${value}`);
279
+
280
+ // Layout 'hidden' maps to 'opacity-30' in editor
281
+ if (value === 'hidden') {
282
+ tokens.add(`${prefix}opacity-30`);
283
+ }
284
+ }
206
285
  }
207
286
  }
208
287
 
209
- return classes.join(' ') ?? '';
288
+ return tokens;
210
289
  };
211
290
 
212
291
  export const inlineStyleGenerator = (): Record<string, any> => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@webikon/webentor-core",
3
3
  "homepage": "https://webikon.sk",
4
- "version": "0.10.1",
4
+ "version": "0.11.0",
5
5
  "description": "Core functionality and useful utilities for Webentor Stack",
6
6
  "license": "MIT",
7
7
  "author": "Webikon s.r.o.",