@akinon/pz-theme 2.0.143-rc.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 +8 -35
- 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
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
useValueLocale
|
|
20
20
|
} from '../utils/localized-value';
|
|
21
21
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
22
|
+
import { CardZoom } from '../components/card-zoom';
|
|
22
23
|
|
|
23
24
|
/** "No breakpoint answered", which '' cannot express (see `pickImageSource`). */
|
|
24
25
|
const NO_RESPONSIVE_VALUE = Symbol('no-responsive-value');
|
|
@@ -364,6 +365,29 @@ const ImageBlock = ({
|
|
|
364
365
|
/>
|
|
365
366
|
);
|
|
366
367
|
|
|
368
|
+
/*
|
|
369
|
+
* The card's picture zooming in on hover (ZERO-5173): the property-switch
|
|
370
|
+
* pass wrote `cardZoom` because the card or the list around it asked for
|
|
371
|
+
* it. The frame takes the box the picture had and clips the enlarged one,
|
|
372
|
+
* so the card around it does not move; without the setting the element is
|
|
373
|
+
* the one this block always emitted.
|
|
374
|
+
*/
|
|
375
|
+
const withZoom = (picture: React.ReactElement): React.ReactElement =>
|
|
376
|
+
block.cardZoom && src ? (
|
|
377
|
+
<CardZoom
|
|
378
|
+
style={{
|
|
379
|
+
width: boxWidth,
|
|
380
|
+
height: boxHeight,
|
|
381
|
+
borderRadius: borderRadius ?? '0px',
|
|
382
|
+
aspectRatio: boxAspectRatio
|
|
383
|
+
}}
|
|
384
|
+
>
|
|
385
|
+
{picture}
|
|
386
|
+
</CardZoom>
|
|
387
|
+
) : (
|
|
388
|
+
picture
|
|
389
|
+
);
|
|
390
|
+
|
|
367
391
|
/*
|
|
368
392
|
* No override, or the canvas: emit exactly what this block emitted before
|
|
369
393
|
* this feature existed. Every block published to date takes this path, so its
|
|
@@ -372,7 +396,7 @@ const ImageBlock = ({
|
|
|
372
396
|
* snapshot and every brand's CSS that targets `[data-block-id] > img`.
|
|
373
397
|
*/
|
|
374
398
|
if (!src || isDesigner || pictureSources.length === 0) {
|
|
375
|
-
return content;
|
|
399
|
+
return withZoom(content);
|
|
376
400
|
}
|
|
377
401
|
|
|
378
402
|
/*
|
|
@@ -387,7 +411,7 @@ const ImageBlock = ({
|
|
|
387
411
|
* keeping the element, which is exactly what is wanted: the <source> list is
|
|
388
412
|
* markup, not layout.
|
|
389
413
|
*/
|
|
390
|
-
return (
|
|
414
|
+
return withZoom(
|
|
391
415
|
<picture style={{ display: 'contents' }}>
|
|
392
416
|
{pictureSources.map((source) => (
|
|
393
417
|
<source
|
|
@@ -167,8 +167,11 @@ export const resolveListingContext = (
|
|
|
167
167
|
const source = findListingSource(context);
|
|
168
168
|
if (!source) return null;
|
|
169
169
|
|
|
170
|
-
const
|
|
171
|
-
|
|
170
|
+
const rows = toArray(source.products);
|
|
171
|
+
const products = rows
|
|
172
|
+
.map((entry, index) =>
|
|
173
|
+
normalizeListingProduct(entry, index, { ...options, rows })
|
|
174
|
+
)
|
|
172
175
|
.filter((product): product is ListingProduct => product !== null);
|
|
173
176
|
|
|
174
177
|
const pagination = toRecord(source.pagination);
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* module and the iterator is walked on both sides of hydration.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { resolveSwatchColor } from '../utils/swatch-colors';
|
|
15
16
|
import { formatMoney, MoneyStyleOptions, parseAmount } from './commerce-format';
|
|
16
17
|
|
|
17
18
|
export type { MoneyStyleOptions };
|
|
@@ -60,7 +61,8 @@ export interface ListingProductColor {
|
|
|
60
61
|
/**
|
|
61
62
|
* Hex a circular swatch can be filled with. Commerce sends the colour as a
|
|
62
63
|
* bare value ("kirmizi") and no colour at all, so this is only present when
|
|
63
|
-
* the author mapped that value in `lookups.swatch
|
|
64
|
+
* the author mapped that value in `lookups.swatch`, or the brand resolved it
|
|
65
|
+
* through its registered swatch colours (utils/swatch-colors).
|
|
64
66
|
*/
|
|
65
67
|
color?: string;
|
|
66
68
|
}
|
|
@@ -163,6 +165,8 @@ export interface ListingBasketOffer {
|
|
|
163
165
|
price?: string;
|
|
164
166
|
}
|
|
165
167
|
|
|
168
|
+
export type ListingPriceNote = 'lowest' | 'advantageous';
|
|
169
|
+
|
|
166
170
|
export interface ListingProduct {
|
|
167
171
|
key: string;
|
|
168
172
|
/**
|
|
@@ -214,6 +218,16 @@ export interface ListingProduct {
|
|
|
214
218
|
* the payload would move published cards.
|
|
215
219
|
*/
|
|
216
220
|
discountPercentFromPrevious?: number;
|
|
221
|
+
/**
|
|
222
|
+
* Which price-history caption a discounted row may carry (EU Omnibus; the
|
|
223
|
+
* listing's "Son 10 günün en düşük fiyatı"). `lowest`: the selling price is
|
|
224
|
+
* the lowest of the period — no `previousPrice`, or one at or above it.
|
|
225
|
+
* `advantageous`: commerce's price-history figure is BELOW the selling
|
|
226
|
+
* price, so a "lowest price" claim would be false and the listing says
|
|
227
|
+
* "Avantajlı Fiyat" instead. Absent on a row with no discount. Which words
|
|
228
|
+
* are printed is the card's: it binds a text to each value.
|
|
229
|
+
*/
|
|
230
|
+
priceNote?: ListingPriceNote;
|
|
217
231
|
/**
|
|
218
232
|
* A raw commerce value resolved through the author's lookup table (see
|
|
219
233
|
* `ListingBadgeLookup`). Absent when no table was authored, when the product
|
|
@@ -245,6 +259,12 @@ export interface ListingLocalizationOptions {
|
|
|
245
259
|
* row stays absent, which is the shape the row has always had.
|
|
246
260
|
*/
|
|
247
261
|
lookups?: ListingValueLookups;
|
|
262
|
+
/**
|
|
263
|
+
* The list's rows as commerce sent them, for a brand's swatch colours that
|
|
264
|
+
* read a colour off the row's neighbours (utils/swatch-colors). Absent = the
|
|
265
|
+
* row alone.
|
|
266
|
+
*/
|
|
267
|
+
rows?: unknown[];
|
|
248
268
|
}
|
|
249
269
|
|
|
250
270
|
|
|
@@ -511,7 +531,8 @@ const isColorAttributeKey = (key: string): boolean => {
|
|
|
511
531
|
|
|
512
532
|
const normalizeProductColors = (
|
|
513
533
|
product: UnknownRecord,
|
|
514
|
-
swatch?: Record<string, string
|
|
534
|
+
swatch?: Record<string, string>,
|
|
535
|
+
listRows?: unknown[]
|
|
515
536
|
): ListingProductColor[] | undefined => {
|
|
516
537
|
const groups = toArray(toRecord(product.extra_data).variants);
|
|
517
538
|
// A known key wins over the loose suffix match: a payload carrying both
|
|
@@ -528,6 +549,13 @@ const normalizeProductColors = (
|
|
|
528
549
|
|
|
529
550
|
if (!group) return undefined;
|
|
530
551
|
|
|
552
|
+
const rows = listRows
|
|
553
|
+
? listRows.filter(
|
|
554
|
+
(row): row is UnknownRecord =>
|
|
555
|
+
Boolean(row) && typeof row === 'object' && !Array.isArray(row)
|
|
556
|
+
)
|
|
557
|
+
: [product];
|
|
558
|
+
|
|
531
559
|
// Reshaped once rather than per option, so this table gets the same
|
|
532
560
|
// case-insensitive matching the badge table gets.
|
|
533
561
|
const swatchEntries = swatch
|
|
@@ -557,7 +585,17 @@ const normalizeProductColors = (
|
|
|
557
585
|
// (GANT's `integration_renk` group) carry the code as the option's LABEL
|
|
558
586
|
// and no value at all, so the label is the key there — the same string
|
|
559
587
|
// the swatch table is authored against.
|
|
560
|
-
const hex =
|
|
588
|
+
const hex =
|
|
589
|
+
resolveLookupEntry(swatchEntries, value || label)?.color ??
|
|
590
|
+
// Nothing authored for this colour: the brand's own resolution, the
|
|
591
|
+
// one its listing paints with, when it registered one.
|
|
592
|
+
resolveSwatchColor({
|
|
593
|
+
key: value || label,
|
|
594
|
+
label,
|
|
595
|
+
product,
|
|
596
|
+
variant,
|
|
597
|
+
rows
|
|
598
|
+
});
|
|
561
599
|
|
|
562
600
|
return {
|
|
563
601
|
label,
|
|
@@ -588,6 +626,23 @@ const readInStock = (product: UnknownRecord): boolean | undefined => {
|
|
|
588
626
|
return undefined;
|
|
589
627
|
};
|
|
590
628
|
|
|
629
|
+
/**
|
|
630
|
+
* The listing's price rule (the old storefront's SHOP-88144, shared by its
|
|
631
|
+
* product item, PDP and basket): only a discounted product carries a caption,
|
|
632
|
+
* and it may only claim the lowest price when commerce's price-history figure
|
|
633
|
+
* is not below the selling price.
|
|
634
|
+
*/
|
|
635
|
+
const readPriceNote = (
|
|
636
|
+
hasDiscount: boolean,
|
|
637
|
+
price: number | null,
|
|
638
|
+
previous: number | null
|
|
639
|
+
): ListingPriceNote | undefined => {
|
|
640
|
+
if (!hasDiscount) return undefined;
|
|
641
|
+
return price !== null && previous !== null && price > previous
|
|
642
|
+
? 'advantageous'
|
|
643
|
+
: 'lowest';
|
|
644
|
+
};
|
|
645
|
+
|
|
591
646
|
const normalizeProduct = (
|
|
592
647
|
entry: unknown,
|
|
593
648
|
index: number,
|
|
@@ -631,7 +686,11 @@ const normalizeProduct = (
|
|
|
631
686
|
const rawPreviousPrice = toRecord(product.price_extra_field).previous_price;
|
|
632
687
|
const numericPrevious = parsePriceValue(rawPreviousPrice);
|
|
633
688
|
const numericPk = Number(product.pk);
|
|
634
|
-
const colors = normalizeProductColors(
|
|
689
|
+
const colors = normalizeProductColors(
|
|
690
|
+
product,
|
|
691
|
+
options.lookups?.swatch,
|
|
692
|
+
options.rows
|
|
693
|
+
);
|
|
635
694
|
const attributes = normalizeProductAttributes(product);
|
|
636
695
|
const percentFromRetail = percentOff(numericRetail, numericPrice);
|
|
637
696
|
|
|
@@ -663,6 +722,7 @@ const normalizeProduct = (
|
|
|
663
722
|
discountPercent: percentFromRetail,
|
|
664
723
|
discountPercentFromRetail: percentFromRetail,
|
|
665
724
|
discountPercentFromPrevious: percentOff(numericPrevious, numericPrice),
|
|
725
|
+
priceNote: readPriceNote(hasDiscount, numericPrice, numericPrevious),
|
|
666
726
|
inStock: readInStock(product),
|
|
667
727
|
attributes,
|
|
668
728
|
badge: resolveBadge(attributes, options.lookups?.badge),
|
|
@@ -12,6 +12,13 @@ import {
|
|
|
12
12
|
TABLET_MEDIA_QUERY_COMPACT
|
|
13
13
|
} from '../utils/breakpoints';
|
|
14
14
|
import { neutralizeStyleTagBreakout } from '../utils/css-safety';
|
|
15
|
+
import {
|
|
16
|
+
resolveOwnCardsSlider,
|
|
17
|
+
resolveOwnCardZoom
|
|
18
|
+
} from '../utils/property-switches';
|
|
19
|
+
import { CardListSlider, readListGap } from '../components/card-list-slider';
|
|
20
|
+
import { postBlockSelection } from '../hooks/use-designer-features';
|
|
21
|
+
import { CardZoom } from '../components/card-zoom';
|
|
15
22
|
import ThemeBlock, { Block } from '../theme-block';
|
|
16
23
|
import { hasItemTemplate, ItemTemplateList } from './item-template';
|
|
17
24
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
@@ -284,13 +291,18 @@ const readOption = <T extends string>(
|
|
|
284
291
|
* so a published grid renders exactly what it rendered before.
|
|
285
292
|
*
|
|
286
293
|
* Parts: list, card, image, name, price, sale-price, list-price, badge,
|
|
287
|
-
* density, density-label, density-option, density-icon, insert, empty
|
|
294
|
+
* density, density-label, density-option, density-icon, insert, empty,
|
|
295
|
+
* slider, slider-header, slider-arrow.
|
|
288
296
|
* Properties: columnsDesktop, columnsMobile, limit, showName, nameLines,
|
|
289
297
|
* imageRatio, imageFit, showPrice, showRetailPrice, showDiscountBadge,
|
|
290
298
|
* discountBadgeTemplate, badgePosition, decimals, currencyDisplay,
|
|
291
299
|
* currencyText, currencyPosition, showDensitySwitcher, densityOptions,
|
|
292
300
|
* densityStyle, densityPosition, densityLabel, densityAriaLabel,
|
|
293
|
-
* previewEmptyState, badgeAttribute, badgeMap, swatchMap
|
|
301
|
+
* previewEmptyState, badgeAttribute, badgeMap, swatchMap, cardsSlider,
|
|
302
|
+
* cardsSliderShowTitle, cardsSliderTitle, cardsSliderTitleColor,
|
|
303
|
+
* cardsSliderTitleSize, cardsSliderTitleWeight, cardsSliderTitleFont,
|
|
304
|
+
* cardsSliderTitleTransform, cardsPerView, cardsPerViewMobile,
|
|
305
|
+
* cardsSliderArrows, cardImageHoverZoom.
|
|
294
306
|
* Child properties (on a `grid-insertion` child): insertPosition,
|
|
295
307
|
* insertColumnSpan, insertRowSpan, insertPages.
|
|
296
308
|
*/
|
|
@@ -436,6 +448,15 @@ const ProductGridBlock = ({
|
|
|
436
448
|
const badgePosition = String(
|
|
437
449
|
getResponsiveValue(block.properties?.badgePosition, currentBreakpoint, 'inline')
|
|
438
450
|
);
|
|
451
|
+
// The grid's "Cards:" slider and zoom (ZERO-5173). The slider is the grid's
|
|
452
|
+
// own layout, so it is read here; the zoom reaches a designer-authored card
|
|
453
|
+
// through the property-switch pass, and the built-in card, drawn here,
|
|
454
|
+
// reads it off the grid directly.
|
|
455
|
+
const cardsSlider = resolveOwnCardsSlider(
|
|
456
|
+
block.properties,
|
|
457
|
+
currentBreakpoint
|
|
458
|
+
);
|
|
459
|
+
const builtInZoom = resolveOwnCardZoom(block.properties, currentBreakpoint);
|
|
439
460
|
|
|
440
461
|
const showDensitySwitcher = readToggle(
|
|
441
462
|
block.properties?.showDensitySwitcher,
|
|
@@ -741,7 +762,29 @@ const ProductGridBlock = ({
|
|
|
741
762
|
: null)
|
|
742
763
|
}}
|
|
743
764
|
>
|
|
744
|
-
{product.image ? (
|
|
765
|
+
{product.image && builtInZoom ? (
|
|
766
|
+
<CardZoom
|
|
767
|
+
attributes={{
|
|
768
|
+
'data-product-card-image': '',
|
|
769
|
+
...partAttrs('image')
|
|
770
|
+
}}
|
|
771
|
+
style={
|
|
772
|
+
imageRatio !== 'auto' ? { aspectRatio: imageRatio } : undefined
|
|
773
|
+
}
|
|
774
|
+
>
|
|
775
|
+
<img
|
|
776
|
+
src={product.image}
|
|
777
|
+
alt={product.name}
|
|
778
|
+
loading="lazy"
|
|
779
|
+
style={{
|
|
780
|
+
width: '100%',
|
|
781
|
+
height: imageRatio !== 'auto' ? '100%' : 'auto',
|
|
782
|
+
display: 'block',
|
|
783
|
+
objectFit: imageFit as React.CSSProperties['objectFit']
|
|
784
|
+
}}
|
|
785
|
+
/>
|
|
786
|
+
</CardZoom>
|
|
787
|
+
) : product.image ? (
|
|
745
788
|
<img
|
|
746
789
|
src={product.image}
|
|
747
790
|
alt={product.name}
|
|
@@ -839,6 +882,43 @@ const ProductGridBlock = ({
|
|
|
839
882
|
interleaveCells(builtInCards, insertionTiles, placedInsertions)
|
|
840
883
|
);
|
|
841
884
|
|
|
885
|
+
// ZERO-5173: with the slider on, the same cards — built-in or authored, and
|
|
886
|
+
// any merchandising tile among them — go into one sliding row instead of
|
|
887
|
+
// the grid. The column template, the tiles' cell pins and the density
|
|
888
|
+
// switcher belong to the grid; the row counts its own cards in view per side
|
|
889
|
+
// of the 1024px line, keeps the gap the grid was authored with, and draws
|
|
890
|
+
// the grid's slider title beside its arrows.
|
|
891
|
+
if (cardsSlider) {
|
|
892
|
+
return (
|
|
893
|
+
<div
|
|
894
|
+
data-product-grid=""
|
|
895
|
+
data-product-grid-id={gridId}
|
|
896
|
+
data-product-grid-slider=""
|
|
897
|
+
>
|
|
898
|
+
<CardListSlider
|
|
899
|
+
sliderId={`grid-${gridId}`}
|
|
900
|
+
settings={cardsSlider}
|
|
901
|
+
gap={readListGap(block.styles?.['list/gap'])}
|
|
902
|
+
defaultGap="var(--product-grid-gap, 16px)"
|
|
903
|
+
isDesigner={isDesigner}
|
|
904
|
+
currentBreakpoint={currentBreakpoint}
|
|
905
|
+
trackAttributes={partAttrs('list')}
|
|
906
|
+
onTitleSelect={() =>
|
|
907
|
+
postBlockSelection({
|
|
908
|
+
placeholderId,
|
|
909
|
+
sectionId,
|
|
910
|
+
blockId: block.id,
|
|
911
|
+
blockType: block.type,
|
|
912
|
+
blockLabel: block.label
|
|
913
|
+
})
|
|
914
|
+
}
|
|
915
|
+
>
|
|
916
|
+
{gridChildren}
|
|
917
|
+
</CardListSlider>
|
|
918
|
+
</div>
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
|
|
842
922
|
const grid = (
|
|
843
923
|
<div
|
|
844
924
|
data-product-grid=""
|