@akinon/next 2.0.0-beta.2 → 2.0.0-beta.20

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 (189) hide show
  1. package/.eslintrc.js +12 -0
  2. package/CHANGELOG.md +377 -7
  3. package/__tests__/next-config.test.ts +83 -0
  4. package/__tests__/tsconfig.json +23 -0
  5. package/api/auth.ts +133 -44
  6. package/api/barcode-search.ts +59 -0
  7. package/api/cache.ts +41 -5
  8. package/api/client.ts +21 -4
  9. package/api/form.ts +85 -0
  10. package/api/image-proxy.ts +75 -0
  11. package/api/product-categories.ts +53 -0
  12. package/api/similar-product-list.ts +63 -0
  13. package/api/similar-products.ts +111 -0
  14. package/api/virtual-try-on.ts +382 -0
  15. package/assets/styles/index.scss +84 -0
  16. package/babel.config.js +6 -0
  17. package/bin/pz-generate-routes.js +115 -0
  18. package/bin/pz-prebuild.js +1 -0
  19. package/bin/pz-predev.js +1 -0
  20. package/bin/pz-run-tests.js +99 -0
  21. package/bin/run-prebuild-tests.js +46 -0
  22. package/components/accordion.tsx +20 -5
  23. package/components/button.tsx +51 -36
  24. package/components/client-root.tsx +138 -2
  25. package/components/file-input.tsx +65 -3
  26. package/components/index.ts +1 -0
  27. package/components/input.tsx +1 -1
  28. package/components/link.tsx +46 -16
  29. package/components/logger-popup.tsx +213 -0
  30. package/components/modal.tsx +32 -16
  31. package/components/plugin-module.tsx +62 -3
  32. package/components/price.tsx +2 -2
  33. package/components/select.tsx +1 -1
  34. package/components/selected-payment-option-view.tsx +21 -0
  35. package/components/theme-editor/blocks/accordion-block.tsx +136 -0
  36. package/components/theme-editor/blocks/block-renderer-registry.tsx +77 -0
  37. package/components/theme-editor/blocks/button-block.tsx +593 -0
  38. package/components/theme-editor/blocks/counter-block.tsx +348 -0
  39. package/components/theme-editor/blocks/divider-block.tsx +20 -0
  40. package/components/theme-editor/blocks/embed-block.tsx +208 -0
  41. package/components/theme-editor/blocks/group-block.tsx +116 -0
  42. package/components/theme-editor/blocks/hotspot-block.tsx +147 -0
  43. package/components/theme-editor/blocks/icon-block.tsx +230 -0
  44. package/components/theme-editor/blocks/image-block.tsx +137 -0
  45. package/components/theme-editor/blocks/image-gallery-block.tsx +269 -0
  46. package/components/theme-editor/blocks/input-block.tsx +123 -0
  47. package/components/theme-editor/blocks/link-block.tsx +216 -0
  48. package/components/theme-editor/blocks/lottie-block.tsx +325 -0
  49. package/components/theme-editor/blocks/map-block.tsx +89 -0
  50. package/components/theme-editor/blocks/slider-block.tsx +595 -0
  51. package/components/theme-editor/blocks/tab-block.tsx +10 -0
  52. package/components/theme-editor/blocks/text-block.tsx +52 -0
  53. package/components/theme-editor/blocks/video-block.tsx +122 -0
  54. package/components/theme-editor/components/action-toolbar.tsx +305 -0
  55. package/components/theme-editor/components/designer-overlay.tsx +74 -0
  56. package/components/theme-editor/components/with-designer-features.tsx +142 -0
  57. package/components/theme-editor/dynamic-font-loader.tsx +79 -0
  58. package/components/theme-editor/hooks/use-designer-features.tsx +100 -0
  59. package/components/theme-editor/hooks/use-external-designer.tsx +95 -0
  60. package/components/theme-editor/hooks/use-native-widget-data.ts +188 -0
  61. package/components/theme-editor/hooks/use-visibility-context.ts +27 -0
  62. package/components/theme-editor/placeholder-registry.ts +31 -0
  63. package/components/theme-editor/sections/before-after-section.tsx +245 -0
  64. package/components/theme-editor/sections/contact-form-section.tsx +563 -0
  65. package/components/theme-editor/sections/countdown-campaign-banner-section.tsx +433 -0
  66. package/components/theme-editor/sections/coupon-banner-section.tsx +710 -0
  67. package/components/theme-editor/sections/divider-section.tsx +62 -0
  68. package/components/theme-editor/sections/featured-product-spotlight-section.tsx +507 -0
  69. package/components/theme-editor/sections/find-in-store-section.tsx +1995 -0
  70. package/components/theme-editor/sections/hover-showcase-section.tsx +326 -0
  71. package/components/theme-editor/sections/image-hotspot-section.tsx +142 -0
  72. package/components/theme-editor/sections/installment-options-section.tsx +1065 -0
  73. package/components/theme-editor/sections/notification-banner-section.tsx +173 -0
  74. package/components/theme-editor/sections/order-tracking-lookup-section.tsx +1379 -0
  75. package/components/theme-editor/sections/posts-slider-section.tsx +472 -0
  76. package/components/theme-editor/sections/pre-order-launch-banner-section.tsx +663 -0
  77. package/components/theme-editor/sections/section-renderer-registry.tsx +89 -0
  78. package/components/theme-editor/sections/section-wrapper.tsx +135 -0
  79. package/components/theme-editor/sections/shipping-threshold-progress-section.tsx +586 -0
  80. package/components/theme-editor/sections/stats-counter-section.tsx +486 -0
  81. package/components/theme-editor/sections/tabs-section.tsx +578 -0
  82. package/components/theme-editor/theme-block.tsx +102 -0
  83. package/components/theme-editor/theme-placeholder-client.tsx +218 -0
  84. package/components/theme-editor/theme-placeholder-wrapper.tsx +732 -0
  85. package/components/theme-editor/theme-placeholder.tsx +288 -0
  86. package/components/theme-editor/theme-section.tsx +1224 -0
  87. package/components/theme-editor/theme-settings-context.tsx +13 -0
  88. package/components/theme-editor/utils/index.ts +792 -0
  89. package/components/theme-editor/utils/iterator-utils.ts +234 -0
  90. package/components/theme-editor/utils/publish-window.ts +86 -0
  91. package/components/theme-editor/utils/visibility-rules.ts +188 -0
  92. package/data/client/account.ts +17 -2
  93. package/data/client/api.ts +2 -0
  94. package/data/client/basket.ts +66 -5
  95. package/data/client/checkout.ts +391 -99
  96. package/data/client/misc.ts +38 -2
  97. package/data/client/product.ts +19 -2
  98. package/data/client/user.ts +16 -8
  99. package/data/server/category.ts +11 -9
  100. package/data/server/flatpage.ts +11 -4
  101. package/data/server/form.ts +15 -4
  102. package/data/server/landingpage.ts +11 -4
  103. package/data/server/list.ts +5 -4
  104. package/data/server/menu.ts +11 -3
  105. package/data/server/product.ts +111 -55
  106. package/data/server/seo.ts +14 -4
  107. package/data/server/special-page.ts +5 -4
  108. package/data/server/widget.ts +90 -5
  109. package/data/urls.ts +16 -5
  110. package/hocs/client/with-segment-defaults.tsx +2 -2
  111. package/hocs/server/with-segment-defaults.tsx +65 -20
  112. package/hooks/index.ts +4 -0
  113. package/hooks/use-localization.ts +24 -10
  114. package/hooks/use-logger-context.tsx +114 -0
  115. package/hooks/use-logger.ts +92 -0
  116. package/hooks/use-loyalty-availability.ts +21 -0
  117. package/hooks/use-payment-options.ts +2 -1
  118. package/hooks/use-pz-params.ts +37 -0
  119. package/hooks/use-router.ts +51 -14
  120. package/hooks/use-sentry-uncaught-errors.ts +24 -0
  121. package/instrumentation/index.ts +10 -1
  122. package/instrumentation/node.ts +2 -20
  123. package/jest.config.js +25 -0
  124. package/lib/cache-handler.mjs +534 -16
  125. package/lib/cache.ts +272 -37
  126. package/localization/index.ts +2 -1
  127. package/localization/provider.tsx +2 -5
  128. package/middlewares/bfcache-headers.ts +18 -0
  129. package/middlewares/checkout-provider.ts +1 -1
  130. package/middlewares/complete-gpay.ts +32 -26
  131. package/middlewares/complete-masterpass.ts +33 -26
  132. package/middlewares/complete-wallet.ts +182 -0
  133. package/middlewares/default.ts +360 -215
  134. package/middlewares/index.ts +10 -2
  135. package/middlewares/locale.ts +34 -11
  136. package/middlewares/masterpass-rest-callback.ts +230 -0
  137. package/middlewares/oauth-login.ts +200 -57
  138. package/middlewares/pretty-url.ts +21 -8
  139. package/middlewares/redirection-payment.ts +32 -26
  140. package/middlewares/saved-card-redirection.ts +33 -26
  141. package/middlewares/three-d-redirection.ts +32 -26
  142. package/middlewares/url-redirection.ts +11 -1
  143. package/middlewares/wallet-complete-redirection.ts +206 -0
  144. package/package.json +25 -10
  145. package/plugins.d.ts +19 -4
  146. package/plugins.js +10 -1
  147. package/redux/actions.ts +47 -0
  148. package/redux/middlewares/checkout.ts +63 -138
  149. package/redux/middlewares/index.ts +14 -10
  150. package/redux/middlewares/pre-order/address.ts +7 -2
  151. package/redux/middlewares/pre-order/attribute-based-shipping-option.ts +7 -1
  152. package/redux/middlewares/pre-order/data-source-shipping-option.ts +7 -1
  153. package/redux/middlewares/pre-order/delivery-option.ts +7 -1
  154. package/redux/middlewares/pre-order/index.ts +16 -10
  155. package/redux/middlewares/pre-order/installment-option.ts +8 -1
  156. package/redux/middlewares/pre-order/payment-option-reset.ts +37 -0
  157. package/redux/middlewares/pre-order/payment-option.ts +7 -1
  158. package/redux/middlewares/pre-order/pre-order-validation.ts +8 -3
  159. package/redux/middlewares/pre-order/redirection.ts +8 -2
  160. package/redux/middlewares/pre-order/set-pre-order.ts +6 -2
  161. package/redux/middlewares/pre-order/shipping-option.ts +7 -1
  162. package/redux/middlewares/pre-order/shipping-step.ts +5 -1
  163. package/redux/reducers/checkout.ts +23 -3
  164. package/redux/reducers/index.ts +11 -3
  165. package/redux/reducers/root.ts +7 -2
  166. package/redux/reducers/widget.ts +80 -0
  167. package/sentry/index.ts +69 -13
  168. package/tailwind/content.js +16 -0
  169. package/types/commerce/account.ts +5 -1
  170. package/types/commerce/checkout.ts +35 -1
  171. package/types/commerce/widget.ts +33 -0
  172. package/types/index.ts +101 -6
  173. package/types/next-auth.d.ts +2 -2
  174. package/types/widget.ts +80 -0
  175. package/utils/app-fetch.ts +7 -2
  176. package/utils/generate-commerce-search-params.ts +3 -2
  177. package/utils/get-checkout-path.ts +3 -0
  178. package/utils/get-root-hostname.ts +28 -0
  179. package/utils/index.ts +64 -10
  180. package/utils/localization.ts +4 -0
  181. package/utils/mobile-3d-iframe.ts +8 -2
  182. package/utils/override-middleware.ts +7 -12
  183. package/utils/pz-segments.ts +92 -0
  184. package/utils/redirect-ignore.ts +35 -0
  185. package/utils/redirect.ts +9 -3
  186. package/utils/redirection-iframe.ts +8 -2
  187. package/utils/widget-styles.ts +107 -0
  188. package/views/error-page.tsx +93 -0
  189. package/with-pz-config.js +13 -6
