@akinon/pz-theme 2.0.83-rc.0 → 2.0.84-beta.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 +39 -33
- package/package.json +4 -4
- package/src/__tests__/auth-flow-block.test.tsx +211 -0
- package/src/__tests__/auth-flow-bus.test.ts +290 -0
- package/src/__tests__/auth-form-error-block.test.tsx +326 -0
- package/src/__tests__/auth-form-validation.test.tsx +204 -0
- package/src/__tests__/auth-step-title.test.tsx +202 -0
- package/src/__tests__/detached-submit-state.test.tsx +216 -0
- package/src/__tests__/forgot-password-composed.test.tsx +313 -0
- package/src/__tests__/otp-form-composed.test.tsx +487 -0
- package/src/__tests__/password-reset-step.test.tsx +269 -0
- package/src/__tests__/register-form-composed.test.tsx +739 -0
- package/src/__tests__/registry-parity.test.ts +9 -0
- package/src/auth/auth-field-errors.tsx +151 -0
- package/src/auth/auth-flow-bus.ts +415 -0
- package/src/auth/form-error-context.tsx +55 -0
- package/src/auth/form-submit-bus.ts +93 -0
- package/src/auth/otp-form-context.tsx +80 -0
- package/src/blocks/__tests__/button-block.test.tsx +55 -0
- package/src/blocks/__tests__/link-block.test.tsx +48 -30
- package/src/blocks/__tests__/overlay-sweep.test.tsx +31 -8
- package/src/blocks/__tests__/product-gallery-block.test.tsx +12 -4
- package/src/blocks/__tests__/product-recommendations-block.test.tsx +9 -2
- package/src/blocks/__tests__/store-locator-block.test.tsx +35 -8
- package/src/blocks/__tests__/text-block.test.tsx +37 -3
- package/src/blocks/__tests__/variant-selector-block.test.tsx +16 -4
- package/src/blocks/auth-consent-block.tsx +214 -0
- package/src/blocks/auth-error-block.tsx +82 -0
- package/src/blocks/auth-field-block.tsx +270 -0
- package/src/blocks/auth-flow-block.tsx +251 -0
- package/src/blocks/auth-form-block.tsx +391 -47
- package/src/blocks/auth-step-block.tsx +81 -0
- package/src/blocks/auth-step-title-block.tsx +81 -0
- package/src/blocks/block-renderer-registry.tsx +18 -0
- package/src/blocks/button-block.tsx +91 -13
- package/src/blocks/forgot-password-block.tsx +318 -24
- package/src/blocks/link-block.tsx +22 -84
- package/src/blocks/otp-code-input-block.tsx +180 -0
- package/src/blocks/otp-form-block.tsx +1092 -0
- package/src/blocks/otp-phase-block.tsx +71 -0
- package/src/blocks/register-form-block.tsx +541 -74
- package/src/blocks/text-block.tsx +17 -40
- package/src/chrome/__tests__/theme-config-source.test.ts +4 -1
- package/src/hooks/__tests__/use-overlay.test.tsx +97 -5
- package/src/hooks/use-block-action.ts +87 -4
- package/src/hooks/use-overlay.ts +60 -9
- package/src/overlays/__tests__/modal-chrome-padding.test.tsx +119 -0
- package/src/overlays/__tests__/modal-registry.test.ts +6 -1
- package/src/overlays/__tests__/panel-stretch.test.tsx +60 -0
- package/src/overlays/__tests__/responsive-chrome.test.tsx +446 -0
- package/src/overlays/__tests__/responsive-presentation.test.tsx +192 -0
- package/src/overlays/__tests__/theme-modal-mount.test.tsx +12 -1
- package/src/overlays/modal-preview-host.tsx +39 -2
- package/src/overlays/modal-registry.ts +344 -4
- package/src/overlays/overlay-surface.tsx +249 -19
- package/src/overlays/overlay-tokens.ts +94 -0
- package/src/overlays/theme-modal-mount.tsx +34 -4
- package/src/theme-block.tsx +6 -0
- package/src/utils/__tests__/inner-root-styles.test.tsx +139 -0
- package/src/utils/__tests__/iterator-bindings.test.tsx +9 -3
- package/src/utils/index.ts +107 -6
- package/src/utils/iterator-utils.ts +39 -2
- package/src/utils/part-styles.ts +4 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
INNER_ROOT_BLOCK_TYPES,
|
|
6
|
+
generateThemeCSS,
|
|
7
|
+
getInnerRootInlineStyles
|
|
8
|
+
} from '../index';
|
|
9
|
+
import TextBlock from '../../blocks/text-block';
|
|
10
|
+
import type { Block } from '../../theme-block';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Where a block's root styles are applied — once.
|
|
14
|
+
*
|
|
15
|
+
* A handful of renderers draw their own root element rather than letting the
|
|
16
|
+
* designer wrapper be the box: a text is a `<div>` with the copy in it, a link
|
|
17
|
+
* is an `<a>`, a button is a `<button>`. Those renderers also inlined the
|
|
18
|
+
* block's whole style object on that element, while the generated stylesheet
|
|
19
|
+
* wrote the same declarations onto the wrapper around it. A padding of 24 was
|
|
20
|
+
* therefore rendered as 24 outside AND 24 inside, and a designer who typed one
|
|
21
|
+
* number saw two gaps.
|
|
22
|
+
*
|
|
23
|
+
* The fix is one rule: for these types the sheet targets the element the
|
|
24
|
+
* renderer draws (`data-part="root"`), and the renderer inlines only what a
|
|
25
|
+
* stylesheet cannot say.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const blockOf = (overrides: Partial<Block>): Block =>
|
|
29
|
+
({
|
|
30
|
+
id: 'blk',
|
|
31
|
+
type: 'text',
|
|
32
|
+
label: 'Block',
|
|
33
|
+
value: 'Hello',
|
|
34
|
+
order: 0,
|
|
35
|
+
hidden: false,
|
|
36
|
+
properties: {},
|
|
37
|
+
styles: {},
|
|
38
|
+
blocks: [],
|
|
39
|
+
...overrides
|
|
40
|
+
}) as Block;
|
|
41
|
+
|
|
42
|
+
const sectionWith = (block: Partial<Block>) =>
|
|
43
|
+
[
|
|
44
|
+
{
|
|
45
|
+
id: 'sec-1',
|
|
46
|
+
styles: {},
|
|
47
|
+
blocks: [blockOf(block)]
|
|
48
|
+
}
|
|
49
|
+
] as unknown as Parameters<typeof generateThemeCSS>[0];
|
|
50
|
+
|
|
51
|
+
describe('a renderer that draws its own root', () => {
|
|
52
|
+
it('is the element the stylesheet targets, so a padding lands once', () => {
|
|
53
|
+
const css = generateThemeCSS(
|
|
54
|
+
sectionWith({ id: 'txt', type: 'text', styles: { padding: '24px' } })
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
expect(css).toContain('[data-block-id="txt"] [data-part="root"] {');
|
|
58
|
+
// The wrapper must NOT also carry it: that second application is the bug.
|
|
59
|
+
expect(css).not.toContain('[data-block-id="txt"] {');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('leaves every other block type on the wrapper', () => {
|
|
63
|
+
const css = generateThemeCSS(
|
|
64
|
+
sectionWith({ id: 'img', type: 'image', styles: { padding: '24px' } })
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Published compositions render against the wrapper rule; only the types
|
|
68
|
+
// whose renderer marks its root may be moved.
|
|
69
|
+
expect(css).toContain('[data-block-id="img"] {');
|
|
70
|
+
expect(css).not.toContain('[data-part="root"]');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('covers the three renderers that mark their root', () => {
|
|
74
|
+
expect([...INNER_ROOT_BLOCK_TYPES].sort()).toEqual([
|
|
75
|
+
'button',
|
|
76
|
+
'link',
|
|
77
|
+
'text'
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('what still has to be inlined', () => {
|
|
83
|
+
it('is nothing, for a plain value the sheet can write', () => {
|
|
84
|
+
expect(
|
|
85
|
+
getInnerRootInlineStyles({ padding: '24px', 'font-size': '16px' })
|
|
86
|
+
).toEqual({});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('is a theme token, which only the renderer can resolve', () => {
|
|
90
|
+
const inline = getInnerRootInlineStyles(
|
|
91
|
+
{ color: 'var(--theme-primary)', padding: '24px' },
|
|
92
|
+
{ primaryColor: '#00a86b' } as never
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Normalised to rgba the same way every colour is, alpha 1 for "no
|
|
96
|
+
// opacity set" — what matters is that the token became a real colour.
|
|
97
|
+
expect(inline).toEqual({ color: 'rgba(0, 168, 107, 1)' });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('is a colour with text-opacity folded into it', () => {
|
|
101
|
+
// The generated CSS treats text-opacity as a helper key and never applies
|
|
102
|
+
// it to `color`, so this composition only exists inline.
|
|
103
|
+
const inline = getInnerRootInlineStyles({
|
|
104
|
+
color: '#00a86b',
|
|
105
|
+
'text-opacity': '40'
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(inline).toEqual({ color: 'rgba(0, 168, 107, 0.4)' });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('is whatever an iterator row resolved for itself', () => {
|
|
112
|
+
// One rule serves every clone, so a per-row value has nowhere else to live.
|
|
113
|
+
const inline = getInnerRootInlineStyles(
|
|
114
|
+
{ 'background-color': '#e63946', 'border-radius': '4px' },
|
|
115
|
+
undefined,
|
|
116
|
+
'desktop',
|
|
117
|
+
['background-color']
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(inline).toEqual({ backgroundColor: 'rgba(230, 57, 70, 1)' });
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('the text block', () => {
|
|
125
|
+
it('marks its root and inlines no padding of its own', () => {
|
|
126
|
+
const { container } = render(
|
|
127
|
+
<TextBlock
|
|
128
|
+
block={blockOf({ styles: { padding: '24px', color: '#111111' } })}
|
|
129
|
+
currentBreakpoint="desktop"
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const root = container.querySelector('[data-part="root"]') as HTMLElement;
|
|
134
|
+
|
|
135
|
+
expect(root).not.toBeNull();
|
|
136
|
+
expect(root.style.padding).toBe('');
|
|
137
|
+
expect(root.style.color).toBe('');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -303,15 +303,21 @@ describe('a style value can carry a binding', () => {
|
|
|
303
303
|
// through, so the row gets a real rgba rather than a flat swatch.
|
|
304
304
|
expect(painted[0].style.backgroundColor).toBe('rgba(0, 168, 107, 0.4)');
|
|
305
305
|
expect(painted[1].style.backgroundColor).toMatch(/230, 57, 70/);
|
|
306
|
-
// The
|
|
307
|
-
|
|
306
|
+
// The UNBOUND property is deliberately not inlined: it belongs to the one
|
|
307
|
+
// rule every clone shares (see INNER_ROOT_BLOCK_TYPES). Inlining it as
|
|
308
|
+
// well applied it twice — once from the rule, once from here — which is
|
|
309
|
+
// what doubled every padding a designer set on a text block.
|
|
310
|
+
expect(painted[0].style.borderRadius).toBe('');
|
|
308
311
|
});
|
|
309
312
|
|
|
310
313
|
it('paints nothing where the row carries no colour', () => {
|
|
311
314
|
const [painted] = paintBadges([{ name: 'No badge' }]);
|
|
312
315
|
|
|
313
316
|
expect(painted.style.backgroundColor).toBe('');
|
|
314
|
-
|
|
317
|
+
// No inline attribute at all now that the unbound properties live on the
|
|
318
|
+
// shared rule — but the point stands: an unresolved binding must never
|
|
319
|
+
// reach the element as the string `undefined`.
|
|
320
|
+
expect(painted.getAttribute('style') ?? '').not.toContain('undefined');
|
|
315
321
|
});
|
|
316
322
|
});
|
|
317
323
|
});
|
package/src/utils/index.ts
CHANGED
|
@@ -477,9 +477,11 @@ const resolveSectionMaxWidthTokens = <T extends Record<string, unknown>>(
|
|
|
477
477
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
478
478
|
const rawEntries = Object.entries(value as Record<string, unknown>);
|
|
479
479
|
|
|
480
|
-
if (
|
|
481
|
-
|
|
482
|
-
|
|
480
|
+
if (
|
|
481
|
+
rawEntries.some(([, breakpointValue]) =>
|
|
482
|
+
isSectionMaxWidthToken(breakpointValue)
|
|
483
|
+
)
|
|
484
|
+
) {
|
|
483
485
|
constrained = true;
|
|
484
486
|
}
|
|
485
487
|
|
|
@@ -525,6 +527,105 @@ const resolveSectionMaxWidthTokens = <T extends Record<string, unknown>>(
|
|
|
525
527
|
return changed ? (resolved as T) : styles;
|
|
526
528
|
};
|
|
527
529
|
|
|
530
|
+
/**
|
|
531
|
+
* Blocks whose renderer draws its OWN root element and marks it
|
|
532
|
+
* `data-part="root"`.
|
|
533
|
+
*
|
|
534
|
+
* Their generated root rule is routed to that element instead of onto the
|
|
535
|
+
* designer wrapper (see `rootSelector` in `collectBlockCSS`). Two things go
|
|
536
|
+
* wrong without it: a padding set on the block lands on the wrapper AND is
|
|
537
|
+
* inlined on the inner element by the renderer, so the shopper sees it twice;
|
|
538
|
+
* and a border or background set on a functional block stops at a transparent
|
|
539
|
+
* div that wraps the control instead of reaching it.
|
|
540
|
+
*
|
|
541
|
+
* A type not listed here keeps landing on the wrapper, which is what every
|
|
542
|
+
* other published block renders against today.
|
|
543
|
+
*/
|
|
544
|
+
export const INNER_ROOT_BLOCK_TYPES: ReadonlySet<string> = new Set([
|
|
545
|
+
'text',
|
|
546
|
+
'link',
|
|
547
|
+
'button'
|
|
548
|
+
]);
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* The part of a block's root styles that the STYLESHEET cannot produce, and
|
|
552
|
+
* which a renderer drawing its own root therefore still has to inline:
|
|
553
|
+
*
|
|
554
|
+
* - values carrying a theme token, which only `resolveThemeCssVariables`
|
|
555
|
+
* turns into a real colour;
|
|
556
|
+
* - `color`, once `text-opacity` has to be composited into it — the generated
|
|
557
|
+
* CSS treats text-opacity as a helper key and never applies it.
|
|
558
|
+
*
|
|
559
|
+
* Everything else comes from the routed rule, which is the whole point: a
|
|
560
|
+
* padding inlined here as well would be applied twice, and — being inline — it
|
|
561
|
+
* would also outrank the media query and the `:hover` the same stylesheet
|
|
562
|
+
* emits for that element.
|
|
563
|
+
*/
|
|
564
|
+
export const getInnerRootInlineStyles = (
|
|
565
|
+
rawStyles?: Record<string, unknown>,
|
|
566
|
+
themeSettings?: ThemeSettings,
|
|
567
|
+
breakpoint = 'desktop',
|
|
568
|
+
/**
|
|
569
|
+
* Properties an iterator clone resolved from its own row. They cannot come
|
|
570
|
+
* from the sheet at all: one rule serves every clone, so a per-row colour
|
|
571
|
+
* only exists here.
|
|
572
|
+
*/
|
|
573
|
+
rowStyleKeys?: string[]
|
|
574
|
+
): React.CSSProperties => {
|
|
575
|
+
if (!rawStyles) return {};
|
|
576
|
+
|
|
577
|
+
const resolved = getCSSStyles(rawStyles, themeSettings, breakpoint) as Record<
|
|
578
|
+
string,
|
|
579
|
+
unknown
|
|
580
|
+
>;
|
|
581
|
+
// The same read with no settings to resolve against: whatever differs is a
|
|
582
|
+
// value that carried a token.
|
|
583
|
+
const verbatim = getCSSStyles(rawStyles, undefined, breakpoint) as Record<
|
|
584
|
+
string,
|
|
585
|
+
unknown
|
|
586
|
+
>;
|
|
587
|
+
|
|
588
|
+
const opacityEntry =
|
|
589
|
+
(rawStyles as Record<string, unknown>)['text-opacity'] ??
|
|
590
|
+
(rawStyles as Record<string, unknown>).textOpacity;
|
|
591
|
+
const textOpacity = opacityEntry
|
|
592
|
+
? Number(getResponsiveValue(opacityEntry, breakpoint, '100'))
|
|
593
|
+
: 100;
|
|
594
|
+
const compositesTextAlpha = Number.isFinite(textOpacity) && textOpacity < 100;
|
|
595
|
+
|
|
596
|
+
// The row's own properties, in the camelCase the resolved object uses. An
|
|
597
|
+
// opacity binding paints its companion colour, so it keeps that key instead.
|
|
598
|
+
const OPACITY_TARGETS: Record<string, string[]> = {
|
|
599
|
+
'text-opacity': ['color'],
|
|
600
|
+
'background-opacity': ['backgroundColor', 'background']
|
|
601
|
+
};
|
|
602
|
+
const rowKeys = new Set(
|
|
603
|
+
(rowStyleKeys ?? []).flatMap((key) => {
|
|
604
|
+
const normalized = key.includes('-')
|
|
605
|
+
? key
|
|
606
|
+
: key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
607
|
+
|
|
608
|
+
return OPACITY_TARGETS[normalized] ?? [kebabToCamel(normalized)];
|
|
609
|
+
})
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
const kept: Record<string, unknown> = {};
|
|
613
|
+
|
|
614
|
+
Object.entries(resolved).forEach(([key, value]) => {
|
|
615
|
+
const carriesToken = value !== verbatim[key];
|
|
616
|
+
|
|
617
|
+
if (
|
|
618
|
+
carriesToken ||
|
|
619
|
+
rowKeys.has(key) ||
|
|
620
|
+
(key === 'color' && compositesTextAlpha)
|
|
621
|
+
) {
|
|
622
|
+
kept[key] = value;
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
return kept as React.CSSProperties;
|
|
627
|
+
};
|
|
628
|
+
|
|
528
629
|
export const getCSSStyles = (
|
|
529
630
|
rawStyles?: Record<string, unknown>,
|
|
530
631
|
themeSettings?: ThemeSettings,
|
|
@@ -1262,7 +1363,6 @@ const generateElementCSS = (
|
|
|
1262
1363
|
const mobileHoverCss = buildHoverCss('mobile', ' ');
|
|
1263
1364
|
|
|
1264
1365
|
if (mobileStyles.length > 0) {
|
|
1265
|
-
|
|
1266
1366
|
css += `${MOBILE_MEDIA_QUERY} {\n ${selector} {\n ${mobileStyles.join(
|
|
1267
1367
|
';\n '
|
|
1268
1368
|
)};\n }\n${mobileHoverCss}}\n`;
|
|
@@ -1281,7 +1381,6 @@ const generateElementCSS = (
|
|
|
1281
1381
|
mobileStyles.length > 0 ? mobileHoverCss : buildHoverCss('desktop', ' ');
|
|
1282
1382
|
|
|
1283
1383
|
if (tabletStyles.length > 0 || tabletHoverCss !== inheritedHoverCss) {
|
|
1284
|
-
|
|
1285
1384
|
const tabletDeclarations =
|
|
1286
1385
|
tabletStyles.length > 0
|
|
1287
1386
|
? ` ${selector} {\n ${tabletStyles.join(';\n ')};\n }\n`
|
|
@@ -1407,7 +1506,9 @@ const collectBlockCSS = (
|
|
|
1407
1506
|
// A block that names its real root with `data-part="root"` gets its
|
|
1408
1507
|
// own styles routed there instead. Blocks that do not are untouched,
|
|
1409
1508
|
// which is what keeps published compositions rendering as before.
|
|
1410
|
-
const
|
|
1509
|
+
const targetsInnerRoot =
|
|
1510
|
+
block.stylesTargetRoot ?? INNER_ROOT_BLOCK_TYPES.has(block.type ?? '');
|
|
1511
|
+
const rootSelector = targetsInnerRoot
|
|
1411
1512
|
? `${selector} [data-part="root"]`
|
|
1412
1513
|
: selector;
|
|
1413
1514
|
|
|
@@ -941,6 +941,39 @@ const isRowBoundStyleValue = (value: unknown): value is string =>
|
|
|
941
941
|
* SAME object when nothing is bound, so a composition without style bindings
|
|
942
942
|
* is not merely equal but identical — no new object, no changed CSS.
|
|
943
943
|
*/
|
|
944
|
+
/**
|
|
945
|
+
* Which properties a row resolves for itself.
|
|
946
|
+
*
|
|
947
|
+
* Read from the template's styles, while the tokens are still in them. The
|
|
948
|
+
* renderer needs the list because a per-row value is the one thing the shared
|
|
949
|
+
* stylesheet cannot carry: one rule serves every clone, so a badge colour that
|
|
950
|
+
* differs per product has to be inlined on the element. Everything else stays
|
|
951
|
+
* on the rule — see the note above `resolveStyleBindingsForRow`.
|
|
952
|
+
*/
|
|
953
|
+
export const collectRowBoundStyleKeys = (styles: unknown): string[] => {
|
|
954
|
+
if (!isRecord(styles)) return [];
|
|
955
|
+
|
|
956
|
+
const keys: string[] = [];
|
|
957
|
+
|
|
958
|
+
Object.entries(styles).forEach(([key, value]) => {
|
|
959
|
+
if (typeof value === 'string') {
|
|
960
|
+
if (isRowBoundStyleValue(value)) keys.push(key);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (isRecord(value)) {
|
|
965
|
+
const boundBreakpoint = Object.values(value).some(
|
|
966
|
+
(breakpointValue) =>
|
|
967
|
+
typeof breakpointValue === 'string' &&
|
|
968
|
+
isRowBoundStyleValue(breakpointValue)
|
|
969
|
+
);
|
|
970
|
+
if (boundBreakpoint) keys.push(key);
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
return keys;
|
|
975
|
+
};
|
|
976
|
+
|
|
944
977
|
const resolveStyleBindingsForRow = (
|
|
945
978
|
styles: unknown,
|
|
946
979
|
itemData: unknown
|
|
@@ -1132,12 +1165,16 @@ const replaceBlockValues = (
|
|
|
1132
1165
|
// Only the bound properties are replaced; the rest of the block's styles
|
|
1133
1166
|
// stay on the shared rule, so a card with one bound colour keeps the
|
|
1134
1167
|
// whole stylesheet it had.
|
|
1168
|
+
const rowBoundStyleKeys = collectRowBoundStyleKeys(nextBlock.styles);
|
|
1135
1169
|
const resolvedStyles = resolveStyleBindingsForRow(
|
|
1136
1170
|
nextBlock.styles,
|
|
1137
1171
|
itemData
|
|
1138
1172
|
);
|
|
1139
1173
|
if (resolvedStyles !== nextBlock.styles) {
|
|
1140
1174
|
nextBlock.styles = resolvedStyles;
|
|
1175
|
+
// A renderer that draws its own root has to inline exactly these: the
|
|
1176
|
+
// shared rule cannot say a different colour per row.
|
|
1177
|
+
nextBlock.rowStyleKeys = rowBoundStyleKeys;
|
|
1141
1178
|
}
|
|
1142
1179
|
|
|
1143
1180
|
// A repeat inside a row means "the row's list": a colour-swatch strip
|
|
@@ -1330,8 +1367,8 @@ export const buildIteratorBlock = ({
|
|
|
1330
1367
|
displayItems.length > 0
|
|
1331
1368
|
? displayItems.length
|
|
1332
1369
|
: scopedItems
|
|
1333
|
-
|
|
1334
|
-
|
|
1370
|
+
? 0
|
|
1371
|
+
: Math.max(iteratorCount, 1);
|
|
1335
1372
|
|
|
1336
1373
|
const clonedBlocks = Array.from({ length: actualCount }, (_, index) => {
|
|
1337
1374
|
// `??`, not `||`: an entry of a list of plain values can be falsy (0, '')
|
package/src/utils/part-styles.ts
CHANGED
|
@@ -69,6 +69,10 @@ const STATE_SELECTORS: Record<string, string[]> = {
|
|
|
69
69
|
available: ['[data-state~="available"]'],
|
|
70
70
|
unavailable: ['[data-state~="unavailable"]'],
|
|
71
71
|
unknown: ['[data-state~="unknown"]'],
|
|
72
|
+
// A price a campaign has cut. The card stamps it next to the struck retail
|
|
73
|
+
// figure (product-recommendations-block), so the discounted price can carry
|
|
74
|
+
// its own colour without a second block or a renderer flag.
|
|
75
|
+
discounted: ['[data-state~="discounted"]'],
|
|
72
76
|
// The odd ones out: every state above is a fact about the ELEMENT and matches
|
|
73
77
|
// on the element itself. These two are facts about the PAGE — the chrome slot
|
|
74
78
|
// carries `data-scroll-state` (see chrome/scroll-state.tsx) and `body` carries
|