@webikon/webentor-core 0.9.14 → 0.10.1

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 (126) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +41 -0
  3. package/core-js/_alpine.ts +6 -0
  4. package/core-js/_slider.ts +22 -11
  5. package/core-js/blocks-components/button.tsx +17 -2
  6. package/core-js/blocks-components/custom-image-sizes-panel.tsx +3 -1
  7. package/core-js/blocks-components/typography-picker-select.tsx +16 -1
  8. package/core-js/blocks-filters/_filter-core-typography.tsx +11 -1
  9. package/core-js/blocks-filters/_slider-settings.tsx +1 -1
  10. package/core-js/blocks-filters/_wrap-with-container.tsx +104 -0
  11. package/core-js/blocks-filters/responsive-settings/AGENTS.md +255 -0
  12. package/core-js/blocks-filters/responsive-settings/components/AppliedClassesViewer.tsx +189 -0
  13. package/core-js/blocks-filters/responsive-settings/components/BoxModelControl.tsx +346 -0
  14. package/core-js/blocks-filters/responsive-settings/components/BreakpointResetButton.tsx +94 -0
  15. package/core-js/blocks-filters/responsive-settings/components/DebugPanel.tsx +67 -0
  16. package/core-js/blocks-filters/responsive-settings/components/InheritedIndicator.tsx +32 -0
  17. package/core-js/blocks-filters/responsive-settings/components/LinkedValuesControl.tsx +55 -0
  18. package/core-js/blocks-filters/responsive-settings/components/ResponsiveSelectGroup.tsx +185 -0
  19. package/core-js/blocks-filters/responsive-settings/components/ResponsiveTabPanel.tsx +106 -0
  20. package/core-js/blocks-filters/responsive-settings/index.tsx +97 -148
  21. package/core-js/blocks-filters/responsive-settings/migration.ts +86 -0
  22. package/core-js/blocks-filters/responsive-settings/panels/BlockLinkPanel.tsx +38 -0
  23. package/core-js/blocks-filters/responsive-settings/panels/BorderPanel.tsx +61 -0
  24. package/core-js/blocks-filters/responsive-settings/panels/DisplayLayoutPanel.tsx +92 -0
  25. package/core-js/blocks-filters/responsive-settings/panels/SpacingPanel.tsx +63 -0
  26. package/core-js/blocks-filters/responsive-settings/panels/index.ts +4 -0
  27. package/core-js/blocks-filters/responsive-settings/registry.ts +88 -0
  28. package/core-js/blocks-filters/responsive-settings/settings/block-link/index.ts +3 -0
  29. package/core-js/blocks-filters/responsive-settings/settings/block-link/panel.tsx +6 -6
  30. package/core-js/blocks-filters/responsive-settings/settings/block-link/registration.ts +35 -0
  31. package/core-js/blocks-filters/responsive-settings/settings/border/border/properties.ts +1 -2
  32. package/core-js/blocks-filters/responsive-settings/settings/border/border/settings.tsx +21 -3
  33. package/core-js/blocks-filters/responsive-settings/settings/border/border-radius/index.tsx +2 -1
  34. package/core-js/blocks-filters/responsive-settings/settings/border/border-radius/properties.ts +6 -29
  35. package/core-js/blocks-filters/responsive-settings/settings/border/border-radius/settings.tsx +79 -6
  36. package/core-js/blocks-filters/responsive-settings/settings/border/index.ts +5 -1
  37. package/core-js/blocks-filters/responsive-settings/settings/border/panel.tsx +5 -54
  38. package/core-js/blocks-filters/responsive-settings/settings/border/registration.ts +84 -0
  39. package/core-js/blocks-filters/responsive-settings/settings/border/settings.tsx +21 -0
  40. package/core-js/blocks-filters/responsive-settings/settings/flex-item/index.ts +4 -0
  41. package/core-js/blocks-filters/responsive-settings/settings/flex-item/properties.ts +60 -0
  42. package/core-js/blocks-filters/responsive-settings/settings/flex-item/registration.ts +78 -0
  43. package/core-js/blocks-filters/responsive-settings/settings/flex-item/settings.tsx +90 -0
  44. package/core-js/blocks-filters/responsive-settings/settings/flexbox/index.ts +4 -0
  45. package/core-js/blocks-filters/responsive-settings/settings/flexbox/properties.ts +80 -0
  46. package/core-js/blocks-filters/responsive-settings/settings/flexbox/registration.ts +66 -0
  47. package/core-js/blocks-filters/responsive-settings/settings/flexbox/settings.tsx +78 -0
  48. package/core-js/blocks-filters/responsive-settings/settings/grid/index.ts +4 -0
  49. package/core-js/blocks-filters/responsive-settings/settings/grid/properties.ts +72 -0
  50. package/core-js/blocks-filters/responsive-settings/settings/grid/registration.ts +66 -0
  51. package/core-js/blocks-filters/responsive-settings/settings/grid/settings.tsx +78 -0
  52. package/core-js/blocks-filters/responsive-settings/settings/grid-item/index.ts +4 -0
  53. package/core-js/blocks-filters/responsive-settings/settings/grid-item/properties.ts +44 -0
  54. package/core-js/blocks-filters/responsive-settings/settings/grid-item/registration.ts +74 -0
  55. package/core-js/blocks-filters/responsive-settings/settings/grid-item/settings.tsx +87 -0
  56. package/core-js/blocks-filters/responsive-settings/settings/layout/index.ts +4 -0
  57. package/core-js/blocks-filters/responsive-settings/settings/layout/properties.ts +51 -0
  58. package/core-js/blocks-filters/responsive-settings/settings/layout/registration.ts +96 -0
  59. package/core-js/blocks-filters/responsive-settings/settings/layout/settings.tsx +64 -0
  60. package/core-js/blocks-filters/responsive-settings/settings/presets/index.ts +4 -0
  61. package/core-js/blocks-filters/responsive-settings/settings/presets/presets.ts +52 -0
  62. package/core-js/blocks-filters/responsive-settings/settings/presets/registration.ts +53 -0
  63. package/core-js/blocks-filters/responsive-settings/settings/presets/settings.tsx +100 -0
  64. package/core-js/blocks-filters/responsive-settings/settings/shared/gap-values.ts +16 -0
  65. package/core-js/blocks-filters/responsive-settings/settings/shared/layout-values.ts +56 -0
  66. package/core-js/blocks-filters/responsive-settings/settings/shared/tw-values.ts +107 -0
  67. package/core-js/blocks-filters/responsive-settings/settings/sizing/index.ts +4 -0
  68. package/core-js/blocks-filters/responsive-settings/settings/sizing/properties.ts +71 -0
  69. package/core-js/blocks-filters/responsive-settings/settings/sizing/registration.ts +52 -0
  70. package/core-js/blocks-filters/responsive-settings/settings/sizing/settings.tsx +96 -0
  71. package/core-js/blocks-filters/responsive-settings/settings/spacing/index.ts +7 -2
  72. package/core-js/blocks-filters/responsive-settings/settings/spacing/panel.tsx +5 -45
  73. package/core-js/blocks-filters/responsive-settings/settings/spacing/properties.ts +51 -29
  74. package/core-js/blocks-filters/responsive-settings/settings/spacing/registration.ts +53 -0
  75. package/core-js/blocks-filters/responsive-settings/settings/spacing/settings.tsx +26 -55
  76. package/core-js/blocks-filters/responsive-settings/types/index.ts +174 -28
  77. package/core-js/blocks-filters/responsive-settings/utils.ts +247 -216
  78. package/core-js/config/index.ts +6 -0
  79. package/core-js/config/webentor-config.ts +44 -2
  80. package/core-js/index.ts +8 -10
  81. package/core-js/types/index.ts +6 -0
  82. package/package.json +116 -6
  83. package/public/build/assets/_utils-CzK6Vfiv.js +2 -0
  84. package/public/build/assets/{_utils-PDaZ1Dn1.js.map → _utils-CzK6Vfiv.js.map} +1 -1
  85. package/public/build/assets/coreAppStyles-CukxHLz7.css +1 -0
  86. package/public/build/assets/coreEditorJs-DYd3ZopL.js +366 -0
  87. package/public/build/assets/coreEditorJs-DYd3ZopL.js.map +1 -0
  88. package/public/build/assets/coreEditorStyles-I9xzOGSX.css +1 -0
  89. package/public/build/assets/resources/blocks/e-table/{script-BIchbcPK.js → script-C_Z50hjm.js} +2 -2
  90. package/public/build/assets/resources/blocks/e-table/{script-BIchbcPK.js.map → script-C_Z50hjm.js.map} +1 -1
  91. package/public/build/assets/{sliderJs-Ch69_tVA.js → sliderJs-CyGnrv0Q.js} +3 -3
  92. package/public/build/assets/{sliderJs-Ch69_tVA.js.map → sliderJs-CyGnrv0Q.js.map} +1 -1
  93. package/public/build/manifest.json +10 -10
  94. package/resources/blocks/e-accordion-group/block.json +6 -4
  95. package/resources/blocks/e-gallery/block.json +2 -2
  96. package/resources/blocks/e-gallery/e-gallery.block.tsx +4 -0
  97. package/resources/blocks/e-image/e-image.block.tsx +4 -0
  98. package/resources/blocks/e-slider/block.json +3 -2
  99. package/resources/blocks/e-tab-container/block.json +2 -1
  100. package/resources/blocks/e-tabs/block.json +2 -1
  101. package/resources/blocks/l-flexible-container/block.json +3 -2
  102. package/resources/blocks/l-mobile-nav/block.json +2 -1
  103. package/resources/blocks/l-nav-menu/block.json +2 -1
  104. package/resources/blocks/l-nav-menu/l-nav-menu.block.tsx +2 -0
  105. package/resources/blocks/l-section/block.json +7 -5
  106. package/resources/blocks/l-section/l-section.block.tsx +40 -31
  107. package/resources/scripts/editor.ts +2 -0
  108. package/resources/styles/common/_editor.css +22 -0
  109. package/resources/styles/common/_utilities.css +210 -0
  110. package/core-js/blocks-filters/responsive-settings/constants.ts +0 -11
  111. package/core-js/blocks-filters/responsive-settings/settings/container/display/index.ts +0 -2
  112. package/core-js/blocks-filters/responsive-settings/settings/container/display/properties.ts +0 -167
  113. package/core-js/blocks-filters/responsive-settings/settings/container/display/settings.tsx +0 -73
  114. package/core-js/blocks-filters/responsive-settings/settings/container/flexbox/index.ts +0 -2
  115. package/core-js/blocks-filters/responsive-settings/settings/container/flexbox/properties.ts +0 -187
  116. package/core-js/blocks-filters/responsive-settings/settings/container/flexbox/settings.tsx +0 -131
  117. package/core-js/blocks-filters/responsive-settings/settings/container/grid/index.ts +0 -2
  118. package/core-js/blocks-filters/responsive-settings/settings/container/grid/properties.ts +0 -187
  119. package/core-js/blocks-filters/responsive-settings/settings/container/grid/settings.tsx +0 -132
  120. package/core-js/blocks-filters/responsive-settings/settings/container/index.ts +0 -4
  121. package/core-js/blocks-filters/responsive-settings/settings/container/panel.tsx +0 -92
  122. package/public/build/assets/_utils-PDaZ1Dn1.js +0 -2
  123. package/public/build/assets/coreAppStyles-Dp0WYk4N.css +0 -1
  124. package/public/build/assets/coreEditorJs-Cyc87wTo.js +0 -366
  125. package/public/build/assets/coreEditorJs-Cyc87wTo.js.map +0 -1
  126. package/public/build/assets/coreEditorStyles-D8-nNpQG.css +0 -1