@@ -0,0 +1,792 @@
1
+ import React from 'react';
2
+ export * from './publish-window';
3
+
4
+ type ThemeSettings = Record<string, unknown> | null;
5
+
6
+ const THEME_CSS_VARIABLE_MAP: Record<string, string> = {
7
+ '--theme-primary': 'primaryColor',
8
+ '--theme-secondary': 'secondaryColor',
9
+ '--theme-color3': 'color3',
10
+ '--theme-color4': 'color4',
11
+ '--theme-background': 'backgroundColor'
12
+ };
13
+
14
+ const CSS_VAR_REGEX = /var\((--[\w-]+)(?:,\s*([^)]+))?\)/g;
15
+
16
+ const stripQuotes = (value: string) => value.replace(/^['"]|['"]$/g, '').trim();
17
+
18
+ const getComputedCssVariable = (variableName: string): string | null => {
19
+ if (typeof window === 'undefined') {
20
+ return null;
21
+ }
22
+
23
+ const computedValue = getComputedStyle(
24
+ document.documentElement
25
+ ).getPropertyValue(variableName);
26
+ return computedValue ? computedValue.trim() : null;
27
+ };
28
+
29
+ export const clampAlpha = (
30
+ value: unknown,
31
+ fallbackPercent = 100
32
+ ): number | null => {
33
+ const numeric = Number(
34
+ value === undefined || value === null || value === ''
35
+ ? fallbackPercent
36
+ : value
37
+ );
38
+ if (Number.isNaN(numeric)) return null;
39
+ return Math.max(0, Math.min(1, numeric / 100));
40
+ };
41
+
42
+ export const colorToRgba = (color: string, alpha: number): string | null => {
43
+ let normalized = color.trim();
44
+ // Some theme settings may provide hex without a leading '#'
45
+ if (
46
+ !normalized.startsWith('#') &&
47
+ /^[0-9a-f]{3}([0-9a-f]{3})?$/i.test(normalized)
48
+ ) {
49
+ normalized = `#${normalized}`;
50
+ }
51
+ const short = normalized.match(/^#([0-9a-f]{3})$/i);
52
+ const long = normalized.match(/^#([0-9a-f]{6})$/i);
53
+
54
+ if (short) {
55
+ const h = short[1];
56
+ const r = parseInt(h[0] + h[0], 16);
57
+ const g = parseInt(h[1] + h[1], 16);
58
+ const b = parseInt(h[2] + h[2], 16);
59
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
60
+ }
61
+
62
+ if (long) {
63
+ const h = long[1];
64
+ const r = parseInt(h.slice(0, 2), 16);
65
+ const g = parseInt(h.slice(2, 4), 16);
66
+ const b = parseInt(h.slice(4, 6), 16);
67
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
68
+ }
69
+
70
+ // rgb(12, 34, 56) or rgba(12, 34, 56, 0.5)
71
+ const commaRgb = normalized.match(
72
+ /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})(?:\s*,\s*([0-9.]+))?\s*\)$/i
73
+ );
74
+ if (commaRgb) {
75
+ const r = Number(commaRgb[1]);
76
+ const g = Number(commaRgb[2]);
77
+ const b = Number(commaRgb[3]);
78
+ if ([r, g, b].some((v) => Number.isNaN(v))) return null;
79
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
80
+ }
81
+
82
+ // rgb(12 34 56) or rgb(12 34 56 / 0.5)
83
+ const spaceRgb = normalized.match(
84
+ /^rgb\(\s*(\d{1,3})\s+(\d{1,3})\s+(\d{1,3})(?:\s*\/\s*([0-9.]+%?))?\s*\)$/i
85
+ );
86
+ if (spaceRgb) {
87
+ const r = Number(spaceRgb[1]);
88
+ const g = Number(spaceRgb[2]);
89
+ const b = Number(spaceRgb[3]);
90
+ if ([r, g, b].some((v) => Number.isNaN(v))) return null;
91
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
92
+ }
93
+
94
+ return null;
95
+ };
96
+
97
+ export const applyAlphaToColor = (color: string, alpha: number): string => {
98
+ if (alpha <= 0) return 'transparent';
99
+ if (alpha >= 1) return color;
100
+
101
+ const rgba = colorToRgba(color, alpha);
102
+ if (rgba) return rgba;
103
+
104
+ // Fallback for values like `var(--theme-secondary)` or named colors.
105
+ // Modern browsers support `color-mix` which lets us blend with transparent.
106
+ const percent = Math.round(alpha * 100);
107
+ return `color-mix(in srgb, ${color} ${percent}%, transparent)`;
108
+ };
109
+
110
+ export const resolveThemeCssVariables = (
111
+ value: string,
112
+ themeSettings?: ThemeSettings
113
+ ): string => {
114
+ if (typeof value !== 'string' || !value.includes('var(')) {
115
+ return value;
116
+ }
117
+
118
+ return value.replace(
119
+ CSS_VAR_REGEX,
120
+ (_match, variableName: string, fallbackValue?: string) => {
121
+ const mappedKey = THEME_CSS_VARIABLE_MAP[variableName];
122
+ const themeColor = mappedKey
123
+ ? (themeSettings?.[mappedKey] as string | undefined)
124
+ : undefined;
125
+ const computedValue = getComputedCssVariable(variableName);
126
+ const fallback = fallbackValue ? stripQuotes(fallbackValue) : undefined;
127
+
128
+ return themeColor || computedValue || fallback || `var(${variableName})`;
129
+ }
130
+ );
131
+ };
132
+
133
+ export const resolveThemeStyleObject = (
134
+ styles: React.CSSProperties,
135
+ themeSettings?: ThemeSettings
136
+ ): React.CSSProperties => {
137
+ const resolvedEntries = Object.entries(styles).map(([key, val]) => {
138
+ if (typeof val === 'string') {
139
+ return [key, resolveThemeCssVariables(val, themeSettings)];
140
+ }
141
+ return [key, val];
142
+ });
143
+
144
+ return Object.fromEntries(resolvedEntries) as React.CSSProperties;
145
+ };
146
+
147
+ export const getResponsiveValue = (
148
+ value: unknown,
149
+ breakpoint = 'desktop',
150
+ fallback: unknown = ''
151
+ ) => {
152
+ if (value === undefined || value === null || value === '') return fallback;
153
+ if (typeof value === 'object') {
154
+ const responsiveValue = value as Record<string, unknown>;
155
+ const matched = responsiveValue[breakpoint];
156
+ if (matched !== undefined) return matched;
157
+ if (responsiveValue.desktop !== undefined) return responsiveValue.desktop;
158
+ return fallback;
159
+ }
160
+ return value;
161
+ };
162
+
163
+ const kebabToCamel = (str: string): string => {
164
+ return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
165
+ };
166
+
167
+ export const getCSSStyles = (
168
+ styles?: Record<string, unknown>,
169
+ themeSettings?: ThemeSettings,
170
+ breakpoint: string = 'desktop'
171
+ ): React.CSSProperties => {
172
+ if (!styles) return {};
173
+
174
+ const cssStyles: Record<string, string | number> = {};
175
+
176
+ const normalizeKey = (key: string): string => {
177
+ if (key.includes('-')) return key;
178
+ return key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
179
+ };
180
+
181
+ const getStyleEntry = (kebabKey: string): unknown => {
182
+ // Some sources may serialize keys in camelCase.
183
+ // Prefer kebab-case but fall back to camelCase.
184
+ const camelKey = kebabToCamel(kebabKey);
185
+ return styles[kebabKey] ?? styles[camelKey];
186
+ };
187
+
188
+ const textOpacityEntry = getStyleEntry('text-opacity');
189
+ const textOpacityValue = textOpacityEntry
190
+ ? getResponsiveValue(textOpacityEntry, breakpoint, '100')
191
+ : '100';
192
+ const textOpacity = Number(textOpacityValue);
193
+ const textAlpha = !Number.isNaN(textOpacity)
194
+ ? Math.max(0, Math.min(1, textOpacity / 100))
195
+ : null;
196
+
197
+ const backgroundOpacityEntry = getStyleEntry('background-opacity');
198
+ const backgroundOpacityValue = backgroundOpacityEntry
199
+ ? getResponsiveValue(backgroundOpacityEntry, breakpoint, '100')
200
+ : '100';
201
+ const backgroundOpacity = Number(backgroundOpacityValue);
202
+ const backgroundAlpha = !Number.isNaN(backgroundOpacity)
203
+ ? Math.max(0, Math.min(1, backgroundOpacity / 100))
204
+ : null;
205
+
206
+ Object.keys(styles).forEach((key) => {
207
+ const value = getResponsiveValue(styles[key], breakpoint);
208
+ if (value === undefined || value === null) return;
209
+
210
+ const normalizedKey = normalizeKey(key);
211
+
212
+ // `text-opacity` is not a real CSS property; it should influence `color`.
213
+ if (normalizedKey === 'text-opacity') {
214
+ return;
215
+ }
216
+
217
+ // `background-opacity` is not a real CSS property; it should influence `background-color`.
218
+ if (normalizedKey === 'background-opacity') {
219
+ return;
220
+ }
221
+
222
+ // `hover-opacity` is not a real CSS property; it should influence hover state.
223
+ if (normalizedKey === 'hover-opacity') {
224
+ return;
225
+ }
226
+
227
+ // Skip generic `opacity` from button defaults to prevent element-wide transparency.
228
+ if (normalizedKey === 'opacity') {
229
+ return;
230
+ }
231
+
232
+ const camelKey = kebabToCamel(key);
233
+
234
+ if (
235
+ (key.startsWith('padding-') || key.startsWith('margin-')) &&
236
+ typeof value === 'number'
237
+ ) {
238
+ cssStyles[camelKey] = `${value}px`;
239
+ } else if (key === 'width' && value === 'fill') {
240
+ cssStyles[camelKey] = '100%';
241
+ } else if (key === 'width' && value === 'fit') {
242
+ cssStyles[camelKey] = 'fit-content';
243
+ } else if (key === 'height' && value === 'fill') {
244
+ cssStyles[camelKey] = '100%';
245
+ } else if (key === 'height' && value === 'fit') {
246
+ cssStyles[camelKey] = 'fit-content';
247
+ } else if (typeof value === 'string') {
248
+ const resolved = resolveThemeCssVariables(value, themeSettings);
249
+
250
+ if (normalizedKey === 'color' && textAlpha !== null) {
251
+ const rgba = colorToRgba(resolved, textAlpha);
252
+ if (rgba) {
253
+ cssStyles[camelKey] = rgba;
254
+ return;
255
+ }
256
+ }
257
+
258
+ if (normalizedKey === 'background-color' && backgroundAlpha !== null) {
259
+ if (backgroundAlpha === 0) {
260
+ cssStyles[camelKey] = 'transparent';
261
+ return;
262
+ }
263
+
264
+ const rgba = colorToRgba(resolved, backgroundAlpha);
265
+ if (rgba) {
266
+ cssStyles[camelKey] = rgba;
267
+ return;
268
+ }
269
+ }
270
+
271
+ cssStyles[camelKey] = resolved;
272
+ } else {
273
+ cssStyles[camelKey] = value as number;
274
+ }
275
+ });
276
+
277
+ return cssStyles as React.CSSProperties;
278
+ };
279
+
280
+ interface ResponsiveStyles {
281
+ [key: string]:
282
+ | {
283
+ desktop?: string | number;
284
+ mobile?: string | number;
285
+ }
286
+ | string
287
+ | number;
288
+ }
289
+
290
+ interface Block {
291
+ id: string;
292
+ styles?: ResponsiveStyles;
293
+ blocks?: Block[];
294
+ }
295
+
296
+ interface Section {
297
+ id: string;
298
+ styles?: ResponsiveStyles;
299
+ blocks?: Block[];
300
+ }
301
+
302
+ const camelToKebab = (str: string): string => {
303
+ return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
304
+ };
305
+
306
+ const formatCSSValue = (property: string, value: string | number): string => {
307
+ if (value === undefined || value === null || value === '') return '';
308
+
309
+ const kebabProperty = camelToKebab(property);
310
+
311
+ if (kebabProperty === 'width' || kebabProperty === 'height') {
312
+ if (value === 'fill') return '100%';
313
+ if (value === 'fit') return 'fit-content';
314
+ }
315
+
316
+ const needsUnit = [
317
+ 'width',
318
+ 'height',
319
+ 'max-width',
320
+ 'max-height',
321
+ 'min-width',
322
+ 'min-height',
323
+ 'padding',
324
+ 'padding-top',
325
+ 'padding-right',
326
+ 'padding-bottom',
327
+ 'padding-left',
328
+ 'margin',
329
+ 'margin-top',
330
+ 'margin-right',
331
+ 'margin-bottom',
332
+ 'margin-left',
333
+ 'gap',
334
+ 'row-gap',
335
+ 'column-gap',
336
+ 'top',
337
+ 'right',
338
+ 'bottom',
339
+ 'left',
340
+ 'font-size',
341
+ 'line-height',
342
+ 'letter-spacing',
343
+ 'border-width',
344
+ 'border-radius',
345
+ 'border-top-left-radius',
346
+ 'border-top-right-radius',
347
+ 'border-bottom-left-radius',
348
+ 'border-bottom-right-radius'
349
+ ];
350
+
351
+ if (typeof value === 'number' && needsUnit.includes(kebabProperty)) {
352
+ return `${value}px`;
353
+ }
354
+
355
+ return String(value);
356
+ };
357
+
358
+ const generateElementCSS = (
359
+ selector: string,
360
+ styles: ResponsiveStyles,
361
+ targetBreakpoint?: string
362
+ ): string => {
363
+ if (!styles || Object.keys(styles).length === 0) return '';
364
+
365
+ const getStyleEntry = (kebabKey: string): unknown => {
366
+ const camelKey = kebabToCamel(kebabKey);
367
+ return styles[kebabKey] ?? styles[camelKey];
368
+ };
369
+
370
+ const getBackgroundAlpha = (breakpoint: string): number | null => {
371
+ const entry = getStyleEntry('background-opacity');
372
+ const value = entry ? getResponsiveValue(entry, breakpoint, '100') : '100';
373
+ return clampAlpha(value, 100);
374
+ };
375
+
376
+ const getHoverAlpha = (breakpoint: string): number | null => {
377
+ const entry = getStyleEntry('hover-opacity');
378
+ const value = entry ? getResponsiveValue(entry, breakpoint, '100') : '100';
379
+ return clampAlpha(value, 100);
380
+ };
381
+
382
+ const getHoverColor = (breakpoint: string): string | null => {
383
+ const entry = getStyleEntry('hover-color');
384
+ const value = entry
385
+ ? getResponsiveValue(entry, breakpoint, 'var(--theme-secondary)')
386
+ : undefined;
387
+ if (value === undefined || value === null || value === '') return null;
388
+ return String(value);
389
+ };
390
+
391
+ const getHoverFlex = (breakpoint: string): string | null => {
392
+ const entry = getStyleEntry('hover-flex');
393
+ const value = entry
394
+ ? getResponsiveValue(entry, breakpoint, undefined)
395
+ : undefined;
396
+ if (value === undefined || value === null || value === '') return null;
397
+ return String(value);
398
+ };
399
+
400
+ const getHoverChildFlex = (breakpoint: string): string | null => {
401
+ const entry = getStyleEntry('hover-child-flex');
402
+ const value = entry
403
+ ? getResponsiveValue(entry, breakpoint, undefined)
404
+ : undefined;
405
+ if (value === undefined || value === null || value === '') return null;
406
+ return String(value);
407
+ };
408
+
409
+ const buildHoverDeclarations = (breakpoint: string): string[] => {
410
+ const declarations: string[] = [];
411
+
412
+ const hoverColor = getHoverColor(breakpoint);
413
+ if (hoverColor) {
414
+ const alpha = getHoverAlpha(breakpoint);
415
+ if (alpha === null) {
416
+ declarations.push(` background-color: ${hoverColor}`);
417
+ declarations.push(` border-color: ${hoverColor}`);
418
+ } else {
419
+ const resolved = applyAlphaToColor(hoverColor, alpha);
420
+ declarations.push(` background-color: ${resolved}`);
421
+ declarations.push(` border-color: ${resolved}`);
422
+ }
423
+ }
424
+
425
+ const hoverFlex = getHoverFlex(breakpoint);
426
+ if (hoverFlex) {
427
+ declarations.push(` flex: ${hoverFlex}`);
428
+ }
429
+
430
+ return declarations;
431
+ };
432
+
433
+ const buildHoverCss = (breakpoint: string, indent = ''): string => {
434
+ let css = '';
435
+
436
+ const hoverChildFlex = getHoverChildFlex(breakpoint);
437
+ if (hoverChildFlex) {
438
+ css += `${indent}${selector}:hover > [data-block-id] {\n${indent} flex: ${hoverChildFlex} !important;\n${indent}}\n`;
439
+ }
440
+
441
+ const hoverDecls = buildHoverDeclarations(breakpoint);
442
+ if (hoverDecls.length === 0) return css;
443
+
444
+ const joined = hoverDecls
445
+ .map((line) => `${indent}${line.trimStart()}`)
446
+ .join(`;\n${indent}`);
447
+
448
+ const importantJoined = hoverDecls
449
+ .map((line) => {
450
+ const trimmed = line.trimStart();
451
+ return `${indent}${trimmed.replace(/:\s*(.+)$/, ': $1 !important')}`;
452
+ })
453
+ .join(`;\n${indent}`);
454
+
455
+ const wrapperRule = `${indent}${selector}:hover {\n${importantJoined};\n${indent}}\n`;
456
+ // Scope to the last child wrapper (button block content) so designer overlay controls aren't affected.
457
+ const childRule = `${indent}${selector}:hover > div:last-child a,\n${indent}${selector}:hover > div:last-child button {\n${importantJoined};\n${indent}}\n`;
458
+
459
+ css += `${wrapperRule}${childRule}`;
460
+ return css;
461
+ };
462
+
463
+ const cssStyles: string[] = [];
464
+
465
+ Object.entries(styles).forEach(([property, value]) => {
466
+ if (value === undefined || value === null) return;
467
+
468
+ const cssProperty = camelToKebab(property);
469
+
470
+ // Skip non-CSS helper keys.
471
+ if (
472
+ cssProperty === 'text-opacity' ||
473
+ cssProperty === 'background-opacity' ||
474
+ cssProperty === 'hover-opacity' ||
475
+ cssProperty === 'hover-color' ||
476
+ cssProperty === 'hover-flex' ||
477
+ cssProperty === 'hover-child-flex' ||
478
+ cssProperty === 'opacity'
479
+ ) {
480
+ return;
481
+ }
482
+
483
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
484
+ const responsiveValue = value as {
485
+ desktop?: string | number;
486
+ mobile?: string | number;
487
+ };
488
+
489
+ if (targetBreakpoint) {
490
+ const breakpointValue =
491
+ responsiveValue[targetBreakpoint as keyof typeof responsiveValue] ??
492
+ responsiveValue.desktop;
493
+ if (
494
+ breakpointValue !== undefined &&
495
+ breakpointValue !== null &&
496
+ breakpointValue !== ''
497
+ ) {
498
+ if (
499
+ cssProperty === 'background-color' &&
500
+ typeof breakpointValue === 'string'
501
+ ) {
502
+ const backgroundAlpha = getBackgroundAlpha(
503
+ targetBreakpoint === 'mobile' ? 'mobile' : 'desktop'
504
+ );
505
+ if (backgroundAlpha !== null) {
506
+ if (backgroundAlpha === 0) {
507
+ cssStyles.push(` ${cssProperty}: transparent`);
508
+ return;
509
+ }
510
+ const rgba = colorToRgba(breakpointValue, backgroundAlpha);
511
+ if (rgba) {
512
+ cssStyles.push(` ${cssProperty}: ${rgba}`);
513
+ return;
514
+ }
515
+ }
516
+ }
517
+
518
+ const formattedValue = formatCSSValue(property, breakpointValue);
519
+ if (formattedValue) {
520
+ cssStyles.push(` ${cssProperty}: ${formattedValue}`);
521
+ }
522
+ }
523
+ } else {
524
+ if (
525
+ responsiveValue.desktop !== undefined &&
526
+ responsiveValue.desktop !== null &&
527
+ responsiveValue.desktop !== ''
528
+ ) {
529
+ if (
530
+ cssProperty === 'background-color' &&
531
+ typeof responsiveValue.desktop === 'string'
532
+ ) {
533
+ const backgroundAlpha = getBackgroundAlpha('desktop');
534
+ if (backgroundAlpha !== null) {
535
+ if (backgroundAlpha === 0) {
536
+ cssStyles.push(` ${cssProperty}: transparent`);
537
+ return;
538
+ }
539
+ const rgba = colorToRgba(
540
+ responsiveValue.desktop,
541
+ backgroundAlpha
542
+ );
543
+ if (rgba) {
544
+ cssStyles.push(` ${cssProperty}: ${rgba}`);
545
+ return;
546
+ }
547
+ }
548
+ }
549
+
550
+ const formattedValue = formatCSSValue(
551
+ property,
552
+ responsiveValue.desktop
553
+ );
554
+ if (formattedValue) {
555
+ cssStyles.push(` ${cssProperty}: ${formattedValue}`);
556
+ }
557
+ }
558
+ }
559
+ } else {
560
+ if (cssProperty === 'background-color' && typeof value === 'string') {
561
+ const backgroundAlpha = getBackgroundAlpha('desktop');
562
+ if (backgroundAlpha !== null) {
563
+ if (backgroundAlpha === 0) {
564
+ cssStyles.push(` ${cssProperty}: transparent`);
565
+ return;
566
+ }
567
+ const rgba = colorToRgba(value, backgroundAlpha);
568
+ if (rgba) {
569
+ cssStyles.push(` ${cssProperty}: ${rgba}`);
570
+ return;
571
+ }
572
+ }
573
+ }
574
+
575
+ const formattedValue = formatCSSValue(property, value as string | number);
576
+ if (formattedValue) {
577
+ cssStyles.push(` ${cssProperty}: ${formattedValue}`);
578
+ }
579
+ }
580
+ });
581
+
582
+ if (targetBreakpoint) {
583
+ const breakpointKey = targetBreakpoint === 'mobile' ? 'mobile' : 'desktop';
584
+ const hoverCss = buildHoverCss(breakpointKey);
585
+
586
+ if (cssStyles.length > 0) {
587
+ return `${selector} {\n${cssStyles.join(';\n')};\n}\n${hoverCss}`;
588
+ }
589
+
590
+ if (hoverCss) {
591
+ return hoverCss;
592
+ }
593
+
594
+ return '';
595
+ }
596
+
597
+ const desktopStyles: string[] = [];
598
+ const mobileStyles: string[] = [];
599
+
600
+ Object.entries(styles).forEach(([property, value]) => {
601
+ if (value === undefined || value === null) return;
602
+
603
+ const cssProperty = camelToKebab(property);
604
+
605
+ // Skip non-CSS helper keys.
606
+ if (
607
+ cssProperty === 'text-opacity' ||
608
+ cssProperty === 'background-opacity' ||
609
+ cssProperty === 'hover-opacity' ||
610
+ cssProperty === 'hover-color' ||
611
+ cssProperty === 'hover-flex' ||
612
+ cssProperty === 'hover-child-flex' ||
613
+ cssProperty === 'opacity'
614
+ ) {
615
+ return;
616
+ }
617
+
618
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
619
+ const responsiveValue = value as {
620
+ desktop?: string | number;
621
+ mobile?: string | number;
622
+ };
623
+
624
+ if (
625
+ responsiveValue.desktop !== undefined &&
626
+ responsiveValue.desktop !== null &&
627
+ responsiveValue.desktop !== ''
628
+ ) {
629
+ if (
630
+ cssProperty === 'background-color' &&
631
+ typeof responsiveValue.desktop === 'string'
632
+ ) {
633
+ const backgroundAlpha = getBackgroundAlpha('desktop');
634
+ if (backgroundAlpha !== null) {
635
+ if (backgroundAlpha === 0) {
636
+ desktopStyles.push(` ${cssProperty}: transparent`);
637
+ } else {
638
+ const rgba = colorToRgba(
639
+ responsiveValue.desktop,
640
+ backgroundAlpha
641
+ );
642
+ desktopStyles.push(
643
+ ` ${cssProperty}: ${rgba || responsiveValue.desktop}`
644
+ );
645
+ }
646
+ } else {
647
+ const formattedValue = formatCSSValue(
648
+ property,
649
+ responsiveValue.desktop
650
+ );
651
+ if (formattedValue) {
652
+ desktopStyles.push(` ${cssProperty}: ${formattedValue}`);
653
+ }
654
+ }
655
+ } else {
656
+ const formattedValue = formatCSSValue(
657
+ property,
658
+ responsiveValue.desktop
659
+ );
660
+ if (formattedValue) {
661
+ desktopStyles.push(` ${cssProperty}: ${formattedValue}`);
662
+ }
663
+ }
664
+ }
665
+
666
+ if (
667
+ responsiveValue.mobile !== undefined &&
668
+ responsiveValue.mobile !== null &&
669
+ responsiveValue.mobile !== ''
670
+ ) {
671
+ if (
672
+ cssProperty === 'background-color' &&
673
+ typeof responsiveValue.mobile === 'string'
674
+ ) {
675
+ const backgroundAlpha = getBackgroundAlpha('mobile');
676
+ if (backgroundAlpha !== null) {
677
+ if (backgroundAlpha === 0) {
678
+ mobileStyles.push(` ${cssProperty}: transparent`);
679
+ } else {
680
+ const rgba = colorToRgba(responsiveValue.mobile, backgroundAlpha);
681
+ mobileStyles.push(
682
+ ` ${cssProperty}: ${rgba || responsiveValue.mobile}`
683
+ );
684
+ }
685
+ } else {
686
+ const formattedMobile = formatCSSValue(
687
+ property,
688
+ responsiveValue.mobile
689
+ );
690
+ const formattedDesktop =
691
+ responsiveValue.desktop !== undefined
692
+ ? formatCSSValue(property, responsiveValue.desktop)
693
+ : '';
694
+
695
+ if (formattedMobile && formattedMobile !== formattedDesktop) {
696
+ mobileStyles.push(` ${cssProperty}: ${formattedMobile}`);
697
+ }
698
+ }
699
+ } else {
700
+ const formattedMobile = formatCSSValue(
701
+ property,
702
+ responsiveValue.mobile
703
+ );
704
+ const formattedDesktop =
705
+ responsiveValue.desktop !== undefined
706
+ ? formatCSSValue(property, responsiveValue.desktop)
707
+ : '';
708
+
709
+ if (formattedMobile && formattedMobile !== formattedDesktop) {
710
+ mobileStyles.push(` ${cssProperty}: ${formattedMobile}`);
711
+ }
712
+ }
713
+ }
714
+ } else {
715
+ if (cssProperty === 'background-color' && typeof value === 'string') {
716
+ const backgroundAlpha = getBackgroundAlpha('desktop');
717
+ if (backgroundAlpha !== null) {
718
+ if (backgroundAlpha === 0) {
719
+ desktopStyles.push(` ${cssProperty}: transparent`);
720
+ } else {
721
+ const rgba = colorToRgba(value, backgroundAlpha);
722
+ desktopStyles.push(` ${cssProperty}: ${rgba || value}`);
723
+ }
724
+ return;
725
+ }
726
+ }
727
+
728
+ const formattedValue = formatCSSValue(property, value as string | number);
729
+ if (formattedValue) {
730
+ desktopStyles.push(` ${cssProperty}: ${formattedValue}`);
731
+ }
732
+ }
733
+ });
734
+
735
+ let css = '';
736
+
737
+ if (desktopStyles.length > 0) {
738
+ css += `${selector} {\n${desktopStyles.join(';\n')};\n}\n`;
739
+ }
740
+
741
+ css += buildHoverCss('desktop');
742
+
743
+ if (mobileStyles.length > 0) {
744
+ const mobileHoverCss = buildHoverCss('mobile', ' ');
745
+
746
+ css += `@media (max-width: 1023px) {\n ${selector} {\n ${mobileStyles.join(
747
+ ';\n '
748
+ )};\n }\n${mobileHoverCss}}\n`;
749
+ }
750
+
751
+ return css;
752
+ };
753
+
754
+ const collectBlockCSS = (
755
+ blocks: Block[],
756
+ targetBreakpoint?: string
757
+ ): string => {
758
+ let css = '';
759
+
760
+ blocks.forEach((block) => {
761
+ if (block.styles && Object.keys(block.styles).length > 0) {
762
+ const selector = `[data-block-id="${block.id}"]`;
763
+ css += generateElementCSS(selector, block.styles, targetBreakpoint);
764
+ }
765
+
766
+ if (block.blocks && block.blocks.length > 0) {
767
+ css += collectBlockCSS(block.blocks, targetBreakpoint);
768
+ }
769
+ });
770
+
771
+ return css;
772
+ };
773
+
774
+ export const generateThemeCSS = (
775
+ sections: Section[],
776
+ targetBreakpoint?: string
777
+ ): string => {
778
+ let css = '';
779
+
780
+ sections.forEach((section) => {
781
+ if (section.styles && Object.keys(section.styles).length > 0) {
782
+ const selector = `[data-section-id="${section.id}"]`;
783
+ css += generateElementCSS(selector, section.styles, targetBreakpoint);
784
+ }
785
+
786
+ if (section.blocks && section.blocks.length > 0) {
787
+ css += collectBlockCSS(section.blocks, targetBreakpoint);
788
+ }
789
+ });
790
+
791
+ return css;
792
+ };