@akinon/pz-theme 2.0.36-rc.0 → 2.0.36
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 +8 -31
- package/jest/__mocks__/hookform-resolvers.js +5 -0
- package/jest/__mocks__/next-auth-react.js +5 -0
- package/jest.config.js +3 -0
- package/package.json +4 -4
- package/src/blocks/accordion-block.tsx +35 -3
- package/src/blocks/commerce-format.ts +220 -0
- package/src/blocks/icon-block.tsx +6 -1
- package/src/blocks/image-gallery-block.tsx +1 -1
- package/src/blocks/link-block.tsx +9 -1
- package/src/blocks/slider-block.tsx +2 -1
- package/src/blocks/tab-block.tsx +15 -2
- package/src/blocks/video-block.tsx +6 -0
- package/src/components/__tests__/part-state-preview-scope.test.tsx +148 -0
- package/src/components/action-toolbar.tsx +182 -3
- package/src/components/part-state-preview.tsx +92 -0
- package/src/hooks/__tests__/use-overlay.test.tsx +503 -0
- package/src/hooks/use-designer-features.tsx +16 -1
- package/src/hooks/use-overlay.ts +538 -0
- package/src/hooks/use-visibility-context.ts +14 -2
- package/src/sections/__tests__/divider-section.test.tsx +28 -1
- package/src/sections/contact-form-section.tsx +4 -1
- package/src/sections/coupon-banner-section.tsx +2 -1
- package/src/sections/featured-product-spotlight-section.tsx +4 -4
- package/src/sections/find-in-store-section.tsx +2 -1
- package/src/sections/hover-showcase-section.tsx +2 -1
- package/src/sections/installment-options-section.tsx +2 -1
- package/src/sections/order-tracking-lookup-section.tsx +6 -1
- package/src/sections/posts-slider-section.tsx +8 -3
- package/src/sections/pre-order-launch-banner-section.tsx +2 -1
- package/src/sections/tabs-section.tsx +7 -13
- package/src/theme-block.tsx +81 -16
- package/src/theme-placeholder-client.tsx +8 -1
- package/src/theme-placeholder-wrapper.tsx +17 -3
- package/src/theme-placeholder.tsx +51 -5
- package/src/theme-section.tsx +20 -4
- package/src/utils/__tests__/breakpoint-cascade.test.ts +351 -0
- package/src/utils/__tests__/group-box-chrome.test.ts +83 -0
- package/src/utils/__tests__/iterator-bindings.test.tsx +1178 -0
- package/src/utils/__tests__/part-ownership-guard.test.tsx +298 -0
- package/src/utils/breakpoints.ts +116 -0
- package/src/utils/custom-css.ts +233 -0
- package/src/utils/index.ts +353 -42
- package/src/utils/iterator-utils.ts +675 -62
- package/src/utils/page-context-discovery.test.ts +188 -0
- package/src/utils/page-context-discovery.ts +48 -4
- package/src/utils/part-styles.ts +229 -0
- package/src/utils/row-visibility.ts +273 -0
- package/src/utils/visibility-rules.test.ts +579 -0
- package/src/utils/visibility-rules.ts +346 -3
package/src/utils/index.ts
CHANGED
|
@@ -5,8 +5,19 @@ import {
|
|
|
5
5
|
neutralizeStyleTagBreakout,
|
|
6
6
|
escapeCssAttributeValue
|
|
7
7
|
} from './css-safety';
|
|
8
|
+
import { buildPartSelectors, splitPartStyles } from './part-styles';
|
|
9
|
+
import { buildCustomCss } from './custom-css';
|
|
10
|
+
import { hasBindingToken } from './iterator-utils';
|
|
11
|
+
import {
|
|
12
|
+
BREAKPOINT_FALLBACKS,
|
|
13
|
+
MOBILE_MEDIA_QUERY,
|
|
14
|
+
TABLET_MEDIA_QUERY
|
|
15
|
+
} from './breakpoints';
|
|
8
16
|
|
|
9
17
|
export * from './publish-window';
|
|
18
|
+
export * from './part-styles';
|
|
19
|
+
export * from './custom-css';
|
|
20
|
+
export * from './breakpoints';
|
|
10
21
|
|
|
11
22
|
export type ImageLoadingStrategy = 'eager' | 'lazy';
|
|
12
23
|
export type ImageFetchPriority = 'high' | 'auto' | 'low';
|
|
@@ -196,6 +207,20 @@ export const getResponsiveValue = (
|
|
|
196
207
|
const responsiveValue = value as Record<string, unknown>;
|
|
197
208
|
const matched = responsiveValue[breakpoint];
|
|
198
209
|
if (matched !== undefined) return matched;
|
|
210
|
+
|
|
211
|
+
// Nothing authored here, so fall through the way the emitted cascade does:
|
|
212
|
+
// a tablet reads the mobile value before it reads the desktop one, because
|
|
213
|
+
// the mobile rule is the one that covers 768-1023 when no tablet rule was
|
|
214
|
+
// emitted. `desktop` and `mobile` have no intermediate step, so this loop
|
|
215
|
+
// does not run for them and they resolve exactly as they always have.
|
|
216
|
+
const chain = BREAKPOINT_FALLBACKS[breakpoint];
|
|
217
|
+
if (chain) {
|
|
218
|
+
for (const next of chain) {
|
|
219
|
+
const inherited = responsiveValue[next];
|
|
220
|
+
if (inherited !== undefined) return inherited;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
199
224
|
if (responsiveValue.desktop !== undefined) return responsiveValue.desktop;
|
|
200
225
|
return fallback;
|
|
201
226
|
}
|
|
@@ -251,13 +276,59 @@ const kebabToCamel = (str: string): string => {
|
|
|
251
276
|
return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
252
277
|
};
|
|
253
278
|
|
|
279
|
+
/**
|
|
280
|
+
* A style value carrying a binding token (`{{item.badgeColor}}`) is an
|
|
281
|
+
* instruction the iterator resolves per row, not a CSS value. It is removed
|
|
282
|
+
* before emission for two reasons: a token that reached the stylesheet would
|
|
283
|
+
* write a declaration the browser throws away (so an unresolved binding must
|
|
284
|
+
* leave the property unset, not half-written), and nothing resembling a token
|
|
285
|
+
* should ever be interpolated into a `<style>` element.
|
|
286
|
+
*
|
|
287
|
+
* Styles with no token are returned by reference, so every composition that
|
|
288
|
+
* renders today produces byte-identical CSS.
|
|
289
|
+
*/
|
|
290
|
+
const stripBoundStyleValues = <T extends Record<string, unknown>>(
|
|
291
|
+
styles: T
|
|
292
|
+
): T => {
|
|
293
|
+
let changed = false;
|
|
294
|
+
const stripped: Record<string, unknown> = {};
|
|
295
|
+
|
|
296
|
+
Object.entries(styles).forEach(([key, value]) => {
|
|
297
|
+
if (hasBindingToken(value)) {
|
|
298
|
+
changed = true;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
303
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
304
|
+
if (
|
|
305
|
+
entries.some(([, breakpointValue]) => hasBindingToken(breakpointValue))
|
|
306
|
+
) {
|
|
307
|
+
changed = true;
|
|
308
|
+
const kept = entries.filter(
|
|
309
|
+
([, breakpointValue]) => !hasBindingToken(breakpointValue)
|
|
310
|
+
);
|
|
311
|
+
if (kept.length > 0) {
|
|
312
|
+
stripped[key] = Object.fromEntries(kept);
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
stripped[key] = value;
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return changed ? (stripped as T) : styles;
|
|
322
|
+
};
|
|
323
|
+
|
|
254
324
|
export const getCSSStyles = (
|
|
255
|
-
|
|
325
|
+
rawStyles?: Record<string, unknown>,
|
|
256
326
|
themeSettings?: ThemeSettings,
|
|
257
327
|
breakpoint: string = 'desktop'
|
|
258
328
|
): React.CSSProperties => {
|
|
259
|
-
if (!
|
|
329
|
+
if (!rawStyles) return {};
|
|
260
330
|
|
|
331
|
+
const styles = stripBoundStyleValues(rawStyles);
|
|
261
332
|
const cssStyles: Record<string, string | number> = {};
|
|
262
333
|
|
|
263
334
|
const normalizeKey = (key: string): string => {
|
|
@@ -377,22 +448,62 @@ interface ResponsiveStyles {
|
|
|
377
448
|
[key: string]:
|
|
378
449
|
| {
|
|
379
450
|
desktop?: string | number;
|
|
451
|
+
tablet?: string | number;
|
|
380
452
|
mobile?: string | number;
|
|
381
453
|
}
|
|
382
454
|
| string
|
|
383
455
|
| number;
|
|
384
456
|
}
|
|
385
457
|
|
|
458
|
+
type ResponsiveEntry = {
|
|
459
|
+
desktop?: string | number;
|
|
460
|
+
tablet?: string | number;
|
|
461
|
+
mobile?: string | number;
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
/** Authored at all, as the generator has always judged it. */
|
|
465
|
+
const isAuthored = (value: unknown): value is string | number =>
|
|
466
|
+
value !== undefined && value !== null && value !== '';
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* The value a breakpoint resolves to inside a responsive entry, following the
|
|
470
|
+
* same chain as `getResponsiveValue` — so the stylesheet and the value readers
|
|
471
|
+
* cannot disagree about what "nothing authored here" means.
|
|
472
|
+
*/
|
|
473
|
+
const resolveEntryValue = (
|
|
474
|
+
entry: ResponsiveEntry,
|
|
475
|
+
breakpoint: string
|
|
476
|
+
): string | number | undefined => {
|
|
477
|
+
const own = entry[breakpoint as keyof ResponsiveEntry];
|
|
478
|
+
if (own !== undefined) return own;
|
|
479
|
+
|
|
480
|
+
const chain = BREAKPOINT_FALLBACKS[breakpoint];
|
|
481
|
+
if (chain) {
|
|
482
|
+
for (const next of chain) {
|
|
483
|
+
const inherited = entry[next as keyof ResponsiveEntry];
|
|
484
|
+
if (inherited !== undefined) return inherited;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return entry.desktop;
|
|
489
|
+
};
|
|
490
|
+
|
|
386
491
|
interface Block {
|
|
387
492
|
id: string;
|
|
388
493
|
styleSourceId?: string;
|
|
389
494
|
styles?: ResponsiveStyles;
|
|
495
|
+
/** Set by blocks that name their real root with `data-part="root"`. */
|
|
496
|
+
stylesTargetRoot?: boolean;
|
|
497
|
+
// Only read for the scoped custom-CSS escape hatch; the rest of a block's
|
|
498
|
+
// properties never reach the stylesheet.
|
|
499
|
+
properties?: Record<string, unknown>;
|
|
390
500
|
blocks?: Block[];
|
|
391
501
|
}
|
|
392
502
|
|
|
393
503
|
interface Section {
|
|
394
504
|
id: string;
|
|
395
505
|
styles?: ResponsiveStyles;
|
|
506
|
+
properties?: Record<string, unknown>;
|
|
396
507
|
blocks?: Block[];
|
|
397
508
|
}
|
|
398
509
|
|
|
@@ -472,10 +583,21 @@ const formatCSSValue = (property: string, value: string | number): string => {
|
|
|
472
583
|
|
|
473
584
|
const generateElementCSS = (
|
|
474
585
|
selector: string,
|
|
475
|
-
|
|
476
|
-
targetBreakpoint?: string
|
|
586
|
+
rawStyles: ResponsiveStyles,
|
|
587
|
+
targetBreakpoint?: string,
|
|
588
|
+
options?: { treatHelperKeysAsCss?: boolean }
|
|
477
589
|
): string => {
|
|
478
|
-
if (!
|
|
590
|
+
if (!rawStyles || Object.keys(rawStyles).length === 0) return '';
|
|
591
|
+
|
|
592
|
+
// Bound values belong to the per-row resolution, not to the shared rule.
|
|
593
|
+
const styles = stripBoundStyleValues(
|
|
594
|
+
rawStyles as Record<string, unknown>
|
|
595
|
+
) as ResponsiveStyles;
|
|
596
|
+
|
|
597
|
+
// `opacity` and friends are helper keys on a block root, feeding the
|
|
598
|
+
// hover/background-alpha maths rather than being emitted directly. A named
|
|
599
|
+
// part has no such helper, so there they mean the CSS property.
|
|
600
|
+
const helperKeysAreCss = options?.treatHelperKeysAsCss === true;
|
|
479
601
|
|
|
480
602
|
const getStyleEntry = (kebabKey: string): unknown => {
|
|
481
603
|
const camelKey = kebabToCamel(kebabKey);
|
|
@@ -580,27 +702,33 @@ const generateElementCSS = (
|
|
|
580
702
|
|
|
581
703
|
// Skip non-CSS helper keys.
|
|
582
704
|
if (
|
|
583
|
-
|
|
584
|
-
cssProperty === '
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
705
|
+
!helperKeysAreCss &&
|
|
706
|
+
(cssProperty === 'text-opacity' ||
|
|
707
|
+
cssProperty === 'background-opacity' ||
|
|
708
|
+
cssProperty === 'hover-opacity' ||
|
|
709
|
+
cssProperty === 'hover-color' ||
|
|
710
|
+
cssProperty === 'hover-flex' ||
|
|
711
|
+
cssProperty === 'hover-child-flex' ||
|
|
712
|
+
cssProperty === 'opacity')
|
|
590
713
|
) {
|
|
591
714
|
return;
|
|
592
715
|
}
|
|
593
716
|
|
|
594
717
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
718
|
+
// `ResponsiveEntry`, not an inline copy of it: the copies predated the
|
|
719
|
+
// tablet breakpoint and silently lacked it, so reading `.tablet` here
|
|
720
|
+
// was a type error only the app build could see.
|
|
721
|
+
const responsiveValue = value as ResponsiveEntry;
|
|
599
722
|
|
|
600
723
|
if (targetBreakpoint) {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
724
|
+
// The canvas shows one viewport, so it gets the value that viewport
|
|
725
|
+
// resolves to — including the tablet -> mobile -> desktop fall-through.
|
|
726
|
+
// Reading only `[breakpoint] ?? desktop` here is what would make the
|
|
727
|
+
// canvas show a tablet the device never renders.
|
|
728
|
+
const breakpointValue = resolveEntryValue(
|
|
729
|
+
responsiveValue,
|
|
730
|
+
targetBreakpoint
|
|
731
|
+
);
|
|
604
732
|
if (
|
|
605
733
|
breakpointValue !== undefined &&
|
|
606
734
|
breakpointValue !== null &&
|
|
@@ -610,9 +738,7 @@ const generateElementCSS = (
|
|
|
610
738
|
cssProperty === 'background-color' &&
|
|
611
739
|
typeof breakpointValue === 'string'
|
|
612
740
|
) {
|
|
613
|
-
const backgroundAlpha = getBackgroundAlpha(
|
|
614
|
-
targetBreakpoint === 'mobile' ? 'mobile' : 'desktop'
|
|
615
|
-
);
|
|
741
|
+
const backgroundAlpha = getBackgroundAlpha(targetBreakpoint);
|
|
616
742
|
if (backgroundAlpha !== null) {
|
|
617
743
|
if (backgroundAlpha === 0) {
|
|
618
744
|
cssStyles.push(` ${cssProperty}: transparent`);
|
|
@@ -691,8 +817,11 @@ const generateElementCSS = (
|
|
|
691
817
|
});
|
|
692
818
|
|
|
693
819
|
if (targetBreakpoint) {
|
|
694
|
-
|
|
695
|
-
|
|
820
|
+
// Passed straight through rather than folded to desktop/mobile: the hover
|
|
821
|
+
// helpers read through `getResponsiveValue`, which already knows where a
|
|
822
|
+
// breakpoint looks when it has nothing of its own. For 'desktop' and
|
|
823
|
+
// 'mobile' this resolves to what the fold produced.
|
|
824
|
+
const hoverCss = buildHoverCss(targetBreakpoint);
|
|
696
825
|
|
|
697
826
|
if (cssStyles.length > 0) {
|
|
698
827
|
return `${selector} {\n${cssStyles.join(';\n')};\n}\n${hoverCss}`;
|
|
@@ -707,6 +836,32 @@ const generateElementCSS = (
|
|
|
707
836
|
|
|
708
837
|
const desktopStyles: string[] = [];
|
|
709
838
|
const mobileStyles: string[] = [];
|
|
839
|
+
const tabletStyles: string[] = [];
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* The declaration value one breakpoint would produce for one property, by
|
|
843
|
+
* the same rules the desktop and mobile arms below apply inline. Only the
|
|
844
|
+
* tablet arm calls it, so the two older arms keep emitting the bytes they
|
|
845
|
+
* always emitted; the tablet arm uses it twice — once for its own value and
|
|
846
|
+
* once for the value it would inherit — to decide whether a rule is worth
|
|
847
|
+
* writing at all.
|
|
848
|
+
*/
|
|
849
|
+
const declarationValue = (
|
|
850
|
+
property: string,
|
|
851
|
+
cssProperty: string,
|
|
852
|
+
breakpoint: string,
|
|
853
|
+
raw: string | number
|
|
854
|
+
): string => {
|
|
855
|
+
if (cssProperty === 'background-color' && typeof raw === 'string') {
|
|
856
|
+
const backgroundAlpha = getBackgroundAlpha(breakpoint);
|
|
857
|
+
if (backgroundAlpha !== null) {
|
|
858
|
+
if (backgroundAlpha === 0) return 'transparent';
|
|
859
|
+
return colorToRgba(raw, backgroundAlpha) || raw;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
return formatCSSValue(property, raw);
|
|
864
|
+
};
|
|
710
865
|
|
|
711
866
|
Object.entries(styles).forEach(([property, value]) => {
|
|
712
867
|
if (value === undefined || value === null) return;
|
|
@@ -715,22 +870,23 @@ const generateElementCSS = (
|
|
|
715
870
|
|
|
716
871
|
// Skip non-CSS helper keys.
|
|
717
872
|
if (
|
|
718
|
-
|
|
719
|
-
cssProperty === '
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
873
|
+
!helperKeysAreCss &&
|
|
874
|
+
(cssProperty === 'text-opacity' ||
|
|
875
|
+
cssProperty === 'background-opacity' ||
|
|
876
|
+
cssProperty === 'hover-opacity' ||
|
|
877
|
+
cssProperty === 'hover-color' ||
|
|
878
|
+
cssProperty === 'hover-flex' ||
|
|
879
|
+
cssProperty === 'hover-child-flex' ||
|
|
880
|
+
cssProperty === 'opacity')
|
|
725
881
|
) {
|
|
726
882
|
return;
|
|
727
883
|
}
|
|
728
884
|
|
|
729
885
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
886
|
+
// `ResponsiveEntry`, not an inline copy of it: the copies predated the
|
|
887
|
+
// tablet breakpoint and silently lacked it, so reading `.tablet` here
|
|
888
|
+
// was a type error only the app build could see.
|
|
889
|
+
const responsiveValue = value as ResponsiveEntry;
|
|
734
890
|
|
|
735
891
|
if (
|
|
736
892
|
responsiveValue.desktop !== undefined &&
|
|
@@ -822,6 +978,40 @@ const generateElementCSS = (
|
|
|
822
978
|
}
|
|
823
979
|
}
|
|
824
980
|
}
|
|
981
|
+
|
|
982
|
+
// Tablet, last, and only when something is authored here. That guard is
|
|
983
|
+
// what makes every composition published before this breakpoint existed
|
|
984
|
+
// emit the exact bytes it emitted before: with no tablet key there is no
|
|
985
|
+
// tablet declaration, so there is no tablet rule.
|
|
986
|
+
if (isAuthored(responsiveValue.tablet)) {
|
|
987
|
+
const formattedTablet = declarationValue(
|
|
988
|
+
property,
|
|
989
|
+
cssProperty,
|
|
990
|
+
'tablet',
|
|
991
|
+
responsiveValue.tablet
|
|
992
|
+
);
|
|
993
|
+
|
|
994
|
+
// What 768-1023 would show without this rule: the mobile declaration
|
|
995
|
+
// if one was authored (its query covers the band), otherwise desktop's.
|
|
996
|
+
// Matching it means the rule would change nothing, so it is not
|
|
997
|
+
// written — the same reasoning that already suppresses a mobile value
|
|
998
|
+
// equal to desktop.
|
|
999
|
+
const inheritedRaw = isAuthored(responsiveValue.mobile)
|
|
1000
|
+
? responsiveValue.mobile
|
|
1001
|
+
: responsiveValue.desktop;
|
|
1002
|
+
const inherited = isAuthored(inheritedRaw)
|
|
1003
|
+
? declarationValue(
|
|
1004
|
+
property,
|
|
1005
|
+
cssProperty,
|
|
1006
|
+
isAuthored(responsiveValue.mobile) ? 'mobile' : 'desktop',
|
|
1007
|
+
inheritedRaw
|
|
1008
|
+
)
|
|
1009
|
+
: '';
|
|
1010
|
+
|
|
1011
|
+
if (formattedTablet && formattedTablet !== inherited) {
|
|
1012
|
+
tabletStyles.push(` ${cssProperty}: ${formattedTablet}`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
825
1015
|
} else {
|
|
826
1016
|
if (cssProperty === 'background-color' && typeof value === 'string') {
|
|
827
1017
|
const backgroundAlpha = getBackgroundAlpha('desktop');
|
|
@@ -851,14 +1041,80 @@ const generateElementCSS = (
|
|
|
851
1041
|
|
|
852
1042
|
css += buildHoverCss('desktop');
|
|
853
1043
|
|
|
1044
|
+
const mobileHoverCss = buildHoverCss('mobile', ' ');
|
|
1045
|
+
|
|
854
1046
|
if (mobileStyles.length > 0) {
|
|
855
|
-
const mobileHoverCss = buildHoverCss('mobile', ' ');
|
|
856
1047
|
|
|
857
|
-
css +=
|
|
1048
|
+
css += `${MOBILE_MEDIA_QUERY} {\n ${selector} {\n ${mobileStyles.join(
|
|
858
1049
|
';\n '
|
|
859
1050
|
)};\n }\n${mobileHoverCss}}\n`;
|
|
860
1051
|
}
|
|
861
1052
|
|
|
1053
|
+
// After the mobile query, never before it. Both match at 900px, both carry
|
|
1054
|
+
// the same selector and therefore the same specificity, so the one written
|
|
1055
|
+
// second is the one that wins — that ordering is the entire mechanism by
|
|
1056
|
+
// which a tablet value beats the mobile value it falls back to.
|
|
1057
|
+
const tabletHoverCss = buildHoverCss('tablet', ' ');
|
|
1058
|
+
// What is already painting at 900px: the mobile query when one was written,
|
|
1059
|
+
// otherwise the base rule. A helper key (hover-*, background-opacity) never
|
|
1060
|
+
// reaches `tabletStyles`, so keying the query on that array alone dropped a
|
|
1061
|
+
// tablet-authored hover entirely.
|
|
1062
|
+
const inheritedHoverCss =
|
|
1063
|
+
mobileStyles.length > 0 ? mobileHoverCss : buildHoverCss('desktop', ' ');
|
|
1064
|
+
|
|
1065
|
+
if (tabletStyles.length > 0 || tabletHoverCss !== inheritedHoverCss) {
|
|
1066
|
+
|
|
1067
|
+
const tabletDeclarations =
|
|
1068
|
+
tabletStyles.length > 0
|
|
1069
|
+
? ` ${selector} {\n ${tabletStyles.join(';\n ')};\n }\n`
|
|
1070
|
+
: '';
|
|
1071
|
+
|
|
1072
|
+
css += `${TABLET_MEDIA_QUERY} {\n${tabletDeclarations}${tabletHoverCss}}\n`;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return css;
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Rules for the named parts inside a functional block.
|
|
1080
|
+
*
|
|
1081
|
+
* The element being targeted almost always carries the renderer's own inline
|
|
1082
|
+
* defaults, and inline styles outrank any stylesheet — so a designer's
|
|
1083
|
+
* override is emitted `!important`. Without it the override would be written,
|
|
1084
|
+
* saved, shown in the panel, and then quietly do nothing.
|
|
1085
|
+
*/
|
|
1086
|
+
const generatePartCSS = (
|
|
1087
|
+
blockSelector: string,
|
|
1088
|
+
styles: Record<string, unknown>,
|
|
1089
|
+
targetBreakpoint?: string
|
|
1090
|
+
): string => {
|
|
1091
|
+
const { parts } = splitPartStyles(styles);
|
|
1092
|
+
if (parts.length === 0) return '';
|
|
1093
|
+
|
|
1094
|
+
let css = '';
|
|
1095
|
+
|
|
1096
|
+
parts.forEach((bucket) => {
|
|
1097
|
+
const selectors = buildPartSelectors(blockSelector, bucket);
|
|
1098
|
+
if (selectors.length === 0) return;
|
|
1099
|
+
|
|
1100
|
+
const rules = generateElementCSS(
|
|
1101
|
+
selectors.join(',\n'),
|
|
1102
|
+
bucket.styles as ResponsiveStyles,
|
|
1103
|
+
targetBreakpoint,
|
|
1104
|
+
{ treatHelperKeysAsCss: true }
|
|
1105
|
+
);
|
|
1106
|
+
|
|
1107
|
+
if (!rules) return;
|
|
1108
|
+
|
|
1109
|
+
css += rules.replace(
|
|
1110
|
+
/^(\s*)([a-z-]+):\s*([^;\n]+);/gm,
|
|
1111
|
+
(line, indent, property, value) =>
|
|
1112
|
+
value.includes('!important')
|
|
1113
|
+
? line
|
|
1114
|
+
: `${indent}${property}: ${value} !important;`
|
|
1115
|
+
);
|
|
1116
|
+
});
|
|
1117
|
+
|
|
862
1118
|
return css;
|
|
863
1119
|
};
|
|
864
1120
|
|
|
@@ -873,7 +1129,37 @@ const collectBlockCSS = (
|
|
|
873
1129
|
const selector = `[data-block-id="${escapeCssAttributeValue(
|
|
874
1130
|
block.styleSourceId || block.id
|
|
875
1131
|
)}"]`;
|
|
876
|
-
|
|
1132
|
+
// Root styles ignore part keys, so a part override never leaks onto the
|
|
1133
|
+
// block wrapper (a padding meant for the dropdown would otherwise
|
|
1134
|
+
// reposition the whole block).
|
|
1135
|
+
const { root } = splitPartStyles(block.styles);
|
|
1136
|
+
|
|
1137
|
+
if (Object.keys(root).length > 0) {
|
|
1138
|
+
// `data-block-id` sits on the designer wrapper, not on the element the
|
|
1139
|
+
// renderer actually draws — so a padding or border set on a functional
|
|
1140
|
+
// block stopped at a transparent div and never reached the control.
|
|
1141
|
+
// A block that names its real root with `data-part="root"` gets its
|
|
1142
|
+
// own styles routed there instead. Blocks that do not are untouched,
|
|
1143
|
+
// which is what keeps published compositions rendering as before.
|
|
1144
|
+
const rootSelector = block.stylesTargetRoot
|
|
1145
|
+
? `${selector} [data-part="root"]`
|
|
1146
|
+
: selector;
|
|
1147
|
+
|
|
1148
|
+
css += generateElementCSS(rootSelector, root, targetBreakpoint);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
css += generatePartCSS(selector, block.styles, targetBreakpoint);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (block.properties) {
|
|
1155
|
+
const selector = `[data-block-id="${escapeCssAttributeValue(
|
|
1156
|
+
block.styleSourceId || block.id
|
|
1157
|
+
)}"]`;
|
|
1158
|
+
css += buildCustomCss(
|
|
1159
|
+
block.properties as Record<string, unknown>,
|
|
1160
|
+
selector,
|
|
1161
|
+
targetBreakpoint
|
|
1162
|
+
);
|
|
877
1163
|
}
|
|
878
1164
|
|
|
879
1165
|
if (block.blocks && block.blocks.length > 0) {
|
|
@@ -891,11 +1177,36 @@ export const generateThemeCSS = (
|
|
|
891
1177
|
let css = '';
|
|
892
1178
|
|
|
893
1179
|
sections.forEach((section) => {
|
|
1180
|
+
const sectionSelector = `[data-section-id="${escapeCssAttributeValue(
|
|
1181
|
+
section.id
|
|
1182
|
+
)}"]`;
|
|
1183
|
+
|
|
894
1184
|
if (section.styles && Object.keys(section.styles).length > 0) {
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
)
|
|
898
|
-
|
|
1185
|
+
// Sections went straight to generateElementCSS, so a part key on a
|
|
1186
|
+
// section was emitted as the invalid declaration `dropdown/color:`.
|
|
1187
|
+
const { root: sectionRoot } = splitPartStyles(section.styles);
|
|
1188
|
+
|
|
1189
|
+
if (Object.keys(sectionRoot).length > 0) {
|
|
1190
|
+
css += generateElementCSS(
|
|
1191
|
+
sectionSelector,
|
|
1192
|
+
sectionRoot,
|
|
1193
|
+
targetBreakpoint
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
css += generatePartCSS(
|
|
1198
|
+
sectionSelector,
|
|
1199
|
+
section.styles,
|
|
1200
|
+
targetBreakpoint
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
if (section.properties) {
|
|
1205
|
+
css += buildCustomCss(
|
|
1206
|
+
section.properties as Record<string, unknown>,
|
|
1207
|
+
sectionSelector,
|
|
1208
|
+
targetBreakpoint
|
|
1209
|
+
);
|
|
899
1210
|
}
|
|
900
1211
|
|
|
901
1212
|
if (section.blocks && section.blocks.length > 0) {
|