@akinon/pz-theme 2.0.143-beta.0 → 2.0.143
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 +5 -4
- package/package.json +4 -4
- package/src/__tests__/designer-live-rows.test.tsx +257 -0
- package/src/blocks/__tests__/favorite-button-icons.test.tsx +102 -0
- package/src/blocks/__tests__/listing-price-note.test.ts +47 -0
- package/src/blocks/favorite-button-block.tsx +36 -15
- package/src/blocks/group-block.tsx +52 -1
- package/src/blocks/image-block.tsx +26 -2
- package/src/blocks/listing-context.ts +5 -2
- package/src/blocks/listing-row.ts +64 -4
- package/src/blocks/product-grid-block.tsx +83 -3
- package/src/components/card-list-slider.tsx +374 -0
- package/src/components/card-list-views.tsx +278 -0
- package/src/components/card-zoom.tsx +53 -0
- package/src/components/with-designer-features.tsx +18 -0
- package/src/theme-block.tsx +11 -0
- package/src/theme-placeholder-wrapper.tsx +52 -7
- package/src/utils/__tests__/card-media.test.tsx +789 -0
- package/src/utils/__tests__/card-views.test.tsx +332 -0
- package/src/utils/__tests__/swatch-colors.test.ts +145 -0
- package/src/utils/card-media.ts +331 -0
- package/src/utils/iterator-utils.ts +10 -3
- package/src/utils/live-rows.ts +169 -34
- package/src/utils/property-switches.ts +248 -2
- package/src/utils/swatch-colors.ts +81 -0
|
@@ -37,11 +37,46 @@
|
|
|
37
37
|
* Pure, run once over the section tree before anything expands: the iterator
|
|
38
38
|
* copies a template's children as they are, so a child dropped here is absent
|
|
39
39
|
* from every clone and a marked one is marked on every clone.
|
|
40
|
+
*
|
|
41
|
+
* THE SAME CHAIN CARRIES THE CARD LIST'S SETTINGS (ZERO-5173): the zoom on
|
|
42
|
+
* hover is resolved for the card's picture and written as `cardZoom`; the
|
|
43
|
+
* slider — its title among its settings — is resolved for the repeat that
|
|
44
|
+
* holds a section's cards and written as `cardsSlider`, and the listing's grid
|
|
45
|
+
* views, for a repeat that is not sliding, as `cardsViews`
|
|
46
|
+
* (utils/card-media.ts).
|
|
40
47
|
*/
|
|
41
48
|
|
|
42
49
|
import type { Block } from '../theme-block';
|
|
43
50
|
import type { Section } from '../theme-section';
|
|
44
51
|
import { fallbackChainOf } from './breakpoints';
|
|
52
|
+
import {
|
|
53
|
+
CARD_LIST_PROPERTIES,
|
|
54
|
+
type CardsSliderSettings,
|
|
55
|
+
type CardsSliderTitle,
|
|
56
|
+
type CardViewSettings,
|
|
57
|
+
DEFAULT_CARD_VIEW,
|
|
58
|
+
DEFAULT_CARD_VIEW_TOGGLES,
|
|
59
|
+
readCardView,
|
|
60
|
+
readCardViewTogglesAlign,
|
|
61
|
+
readCardViewTogglesColor,
|
|
62
|
+
readCardViewTogglesGap,
|
|
63
|
+
readCardViewTogglesPadding,
|
|
64
|
+
readCardViewTogglesSize,
|
|
65
|
+
DEFAULT_CARDS_PER_VIEW,
|
|
66
|
+
DEFAULT_CARDS_PER_VIEW_MOBILE,
|
|
67
|
+
DEFAULT_CARDS_SLIDER_TITLE,
|
|
68
|
+
isCardPictureBlock,
|
|
69
|
+
MAX_CARDS_PER_VIEW,
|
|
70
|
+
MAX_CARDS_PER_VIEW_MOBILE,
|
|
71
|
+
readCardsPerView,
|
|
72
|
+
readCardsSliderArrows,
|
|
73
|
+
readCardsSliderTitleColor,
|
|
74
|
+
readCardsSliderTitleFont,
|
|
75
|
+
readCardsSliderTitleSize,
|
|
76
|
+
readCardsSliderTitleText,
|
|
77
|
+
readCardsSliderTitleTransform,
|
|
78
|
+
readCardsSliderTitleWeight
|
|
79
|
+
} from './card-media';
|
|
45
80
|
|
|
46
81
|
export const VISIBLE_WHEN_PROPERTY = 'visibleWhenProperty';
|
|
47
82
|
export const HOVER_REVEAL_WHEN_PROPERTY = 'hoverRevealWhenProperty';
|
|
@@ -162,6 +197,202 @@ const findHoverScope = (scopes: Scope[], definer: Scope): Scope | null => {
|
|
|
162
197
|
return definer.isSection ? null : definer;
|
|
163
198
|
};
|
|
164
199
|
|
|
200
|
+
/**
|
|
201
|
+
* The innermost value a scope defines for a setting — `findDefiningScope` for
|
|
202
|
+
* values that are not switches. `read` answers undefined for "not defined
|
|
203
|
+
* here", which is what lets the search walk past a card left on Inherit.
|
|
204
|
+
*/
|
|
205
|
+
const findDefinedValue = <T>(
|
|
206
|
+
scopes: Scope[],
|
|
207
|
+
key: string,
|
|
208
|
+
breakpoint: string,
|
|
209
|
+
read: (raw: unknown) => T | undefined
|
|
210
|
+
): T | undefined => {
|
|
211
|
+
for (let index = scopes.length - 1; index >= 0; index -= 1) {
|
|
212
|
+
const raw = readResponsive(scopes[index].properties[key], breakpoint);
|
|
213
|
+
const value = read(raw);
|
|
214
|
+
if (value !== undefined) return value;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return undefined;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/** What a repeated card's root is marked with, whichever host it repeats in. */
|
|
221
|
+
const isCardRoot = (block: Block | undefined): boolean =>
|
|
222
|
+
Boolean(block) &&
|
|
223
|
+
(readKey(block?.properties, 'role') === 'item-template' ||
|
|
224
|
+
readKey(block?.properties, 'dataSourceSlot') === 'item-template' ||
|
|
225
|
+
readKey(block?.properties, 'blockTemplate') === 'product-card');
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The repeat that holds a list of cards: the generated iterator a collection
|
|
229
|
+
* wraps around the Product Cards section's card. Its first child is the card
|
|
230
|
+
* it repeats (the iterator's positional contract).
|
|
231
|
+
*/
|
|
232
|
+
const isCardListRepeat = (block: Block): boolean =>
|
|
233
|
+
block.isIterator === true && isCardRoot((block.blocks || [])[0]);
|
|
234
|
+
|
|
235
|
+
const readBoolean = (raw: unknown) => readSwitch(raw);
|
|
236
|
+
|
|
237
|
+
/** The zoom the card's picture answers to, from the card and the list around it. */
|
|
238
|
+
const resolveCardZoom = (chain: Scope[], breakpoint: string): boolean =>
|
|
239
|
+
findDefinedValue(
|
|
240
|
+
chain,
|
|
241
|
+
CARD_LIST_PROPERTIES.hoverZoom,
|
|
242
|
+
breakpoint,
|
|
243
|
+
readBoolean
|
|
244
|
+
) === true;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The view a list that is not sliding is drawn in, and the toggles when the
|
|
248
|
+
* shopper is offered them — or undefined for the list as authored: the
|
|
249
|
+
* classic view and no toggles, which is every list nobody configured.
|
|
250
|
+
*/
|
|
251
|
+
const resolveCardsViews = (
|
|
252
|
+
chain: Scope[],
|
|
253
|
+
breakpoint: string
|
|
254
|
+
): CardViewSettings | undefined => {
|
|
255
|
+
const setting = <T>(key: string, read: (raw: unknown) => T | undefined) =>
|
|
256
|
+
findDefinedValue(chain, key, breakpoint, read);
|
|
257
|
+
|
|
258
|
+
const view =
|
|
259
|
+
setting(CARD_LIST_PROPERTIES.view, readCardView) ?? DEFAULT_CARD_VIEW;
|
|
260
|
+
const offered = setting(CARD_LIST_PROPERTIES.viewModes, readBoolean) === true;
|
|
261
|
+
if (!offered && view === DEFAULT_CARD_VIEW) return undefined;
|
|
262
|
+
if (!offered) return { view };
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
view,
|
|
266
|
+
toggles: {
|
|
267
|
+
align:
|
|
268
|
+
setting(
|
|
269
|
+
CARD_LIST_PROPERTIES.viewTogglesAlign,
|
|
270
|
+
readCardViewTogglesAlign
|
|
271
|
+
) ?? DEFAULT_CARD_VIEW_TOGGLES.align,
|
|
272
|
+
color:
|
|
273
|
+
setting(
|
|
274
|
+
CARD_LIST_PROPERTIES.viewTogglesColor,
|
|
275
|
+
readCardViewTogglesColor
|
|
276
|
+
) ?? DEFAULT_CARD_VIEW_TOGGLES.color,
|
|
277
|
+
size:
|
|
278
|
+
setting(
|
|
279
|
+
CARD_LIST_PROPERTIES.viewTogglesSize,
|
|
280
|
+
readCardViewTogglesSize
|
|
281
|
+
) ?? DEFAULT_CARD_VIEW_TOGGLES.size,
|
|
282
|
+
gap:
|
|
283
|
+
setting(CARD_LIST_PROPERTIES.viewTogglesGap, readCardViewTogglesGap) ??
|
|
284
|
+
DEFAULT_CARD_VIEW_TOGGLES.gap,
|
|
285
|
+
padding:
|
|
286
|
+
setting(
|
|
287
|
+
CARD_LIST_PROPERTIES.viewTogglesPadding,
|
|
288
|
+
readCardViewTogglesPadding
|
|
289
|
+
) ?? DEFAULT_CARD_VIEW_TOGGLES.padding
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* The slider's title, when the list shows one: on (unless switched off) and
|
|
296
|
+
* with text. Each part of its type falls back to the rail's own default.
|
|
297
|
+
*/
|
|
298
|
+
const resolveCardsSliderTitle = (
|
|
299
|
+
chain: Scope[],
|
|
300
|
+
breakpoint: string
|
|
301
|
+
): CardsSliderTitle | undefined => {
|
|
302
|
+
const setting = <T>(key: string, read: (raw: unknown) => T | undefined) =>
|
|
303
|
+
findDefinedValue(chain, key, breakpoint, read);
|
|
304
|
+
|
|
305
|
+
if (setting(CARD_LIST_PROPERTIES.sliderShowTitle, readBoolean) === false) {
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const text = (
|
|
310
|
+
setting(CARD_LIST_PROPERTIES.sliderTitle, readCardsSliderTitleText) ?? ''
|
|
311
|
+
).trim();
|
|
312
|
+
if (!text) return undefined;
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
text,
|
|
316
|
+
color:
|
|
317
|
+
setting(
|
|
318
|
+
CARD_LIST_PROPERTIES.sliderTitleColor,
|
|
319
|
+
readCardsSliderTitleColor
|
|
320
|
+
) ?? DEFAULT_CARDS_SLIDER_TITLE.color,
|
|
321
|
+
size:
|
|
322
|
+
setting(CARD_LIST_PROPERTIES.sliderTitleSize, readCardsSliderTitleSize) ??
|
|
323
|
+
DEFAULT_CARDS_SLIDER_TITLE.size,
|
|
324
|
+
weight:
|
|
325
|
+
setting(
|
|
326
|
+
CARD_LIST_PROPERTIES.sliderTitleWeight,
|
|
327
|
+
readCardsSliderTitleWeight
|
|
328
|
+
) ?? DEFAULT_CARDS_SLIDER_TITLE.weight,
|
|
329
|
+
font:
|
|
330
|
+
setting(CARD_LIST_PROPERTIES.sliderTitleFont, readCardsSliderTitleFont) ??
|
|
331
|
+
DEFAULT_CARDS_SLIDER_TITLE.font,
|
|
332
|
+
transform:
|
|
333
|
+
setting(
|
|
334
|
+
CARD_LIST_PROPERTIES.sliderTitleTransform,
|
|
335
|
+
readCardsSliderTitleTransform
|
|
336
|
+
) ?? DEFAULT_CARDS_SLIDER_TITLE.transform
|
|
337
|
+
};
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* The slider the list answers to, or undefined while it is off — so a list
|
|
342
|
+
* that names a count but no slider stays the grid it was.
|
|
343
|
+
*/
|
|
344
|
+
const resolveCardsSlider = (
|
|
345
|
+
chain: Scope[],
|
|
346
|
+
breakpoint: string
|
|
347
|
+
): CardsSliderSettings | undefined => {
|
|
348
|
+
const setting = <T>(key: string, read: (raw: unknown) => T | undefined) =>
|
|
349
|
+
findDefinedValue(chain, key, breakpoint, read);
|
|
350
|
+
|
|
351
|
+
if (setting(CARD_LIST_PROPERTIES.slider, readBoolean) !== true) {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const title = resolveCardsSliderTitle(chain, breakpoint);
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
perView:
|
|
359
|
+
setting(CARD_LIST_PROPERTIES.perView, (raw) =>
|
|
360
|
+
readCardsPerView(raw, MAX_CARDS_PER_VIEW)
|
|
361
|
+
) ?? DEFAULT_CARDS_PER_VIEW,
|
|
362
|
+
perViewMobile:
|
|
363
|
+
setting(CARD_LIST_PROPERTIES.perViewMobile, (raw) =>
|
|
364
|
+
readCardsPerView(raw, MAX_CARDS_PER_VIEW_MOBILE)
|
|
365
|
+
) ?? DEFAULT_CARDS_PER_VIEW_MOBILE,
|
|
366
|
+
arrows:
|
|
367
|
+
setting(CARD_LIST_PROPERTIES.sliderArrows, readCardsSliderArrows) ??
|
|
368
|
+
'top-right',
|
|
369
|
+
...(title ? { title } : {})
|
|
370
|
+
};
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const ownScope = (properties: Record<string, unknown> | undefined): Scope => ({
|
|
374
|
+
properties: (properties || {}) as Properties,
|
|
375
|
+
isSection: false,
|
|
376
|
+
declaresHoverScope: false,
|
|
377
|
+
hoverScope: false
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The slider a block defines for itself, read exactly as a section's repeat
|
|
382
|
+
* reads it — for the product grid, which draws its own list.
|
|
383
|
+
*/
|
|
384
|
+
export const resolveOwnCardsSlider = (
|
|
385
|
+
properties: Record<string, unknown> | undefined,
|
|
386
|
+
breakpoint = 'desktop'
|
|
387
|
+
): CardsSliderSettings | undefined =>
|
|
388
|
+
resolveCardsSlider([ownScope(properties)], breakpoint);
|
|
389
|
+
|
|
390
|
+
/** The zoom a block defines for itself — for the grid's built-in card. */
|
|
391
|
+
export const resolveOwnCardZoom = (
|
|
392
|
+
properties: Record<string, unknown> | undefined,
|
|
393
|
+
breakpoint = 'desktop'
|
|
394
|
+
): boolean => resolveCardZoom([ownScope(properties)], breakpoint);
|
|
395
|
+
|
|
165
396
|
const applyToBlocks = (
|
|
166
397
|
blocks: Block[] | undefined,
|
|
167
398
|
scopes: Scope[],
|
|
@@ -196,13 +427,28 @@ const applyToBlocks = (
|
|
|
196
427
|
readSwitch(block.properties?.[HOVER_SCOPE_PROPERTY], breakpoint) === true,
|
|
197
428
|
hoverScope: false
|
|
198
429
|
};
|
|
199
|
-
const
|
|
430
|
+
const chain = [...scopes, own];
|
|
431
|
+
const cardZoom = isCardPictureBlock(block)
|
|
432
|
+
? resolveCardZoom(chain, breakpoint)
|
|
433
|
+
: false;
|
|
434
|
+
const cardsSlider = isCardListRepeat(block)
|
|
435
|
+
? resolveCardsSlider(chain, breakpoint)
|
|
436
|
+
: undefined;
|
|
437
|
+
// The listing's grid views belong to a list that is not sliding.
|
|
438
|
+
const cardsViews =
|
|
439
|
+
isCardListRepeat(block) && !cardsSlider
|
|
440
|
+
? resolveCardsViews(chain, breakpoint)
|
|
441
|
+
: undefined;
|
|
442
|
+
const children = applyToBlocks(block.blocks, chain, breakpoint);
|
|
200
443
|
|
|
201
444
|
next.push({
|
|
202
445
|
...block,
|
|
203
446
|
...(children !== block.blocks ? { blocks: children } : {}),
|
|
204
447
|
...(revealOnHover ? { revealOnHover: true } : {}),
|
|
205
|
-
...(own.hoverScope ? { hoverScope: true } : {})
|
|
448
|
+
...(own.hoverScope ? { hoverScope: true } : {}),
|
|
449
|
+
...(cardZoom ? { cardZoom: true } : {}),
|
|
450
|
+
...(cardsSlider ? { cardsSlider } : {}),
|
|
451
|
+
...(cardsViews ? { cardsViews } : {})
|
|
206
452
|
});
|
|
207
453
|
}
|
|
208
454
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand hook for the colour of a product card's swatch chips (ZERO-5173).
|
|
3
|
+
*
|
|
4
|
+
* A chip draws `item.colors[].color`, which the list's Colour Swatch Mapping
|
|
5
|
+
* fills — the grid's, or the card's over a collection — keyed by the colour's
|
|
6
|
+
* integration value. Commerce sends no hex, and which value means which colour
|
|
7
|
+
* is brand knowledge: GANT's listing turns a colour code into a tone and the
|
|
8
|
+
* tone into its palette's hex. A brand that already resolves it registers that
|
|
9
|
+
* resolver here from its client root (the same ownership pattern as the
|
|
10
|
+
* search-term transform), and every theme card colours its chips the way the
|
|
11
|
+
* brand's own listing does, without a table copied into every list.
|
|
12
|
+
*
|
|
13
|
+
* An authored mapping still wins over the brand's resolver. No registration
|
|
14
|
+
* and no mapping leaves the chip uncoloured, exactly as before.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface SwatchColorRequest {
|
|
18
|
+
/**
|
|
19
|
+
* The colour's key: the option's value, or its label where the payload
|
|
20
|
+
* carries the code there and no value (GANT's `integration_renk`).
|
|
21
|
+
*/
|
|
22
|
+
key: string;
|
|
23
|
+
/** The shopper-facing label. */
|
|
24
|
+
label: string;
|
|
25
|
+
/** The row's product, as commerce sent it. */
|
|
26
|
+
product: Record<string, unknown>;
|
|
27
|
+
/** The product in that colour — the option's — as commerce sent it. */
|
|
28
|
+
variant: Record<string, unknown>;
|
|
29
|
+
/**
|
|
30
|
+
* The products of the list the row is in, the row among them, as commerce
|
|
31
|
+
* sent them — a page's listing, a collection's rows. GANT's listing pairs a
|
|
32
|
+
* colour code with the tone of a product on the page that carries it before
|
|
33
|
+
* it falls back to its catalogue table; this is that page.
|
|
34
|
+
*/
|
|
35
|
+
rows: Record<string, unknown>[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type SwatchColorResolver = (
|
|
39
|
+
request: SwatchColorRequest
|
|
40
|
+
) => string | null | undefined;
|
|
41
|
+
|
|
42
|
+
let resolver: SwatchColorResolver | null = null;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* What a chip may be painted with. The colour lands in the chip's style, so
|
|
46
|
+
* anything that is not one plain colour value is dropped rather than written
|
|
47
|
+
* into a declaration it could break out of.
|
|
48
|
+
*/
|
|
49
|
+
const PLAIN_COLOR =
|
|
50
|
+
/^(#[0-9a-f]{3,8}|(rgb|rgba|hsl|hsla)\([\d\s.,%/]+\)|[a-z]+)$/i;
|
|
51
|
+
|
|
52
|
+
/** Register the brand's resolver. Returns an unregister function. */
|
|
53
|
+
export const registerSwatchColors = (fn: SwatchColorResolver): (() => void) => {
|
|
54
|
+
resolver = fn;
|
|
55
|
+
return () => {
|
|
56
|
+
if (resolver === fn) resolver = null;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The brand's colour for a chip, or undefined when no brand hooked in, it has
|
|
62
|
+
* no colour for this one, or it throws — a brand bug must never break a card.
|
|
63
|
+
*/
|
|
64
|
+
export const resolveSwatchColor = (
|
|
65
|
+
request: SwatchColorRequest
|
|
66
|
+
): string | undefined => {
|
|
67
|
+
if (!resolver) return undefined;
|
|
68
|
+
try {
|
|
69
|
+
const color = resolver(request);
|
|
70
|
+
if (typeof color !== 'string') return undefined;
|
|
71
|
+
const text = color.trim();
|
|
72
|
+
return PLAIN_COLOR.test(text) ? text : undefined;
|
|
73
|
+
} catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Test-only: reset the registry between cases. */
|
|
79
|
+
export const __resetSwatchColorsForTests = (): void => {
|
|
80
|
+
resolver = null;
|
|
81
|
+
};
|