@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.
@@ -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 children = applyToBlocks(block.blocks, [...scopes, own], breakpoint);
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
+ };