@@ -0,0 +1,189 @@
1
+ /**
2
+ * AppliedClassesViewer — Toolbar button (eye icon) that reveals a popover
3
+ * listing all Tailwind classes generated by responsive settings for the
4
+ * current block. Rendered inside BlockControls > ToolbarGroup in the
5
+ * block toolbar so it covers all panels (spacing, layout, borders).
6
+ *
7
+ * Classes are grouped by panel group (Spacing, Display & Layout, Border)
8
+ * for easier scanning.
9
+ */
10
+ import { getBlockType } from '@wordpress/blocks';
11
+ import { Popover, ToolbarButton } from '@wordpress/components';
12
+ import { useMemo, useState } from '@wordpress/element';
13
+ import { __ } from '@wordpress/i18n';
14
+ import { postList } from '@wordpress/icons';
15
+
16
+ import { useBlockParent } from '../../../blocks-utils/_use-block-parent';
17
+ import { registry } from '../registry';
18
+ import { BlockPanelProps, ClassGenContext, PanelGroup } from '../types';
19
+
20
+ interface ClassGroup {
21
+ group: PanelGroup;
22
+ label: string;
23
+ classes: string[];
24
+ }
25
+
26
+ const GROUP_LABELS: Record<PanelGroup, string> = {
27
+ spacing: 'Spacing',
28
+ displayLayout: 'Display & Layout',
29
+ border: 'Border',
30
+ blockLink: 'Block Link',
31
+ };
32
+
33
+ /** Panel group render order */
34
+ const GROUP_ORDER: PanelGroup[] = [
35
+ 'spacing',
36
+ 'displayLayout',
37
+ 'border',
38
+ 'blockLink',
39
+ ];
40
+
41
+ /**
42
+ * Collect responsive-settings classes grouped by panel group.
43
+ * Same logic as generateClassNames in utils.ts but without useBlockProps.
44
+ */
45
+ const collectGroupedClasses = (
46
+ attributes: Record<string, any>,
47
+ blockName: string,
48
+ parentAttributes: Record<string, any> | undefined,
49
+ orderedBreakpoints: string[],
50
+ ): ClassGroup[] => {
51
+ const blockSettings = getBlockType(blockName);
52
+ const supports = blockSettings?.supports;
53
+
54
+ const context: ClassGenContext = {
55
+ blockName,
56
+ supports,
57
+ parentBlockAttributes: parentAttributes,
58
+ breakpoints: orderedBreakpoints,
59
+ };
60
+
61
+ const allSettings = registry.getAll();
62
+
63
+ // Collect all breakpoints present in any attribute
64
+ const breakpoints = new Set<string>();
65
+ for (const def of allSettings) {
66
+ if (!registry.isSupported(supports, def)) continue;
67
+ for (const attrKey of Object.keys(def.attributeSchema)) {
68
+ const attrGroup = attributes[attrKey];
69
+ if (!attrGroup || typeof attrGroup !== 'object') continue;
70
+ for (const prop of Object.values(attrGroup)) {
71
+ const propData = prop as any;
72
+ if (propData?.value) {
73
+ for (const bp of Object.keys(propData.value)) {
74
+ breakpoints.add(bp);
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
80
+
81
+ // Group classes by panelGroup
82
+ const groupMap = new Map<PanelGroup, string[]>();
83
+ for (const bp of breakpoints) {
84
+ for (const def of allSettings) {
85
+ if (!registry.isSupported(supports, def)) continue;
86
+ const generated = def
87
+ .generateClasses(attributes, bp, context)
88
+ .filter(Boolean);
89
+ if (generated.length === 0) continue;
90
+
91
+ const existing = groupMap.get(def.panelGroup) ?? [];
92
+ existing.push(...generated);
93
+ groupMap.set(def.panelGroup, existing);
94
+ }
95
+ }
96
+
97
+ return GROUP_ORDER.filter((g) => groupMap.has(g)).map((g) => ({
98
+ group: g,
99
+ label: GROUP_LABELS[g],
100
+ classes: groupMap.get(g)!,
101
+ }));
102
+ };
103
+
104
+ export const AppliedClassesViewer = ({
105
+ attributes,
106
+ name,
107
+ breakpoints,
108
+ }: BlockPanelProps) => {
109
+ const [isOpen, setIsOpen] = useState(false);
110
+ const parentBlock = useBlockParent();
111
+
112
+ const groups = useMemo(
113
+ () =>
114
+ collectGroupedClasses(
115
+ attributes,
116
+ name,
117
+ parentBlock?.attributes,
118
+ breakpoints,
119
+ ),
120
+ [attributes, name, parentBlock?.attributes, breakpoints],
121
+ );
122
+
123
+ if (groups.every((g) => g.classes.length === 0)) return null;
124
+
125
+ return (
126
+ <>
127
+ <ToolbarButton
128
+ icon={postList}
129
+ label={__('View applied classes', 'webentor')}
130
+ onClick={() => setIsOpen((prev) => !prev)}
131
+ isPressed={isOpen}
132
+ />
133
+ {isOpen && (
134
+ <Popover
135
+ placement="bottom-start"
136
+ onClose={() => setIsOpen(false)}
137
+ shift
138
+ >
139
+ <div
140
+ style={{
141
+ padding: '12px',
142
+ maxWidth: '320px',
143
+ maxHeight: '280px',
144
+ overflow: 'auto',
145
+ }}
146
+ >
147
+ {groups.map((group, gi) => (
148
+ <div key={group.group} style={{ marginTop: gi > 0 ? '12px' : 0 }}>
149
+ <p
150
+ style={{
151
+ fontSize: '11px',
152
+ textTransform: 'uppercase',
153
+ marginTop: 0,
154
+ marginBottom: '6px',
155
+ color: '#757575',
156
+ }}
157
+ >
158
+ {group.label}
159
+ </p>
160
+ <div
161
+ style={{
162
+ display: 'flex',
163
+ flexWrap: 'wrap',
164
+ gap: '4px',
165
+ }}
166
+ >
167
+ {group.classes.map((cls, i) => (
168
+ <code
169
+ key={`${cls}-${i}`}
170
+ style={{
171
+ fontSize: '11px',
172
+ padding: '2px 6px',
173
+ background: '#f0f0f0',
174
+ borderRadius: '3px',
175
+ whiteSpace: 'nowrap',
176
+ }}
177
+ >
178
+ {cls}
179
+ </code>
180
+ ))}
181
+ </div>
182
+ </div>
183
+ ))}
184
+ </div>
185
+ </Popover>
186
+ )}
187
+ </>
188
+ );
189
+ };
@@ -0,0 +1,346 @@
1
+ import { Icon, SelectControl, Tooltip } from '@wordpress/components';
2
+ import { __, sprintf } from '@wordpress/i18n';
3
+ import {
4
+ sidesBottom,
5
+ sidesHorizontal,
6
+ sidesLeft,
7
+ sidesRight,
8
+ sidesTop,
9
+ sidesVertical,
10
+ } from '@wordpress/icons';
11
+
12
+ import { setImmutably } from '../../../_utils';
13
+ import { SelectOption } from '../types';
14
+ import { getEffectiveValue, getInheritedFromBreakpoint } from '../utils';
15
+ import { LinkedValuesControl, LinkMode } from './LinkedValuesControl';
16
+
17
+ interface Side {
18
+ /** Attribute property name, e.g. 'margin-top' */
19
+ name: string;
20
+ label: string;
21
+ values: SelectOption[];
22
+ }
23
+
24
+ interface BoxModelControlProps {
25
+ type: 'margin' | 'padding';
26
+ sides: Side[];
27
+ attributes: Record<string, any>;
28
+ setAttributes: (attrs: Record<string, any>) => void;
29
+ /** Attribute group key, e.g. 'spacing' */
30
+ attributeKey: string;
31
+ breakpoint: string;
32
+ /** Ordered breakpoint names for cascade indicators */
33
+ breakpoints?: string[];
34
+ disabled?: boolean;
35
+ }
36
+
37
+ // ── Prefix helpers ──────────────────────────────────────────────────
38
+ // Spacing values are stored as full Tailwind classes (e.g. "mt-4").
39
+ // When linking sides we need to map the scale part to each side's prefix.
40
+
41
+ /** Extract the TW utility prefix from a side's value list (e.g. "mt" from "mt-4"). */
42
+ const getSidePrefix = (side: Side): string | null => {
43
+ const entry = side.values.find((v) => v.value !== '');
44
+ if (!entry) return null;
45
+ const match = entry.value.match(/^([a-z]+)-/);
46
+ return match ? match[1] : null;
47
+ };
48
+
49
+ /** Extract the scale/key portion from a TW class: "mt-4" → "4", "mt-auto" → "auto". */
50
+ const extractScale = (value: string): string | null => {
51
+ const match = value.match(/^[a-z]+-(.+)$/);
52
+ return match ? match[1] : null;
53
+ };
54
+
55
+ /** Build the correct TW class for a target side given a scale: ("mr", "4") → "mr-4". */
56
+ const buildValue = (prefix: string, scale: string): string =>
57
+ `${prefix}-${scale}`;
58
+
59
+ /**
60
+ * Visual box-model control for margin or padding.
61
+ * Two states: linked (H+V axis selects) or unlinked (4-corner grid).
62
+ */
63
+ export const BoxModelControl = ({
64
+ type,
65
+ sides,
66
+ attributes,
67
+ setAttributes,
68
+ attributeKey,
69
+ breakpoint,
70
+ breakpoints,
71
+ disabled = false,
72
+ }: BoxModelControlProps) => {
73
+ const linkModeKey = `_${type}LinkMode`;
74
+ const currentLinkMode: LinkMode =
75
+ (attributes[attributeKey]?.[linkModeKey]?.value?.[breakpoint] as
76
+ | LinkMode
77
+ | undefined) ?? 'unlinked';
78
+
79
+ const topSide = sides.find((s) => s.name.includes('top'));
80
+ const bottomSide = sides.find((s) => s.name.includes('bottom'));
81
+ const leftSide = sides.find((s) => s.name.includes('left'));
82
+ const rightSide = sides.find((s) => s.name.includes('right'));
83
+
84
+ const isLinked = currentLinkMode === 'linked';
85
+
86
+ const getValue = (side?: Side): string =>
87
+ side
88
+ ? (attributes[attributeKey]?.[side.name]?.value?.[breakpoint] ?? '')
89
+ : '';
90
+
91
+ // ── Mode toggle: sync pairs when switching to linked ──────────
92
+ const setLinkMode = (nextMode: LinkMode) => {
93
+ let newAttrs = setImmutably(
94
+ attributes,
95
+ [attributeKey, linkModeKey, 'value', breakpoint],
96
+ nextMode,
97
+ );
98
+
99
+ // Sync pairs when switching to linked: left→right, top→bottom
100
+ if (nextMode === 'linked') {
101
+ if (leftSide && rightSide) {
102
+ const leftVal = getValue(leftSide);
103
+ if (leftVal) {
104
+ const scale = extractScale(leftVal);
105
+ const rightPrefix = getSidePrefix(rightSide);
106
+ if (scale && rightPrefix) {
107
+ newAttrs = setImmutably(
108
+ newAttrs,
109
+ [attributeKey, rightSide.name, 'value', breakpoint],
110
+ buildValue(rightPrefix, scale),
111
+ );
112
+ }
113
+ }
114
+ }
115
+ if (topSide && bottomSide) {
116
+ const topVal = getValue(topSide);
117
+ if (topVal) {
118
+ const scale = extractScale(topVal);
119
+ const bottomPrefix = getSidePrefix(bottomSide);
120
+ if (scale && bottomPrefix) {
121
+ newAttrs = setImmutably(
122
+ newAttrs,
123
+ [attributeKey, bottomSide.name, 'value', breakpoint],
124
+ buildValue(bottomPrefix, scale),
125
+ );
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ setAttributes(newAttrs);
132
+ };
133
+
134
+ // ── Value change with pair propagation in linked mode ─────────
135
+ const handleChange = (sideName: string, value: string) => {
136
+ // Determine which sides to update together
137
+ let targetSides: Side[];
138
+
139
+ if (isLinked) {
140
+ const isHorizontal =
141
+ sideName.includes('left') || sideName.includes('right');
142
+ if (isHorizontal) {
143
+ targetSides = [leftSide, rightSide].filter(Boolean) as Side[];
144
+ } else {
145
+ targetSides = [topSide, bottomSide].filter(Boolean) as Side[];
146
+ }
147
+ } else {
148
+ const side = sides.find((s) => s.name === sideName);
149
+ targetSides = side ? [side] : [];
150
+ }
151
+
152
+ let newAttrs = { ...attributes };
153
+
154
+ if (!value) {
155
+ for (const side of targetSides) {
156
+ newAttrs = setImmutably(
157
+ newAttrs,
158
+ [attributeKey, side.name, 'value', breakpoint],
159
+ '',
160
+ );
161
+ }
162
+ } else {
163
+ const scale = extractScale(value);
164
+ for (const side of targetSides) {
165
+ const prefix = getSidePrefix(side);
166
+ const mapped = scale && prefix ? buildValue(prefix, scale) : value;
167
+ newAttrs = setImmutably(
168
+ newAttrs,
169
+ [attributeKey, side.name, 'value', breakpoint],
170
+ mapped,
171
+ );
172
+ }
173
+ }
174
+
175
+ setAttributes(newAttrs);
176
+ };
177
+
178
+ const resetAll = () => {
179
+ let newAttrs = { ...attributes };
180
+ for (const side of sides) {
181
+ newAttrs = setImmutably(
182
+ newAttrs,
183
+ [attributeKey, side.name, 'value', breakpoint],
184
+ '',
185
+ );
186
+ }
187
+ setAttributes(newAttrs);
188
+ };
189
+
190
+ const hasAnyValue = sides.some(
191
+ (s) => !!attributes[attributeKey]?.[s.name]?.value?.[breakpoint],
192
+ );
193
+
194
+ // ── Select renderer with cascade indicators ─────────────────────
195
+ const renderSelect = (
196
+ side: Side | undefined,
197
+ labelOverride?: string,
198
+ hideLabel?: boolean,
199
+ ) => {
200
+ if (!side) return null;
201
+
202
+ const explicitValue = getValue(side);
203
+ let options = side.values;
204
+ let inheritedClassName = '';
205
+
206
+ if (!explicitValue && breakpoints?.length) {
207
+ const inheritedFrom = getInheritedFromBreakpoint(
208
+ attributes,
209
+ attributeKey,
210
+ side.name,
211
+ breakpoint,
212
+ breakpoints,
213
+ );
214
+ if (inheritedFrom) {
215
+ const inheritedValue = getEffectiveValue(
216
+ attributes,
217
+ attributeKey,
218
+ side.name,
219
+ breakpoint,
220
+ breakpoints,
221
+ );
222
+ if (inheritedValue) {
223
+ const label =
224
+ side.values.find((o) => o.value === inheritedValue)?.label ??
225
+ inheritedValue;
226
+ const placeholderText = sprintf(
227
+ __('%s (from %s)', 'webentor'),
228
+ label,
229
+ inheritedFrom,
230
+ );
231
+ options = [
232
+ { label: placeholderText, value: '' },
233
+ ...side.values.filter((o) => o.value !== ''),
234
+ ];
235
+ inheritedClassName = 'wbtr-inherited-value';
236
+ }
237
+ }
238
+ }
239
+
240
+ return (
241
+ <SelectControl
242
+ label={labelOverride ?? side.label}
243
+ hideLabelFromVision={hideLabel}
244
+ value={explicitValue}
245
+ options={options}
246
+ disabled={disabled}
247
+ onChange={(val) => handleChange(side.name, val)}
248
+ className={inheritedClassName}
249
+ __nextHasNoMarginBottom
250
+ />
251
+ );
252
+ };
253
+
254
+ return (
255
+ <div className="wbtr:my-2 wbtr:flex wbtr:flex-col wbtr:gap-2 wbtr:border wbtr:border-editor-border wbtr:p-2">
256
+ <div className="wbtr:flex wbtr:items-center wbtr:gap-2">
257
+ <p className="wbtr:mb-0! wbtr:text-12 wbtr:uppercase">
258
+ {type === 'margin'
259
+ ? __('Margin', 'webentor')
260
+ : __('Padding', 'webentor')}
261
+ </p>
262
+
263
+ <LinkedValuesControl
264
+ mode={currentLinkMode}
265
+ onModeChange={setLinkMode}
266
+ onReset={resetAll}
267
+ resetDisabled={!hasAnyValue}
268
+ />
269
+ </div>
270
+
271
+ {isLinked ? (
272
+ <div className="wbtr:flex wbtr:flex-col wbtr:gap-1.5">
273
+ <div className="wbtr:flex wbtr:items-center wbtr:gap-2">
274
+ <Tooltip text={__('Horizontal', 'webentor')}>
275
+ <span className="wbtr:flex">
276
+ <Icon icon={sidesHorizontal} size={20} />
277
+ </span>
278
+ </Tooltip>
279
+ <div className="wbtr:flex-1">
280
+ {renderSelect(leftSide, __('Horizontal', 'webentor'), true)}
281
+ </div>
282
+ </div>
283
+ <div className="wbtr:flex wbtr:items-center wbtr:gap-2">
284
+ <Tooltip text={__('Vertical', 'webentor')}>
285
+ <span className="wbtr:flex">
286
+ <Icon icon={sidesVertical} size={20} />
287
+ </span>
288
+ </Tooltip>
289
+ <div className="wbtr:flex-1">
290
+ {renderSelect(topSide, __('Vertical', 'webentor'), true)}
291
+ </div>
292
+ </div>
293
+ </div>
294
+ ) : (
295
+ <div
296
+ style={{
297
+ display: 'grid',
298
+ gridTemplateColumns: '1fr 1fr',
299
+ gap: '4px',
300
+ }}
301
+ >
302
+ <div className="wbtr:flex wbtr:items-center wbtr:gap-1.5">
303
+ <Tooltip text={__('Top', 'webentor')}>
304
+ <span className="wbtr:flex">
305
+ <Icon icon={sidesTop} size={20} />
306
+ </span>
307
+ </Tooltip>
308
+ <div className="wbtr:flex-1">
309
+ {renderSelect(topSide, topSide?.label, true)}
310
+ </div>
311
+ </div>
312
+ <div className="wbtr:flex wbtr:items-center wbtr:gap-1.5">
313
+ <Tooltip text={__('Right', 'webentor')}>
314
+ <span className="wbtr:flex">
315
+ <Icon icon={sidesRight} size={20} />
316
+ </span>
317
+ </Tooltip>
318
+ <div className="wbtr:flex-1">
319
+ {renderSelect(rightSide, rightSide?.label, true)}
320
+ </div>
321
+ </div>
322
+ <div className="wbtr:flex wbtr:items-center wbtr:gap-1.5">
323
+ <Tooltip text={__('Bottom', 'webentor')}>
324
+ <span className="wbtr:flex">
325
+ <Icon icon={sidesBottom} size={20} />
326
+ </span>
327
+ </Tooltip>
328
+ <div className="wbtr:flex-1">
329
+ {renderSelect(bottomSide, bottomSide?.label, true)}
330
+ </div>
331
+ </div>
332
+ <div className="wbtr:flex wbtr:items-center wbtr:gap-1.5">
333
+ <Tooltip text={__('Left', 'webentor')}>
334
+ <span className="wbtr:flex">
335
+ <Icon icon={sidesLeft} size={20} />
336
+ </span>
337
+ </Tooltip>
338
+ <div className="wbtr:flex-1">
339
+ {renderSelect(leftSide, leftSide?.label, true)}
340
+ </div>
341
+ </div>
342
+ </div>
343
+ )}
344
+ </div>
345
+ );
346
+ };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * BreakpointResetButton — Clears all setting values for a specific
3
+ * breakpoint within a panel group. Renders as a small button at the
4
+ * top of each breakpoint tab.
5
+ */
6
+ import { Button } from '@wordpress/components';
7
+ import { __ } from '@wordpress/i18n';
8
+
9
+ import { registry } from '../registry';
10
+ import { PanelGroup } from '../types';
11
+
12
+ interface BreakpointResetButtonProps {
13
+ panelGroup: PanelGroup;
14
+ breakpoint: string;
15
+ attributes: Record<string, any>;
16
+ setAttributes: (attrs: Record<string, any>) => void;
17
+ }
18
+
19
+ export const BreakpointResetButton = ({
20
+ panelGroup,
21
+ breakpoint,
22
+ attributes,
23
+ setAttributes,
24
+ }: BreakpointResetButtonProps) => {
25
+ const modules = registry.getByPanelGroup(panelGroup);
26
+
27
+ const hasAnyValues = modules.some((def) => {
28
+ // Check all attribute keys the module owns.
29
+ for (const key of Object.keys(def.attributeSchema)) {
30
+ const attrGroup = attributes?.[key];
31
+ if (!attrGroup || typeof attrGroup !== 'object') continue;
32
+ if (
33
+ Object.values(attrGroup).some(
34
+ (prop: any) => prop?.value?.[breakpoint] !== undefined,
35
+ )
36
+ )
37
+ return true;
38
+ }
39
+ return false;
40
+ });
41
+
42
+ if (!hasAnyValues) return null;
43
+
44
+ const handleReset = () => {
45
+ const patch: Record<string, any> = {};
46
+
47
+ for (const def of modules) {
48
+ // Iterate all attribute keys in the module's schema.
49
+ for (const key of Object.keys(def.attributeSchema)) {
50
+ const attrGroup = attributes?.[key];
51
+ if (!attrGroup || typeof attrGroup !== 'object') continue;
52
+
53
+ const updatedGroup = { ...attrGroup };
54
+ let changed = false;
55
+
56
+ for (const [propName, propData] of Object.entries(updatedGroup)) {
57
+ const prop = propData as any;
58
+ if (prop?.value?.[breakpoint] !== undefined) {
59
+ const { [breakpoint]: _, ...rest } = prop.value;
60
+ updatedGroup[propName] = { ...prop, value: rest };
61
+ changed = true;
62
+ }
63
+ }
64
+
65
+ if (changed) {
66
+ patch[key] = updatedGroup;
67
+ }
68
+ }
69
+ }
70
+
71
+ if (Object.keys(patch).length) {
72
+ setAttributes(patch);
73
+ }
74
+ };
75
+
76
+ return (
77
+ <div
78
+ style={{
79
+ display: 'flex',
80
+ justifyContent: 'flex-end',
81
+ marginBottom: '8px',
82
+ }}
83
+ >
84
+ <Button
85
+ variant="tertiary"
86
+ isDestructive
87
+ size="small"
88
+ onClick={handleReset}
89
+ >
90
+ {__('Reset breakpoint', 'webentor')}
91
+ </Button>
92
+ </div>
93
+ );
94
+ };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * DebugPanel — Collapsible inspector panel showing raw responsive
3
+ * setting attributes. Gated behind window.WEBENTOR_WP_DEBUG.
4
+ */
5
+ import { PanelBody } from '@wordpress/components';
6
+ import { __ } from '@wordpress/i18n';
7
+
8
+ import { BlockPanelProps } from '../types';
9
+
10
+ declare global {
11
+ interface Window {
12
+ WEBENTOR_WP_DEBUG?: boolean;
13
+ }
14
+ }
15
+
16
+ const ATTRIBUTE_KEYS = [
17
+ 'layout',
18
+ 'sizing',
19
+ 'spacing',
20
+ 'flexbox',
21
+ 'grid',
22
+ 'flexItem',
23
+ 'gridItem',
24
+ 'border',
25
+ '_preset',
26
+ '_presetClasses',
27
+ 'blockLink',
28
+ // v1 keys are not supported anymore, kept here for debugging reference
29
+ 'display',
30
+ 'flexboxItem',
31
+ ] as const;
32
+
33
+ export const DebugPanel = ({ attributes }: BlockPanelProps) => {
34
+ if (!window.WEBENTOR_WP_DEBUG) {
35
+ return null;
36
+ }
37
+
38
+ const debugData: Record<string, any> = {};
39
+ for (const key of ATTRIBUTE_KEYS) {
40
+ if (attributes?.[key] !== undefined) {
41
+ debugData[key] = attributes[key];
42
+ }
43
+ }
44
+
45
+ return (
46
+ <PanelBody
47
+ title={__('Debug: Responsive Settings', 'webentor')}
48
+ initialOpen={false}
49
+ >
50
+ <pre
51
+ style={{
52
+ fontSize: '10px',
53
+ lineHeight: '1.4',
54
+ maxHeight: '400px',
55
+ overflow: 'auto',
56
+ background: '#f0f0f0',
57
+ padding: '8px',
58
+ borderRadius: '4px',
59
+ whiteSpace: 'pre-wrap',
60
+ wordBreak: 'break-all',
61
+ }}
62
+ >
63
+ {JSON.stringify(debugData, null, 2)}
64
+ </pre>
65
+ </PanelBody>
66
+ );
67
+ };