@akinon/pz-theme 2.0.100-rc.0 → 2.0.101-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 +16 -33
- package/package.json +4 -4
- package/src/__tests__/registry-parity.test.ts +1 -0
- package/src/blocks/__tests__/collapsible-block.test.tsx +521 -0
- package/src/blocks/__tests__/image-block.test.tsx +173 -0
- package/src/blocks/block-renderer-registry.tsx +2 -0
- package/src/blocks/collapsible-block.tsx +595 -0
- package/src/blocks/image-block.tsx +150 -4
- package/src/utils/breakpoints.ts +30 -0
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
getResponsiveValue,
|
|
9
9
|
resolveOption
|
|
10
10
|
} from '../utils';
|
|
11
|
+
import {
|
|
12
|
+
PHONE_AND_TABLET_MEDIA_CONDITION,
|
|
13
|
+
PHONE_ONLY_MEDIA_CONDITION,
|
|
14
|
+
TABLET_ONLY_MEDIA_CONDITION
|
|
15
|
+
} from '../utils/breakpoints';
|
|
11
16
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
12
17
|
|
|
13
18
|
const pickImageSource = (value: unknown, currentBreakpoint: string): string => {
|
|
@@ -63,8 +68,73 @@ const isValidImageSource = (src: string): boolean => {
|
|
|
63
68
|
}
|
|
64
69
|
};
|
|
65
70
|
|
|
71
|
+
/**
|
|
72
|
+
* The narrow-band overrides, resolved to absolute URLs.
|
|
73
|
+
*
|
|
74
|
+
* Authored as two ordinary `image` properties rather than one map, so the
|
|
75
|
+
* merchant gets the SAME upload control they already know, and so a block that
|
|
76
|
+
* was published before this existed simply has neither key — which is what
|
|
77
|
+
* makes the "no override" path below byte-identical to the old renderer.
|
|
78
|
+
*
|
|
79
|
+
* The desktop source deliberately stays on `block.value`. Moving it to a
|
|
80
|
+
* property would blank every published image (the rule this repo already wrote
|
|
81
|
+
* down for the twin case in the editor's video-block-sources test), and the
|
|
82
|
+
* save path flattens an `image` block's `value` to a single string anyway, so a
|
|
83
|
+
* per-breakpoint map could never survive a round trip there.
|
|
84
|
+
*/
|
|
85
|
+
const resolveOverride = (value: unknown, currentBreakpoint: string): string => {
|
|
86
|
+
const raw = pickImageSource(value, currentBreakpoint);
|
|
87
|
+
const url = normalizeImageSource(raw);
|
|
88
|
+
return isValidImageSource(url) ? url : '';
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export interface PictureSource {
|
|
92
|
+
media: string;
|
|
93
|
+
srcSet: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The `<source>` list, in FIRST-MATCH order.
|
|
98
|
+
*
|
|
99
|
+
* Mirrors how the generated stylesheet cascades, which is the behaviour the
|
|
100
|
+
* merchant has already learned everywhere else in the editor: a value authored
|
|
101
|
+
* at "mobile" applies up to 1023px *unless* a tablet value narrows it. CSS says
|
|
102
|
+
* that with an overlap plus source order; `<picture>` has no cascade, so the
|
|
103
|
+
* overlap has to be removed here instead — hence the phone band widening to
|
|
104
|
+
* 1023 exactly when no tablet image was authored.
|
|
105
|
+
*
|
|
106
|
+
* A band equal to the desktop source is dropped rather than emitted: an
|
|
107
|
+
* identical candidate would make the browser re-decide for nothing, and it is
|
|
108
|
+
* what keeps an untouched block on the plain-`<img>` path.
|
|
109
|
+
*/
|
|
110
|
+
export const buildPictureSources = (
|
|
111
|
+
desktop: string,
|
|
112
|
+
tablet: string,
|
|
113
|
+
mobile: string
|
|
114
|
+
): PictureSource[] => {
|
|
115
|
+
const sources: PictureSource[] = [];
|
|
116
|
+
const phoneSrc = mobile || '';
|
|
117
|
+
const tabletSrc = tablet || '';
|
|
118
|
+
|
|
119
|
+
if (phoneSrc && phoneSrc !== desktop) {
|
|
120
|
+
sources.push({
|
|
121
|
+
media: tabletSrc
|
|
122
|
+
? PHONE_ONLY_MEDIA_CONDITION
|
|
123
|
+
: PHONE_AND_TABLET_MEDIA_CONDITION,
|
|
124
|
+
srcSet: phoneSrc
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (tabletSrc && tabletSrc !== desktop) {
|
|
129
|
+
sources.push({ media: TABLET_ONLY_MEDIA_CONDITION, srcSet: tabletSrc });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return sources;
|
|
133
|
+
};
|
|
134
|
+
|
|
66
135
|
const ImageBlock = ({
|
|
67
136
|
block,
|
|
137
|
+
isDesigner = false,
|
|
68
138
|
currentBreakpoint = 'desktop'
|
|
69
139
|
}: BlockRendererProps) => {
|
|
70
140
|
const imageValue = useMemo(() => {
|
|
@@ -78,7 +148,49 @@ const ImageBlock = ({
|
|
|
78
148
|
};
|
|
79
149
|
}, [block.value, block.properties?.alt, currentBreakpoint]);
|
|
80
150
|
|
|
81
|
-
const { url:
|
|
151
|
+
const { url: desktopSrc, alt } = imageValue;
|
|
152
|
+
|
|
153
|
+
const imageTablet = resolveOverride(
|
|
154
|
+
block.properties?.imageTablet,
|
|
155
|
+
currentBreakpoint
|
|
156
|
+
);
|
|
157
|
+
const imageMobile = resolveOverride(
|
|
158
|
+
block.properties?.imageMobile,
|
|
159
|
+
currentBreakpoint
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const pictureSources = useMemo(
|
|
163
|
+
() => buildPictureSources(desktopSrc, imageTablet, imageMobile),
|
|
164
|
+
[desktopSrc, imageTablet, imageMobile]
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
/*
|
|
168
|
+
* THE CANVAS CANNOT PREVIEW A `<picture>`.
|
|
169
|
+
*
|
|
170
|
+
* `<source media>` is answered by the browser against the REAL viewport, and
|
|
171
|
+
* there is no CSS that can override it. The editor draws a phone preview
|
|
172
|
+
* inside a desktop-wide iframe, so a real `<picture>` would show the desktop
|
|
173
|
+
* image in every preview device — the author would have no way to see the
|
|
174
|
+
* phone crop they just uploaded.
|
|
175
|
+
*
|
|
176
|
+
* So the canvas keeps resolving in JS at the breakpoint the editor selected,
|
|
177
|
+
* and only the storefront ships the real element. The cost is named rather
|
|
178
|
+
* than hidden: the canvas no longer renders byte-identical markup, so a
|
|
179
|
+
* malformed `media` or a dropped `srcset` is invisible there and has to be
|
|
180
|
+
* caught by the tests below it.
|
|
181
|
+
*
|
|
182
|
+
* The fallback chain matches the emitted source list: at tablet, an
|
|
183
|
+
* unauthored tablet image falls through to the phone image the same way the
|
|
184
|
+
* widened phone band does on the storefront.
|
|
185
|
+
*/
|
|
186
|
+
const designerSrc =
|
|
187
|
+
currentBreakpoint === 'mobile'
|
|
188
|
+
? imageMobile || desktopSrc
|
|
189
|
+
: currentBreakpoint === 'tablet'
|
|
190
|
+
? imageTablet || imageMobile || desktopSrc
|
|
191
|
+
: desktopSrc;
|
|
192
|
+
|
|
193
|
+
const src = isDesigner ? designerSrc : desktopSrc;
|
|
82
194
|
const loadingStrategyValue =
|
|
83
195
|
block.properties?.loadingStrategy ?? block.styles?.loadingStrategy;
|
|
84
196
|
const priorityValue = block.properties?.priority ?? block.styles?.priority;
|
|
@@ -152,9 +264,9 @@ const ImageBlock = ({
|
|
|
152
264
|
*/
|
|
153
265
|
const boxIsSized =
|
|
154
266
|
(typeof height === 'string' && height !== 'auto') || Boolean(aspectRatio);
|
|
155
|
-
const boxWidth = isRowBound('width') ? width ?? '100%' : '100%';
|
|
267
|
+
const boxWidth = isRowBound('width') ? (width ?? '100%') : '100%';
|
|
156
268
|
const boxHeight = isRowBound('height')
|
|
157
|
-
? height ?? 'auto'
|
|
269
|
+
? (height ?? 'auto')
|
|
158
270
|
: boxIsSized
|
|
159
271
|
? '100%'
|
|
160
272
|
: 'auto';
|
|
@@ -206,7 +318,41 @@ const ImageBlock = ({
|
|
|
206
318
|
/>
|
|
207
319
|
);
|
|
208
320
|
|
|
209
|
-
|
|
321
|
+
/*
|
|
322
|
+
* No override, or the canvas: emit exactly what this block emitted before
|
|
323
|
+
* this feature existed. Every block published to date takes this path, so its
|
|
324
|
+
* HTML is unchanged byte for byte — a `<picture>` wrapping a lone `<img>`
|
|
325
|
+
* would be harmless to the browser but would still be a diff in every
|
|
326
|
+
* snapshot and every brand's CSS that targets `[data-block-id] > img`.
|
|
327
|
+
*/
|
|
328
|
+
if (!src || isDesigner || pictureSources.length === 0) {
|
|
329
|
+
return content;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/*
|
|
333
|
+
* `display: contents` is load-bearing, not tidiness.
|
|
334
|
+
*
|
|
335
|
+
* `image` is not in INNER_ROOT_BLOCK_TYPES, so the generated stylesheet puts
|
|
336
|
+
* this block's width/height/aspect-ratio on the `[data-block-id]` WRAPPER and
|
|
337
|
+
* the <img> above is sized against it with percentages. A `<picture>` between
|
|
338
|
+
* the two would become the flex item the parent lays out and the percentage
|
|
339
|
+
* parent the <img> resolves against — so `height: 100%` would resolve against
|
|
340
|
+
* an auto-height box and collapse. `display: contents` removes the box while
|
|
341
|
+
* keeping the element, which is exactly what is wanted: the <source> list is
|
|
342
|
+
* markup, not layout.
|
|
343
|
+
*/
|
|
344
|
+
return (
|
|
345
|
+
<picture style={{ display: 'contents' }}>
|
|
346
|
+
{pictureSources.map((source) => (
|
|
347
|
+
<source
|
|
348
|
+
key={source.media}
|
|
349
|
+
media={source.media}
|
|
350
|
+
srcSet={source.srcSet}
|
|
351
|
+
/>
|
|
352
|
+
))}
|
|
353
|
+
{content}
|
|
354
|
+
</picture>
|
|
355
|
+
);
|
|
210
356
|
};
|
|
211
357
|
|
|
212
358
|
export default ImageBlock;
|
package/src/utils/breakpoints.ts
CHANGED
|
@@ -114,3 +114,33 @@ export const breakpointForWidth = (width: number): BreakpointName => {
|
|
|
114
114
|
*/
|
|
115
115
|
export const isNarrowBreakpoint = (breakpoint?: string): boolean =>
|
|
116
116
|
breakpoint === 'mobile' || breakpoint === 'tablet';
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The same two bands as media QUERIES, but as bare media CONDITIONS for a
|
|
120
|
+
* `<picture><source media>` attribute.
|
|
121
|
+
*
|
|
122
|
+
* Two differences from the `@media` constants above, both load-bearing:
|
|
123
|
+
*
|
|
124
|
+
* - No `@media` prefix. `<source media>` takes a media *condition*, and a
|
|
125
|
+
* string starting with `@media` never matches — silently, because a source
|
|
126
|
+
* whose condition fails is simply skipped and the browser falls through to
|
|
127
|
+
* the `<img>`. The bug would look like "the phone image is ignored".
|
|
128
|
+
* - The phone band is EXCLUSIVE of the tablet band (`max-width: 767px`), while
|
|
129
|
+
* `MOBILE_MEDIA_QUERY` deliberately overlaps it (`max-width: 1023px`) and
|
|
130
|
+
* lets source order decide. CSS resolves an overlap by cascade; `<picture>`
|
|
131
|
+
* resolves it by FIRST MATCH, so an overlapping phone condition placed first
|
|
132
|
+
* would swallow every tablet. Where no tablet image is authored the caller
|
|
133
|
+
* widens the phone band back to 1023 on purpose — that is what reproduces
|
|
134
|
+
* the stylesheet's "mobile covers tablet unless tablet overrides" rule.
|
|
135
|
+
*/
|
|
136
|
+
export const PHONE_ONLY_MEDIA_CONDITION = `(max-width: ${
|
|
137
|
+
TABLET_MIN_WIDTH - 1
|
|
138
|
+
}px)`;
|
|
139
|
+
|
|
140
|
+
export const PHONE_AND_TABLET_MEDIA_CONDITION = `(max-width: ${
|
|
141
|
+
DESKTOP_MIN_WIDTH - 1
|
|
142
|
+
}px)`;
|
|
143
|
+
|
|
144
|
+
export const TABLET_ONLY_MEDIA_CONDITION = `(min-width: ${TABLET_MIN_WIDTH}px) and (max-width: ${
|
|
145
|
+
DESKTOP_MIN_WIDTH - 1
|
|
146
|
+
}px)`;
|