@akinon/pz-theme 2.0.103-rc.0 → 2.0.103
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 -33
- package/package.json +4 -4
- package/src/__tests__/collection-row-scope.test.tsx +187 -0
- package/src/__tests__/group-row-styles.test.tsx +110 -0
- package/src/__tests__/theme-typography-vars.test.tsx +125 -0
- package/src/blocks/__tests__/listing-row-swatches.test.ts +111 -0
- package/src/blocks/group-block.tsx +35 -1
- package/src/blocks/item-template-utils.ts +34 -0
- package/src/blocks/item-template.tsx +6 -1
- package/src/blocks/listing-context.ts +17 -622
- package/src/blocks/listing-row.ts +692 -0
- package/src/components/with-designer-features.tsx +6 -0
- package/src/sections/__tests__/posts-slider-row-visibility.test.tsx +147 -0
- package/src/sections/posts-slider-section.tsx +23 -2
- package/src/theme-block.tsx +63 -2
- package/src/theme-placeholder-client.tsx +4 -3
- package/src/theme-placeholder-wrapper.tsx +61 -5
- package/src/utils/__tests__/property-switches.test.tsx +250 -0
- package/src/utils/index.ts +55 -3
- package/src/utils/iterator-price-shapes.test.ts +465 -0
- package/src/utils/iterator-utils.ts +371 -35
- package/src/utils/property-switches.ts +230 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property switches — a block that renders only while a property on an
|
|
3
|
+
* enclosing block says so, and a block revealed on hover of that block.
|
|
4
|
+
*
|
|
5
|
+
* A composed card (ZERO-4829's Product Card) is a tree of plain groups, texts
|
|
6
|
+
* and buttons, and the things a merchandiser wants to switch — the badge, the
|
|
7
|
+
* discount chip, the favourite, the colour swatches — are children several
|
|
8
|
+
* levels down. Without this, "no badge" means finding the right text block in
|
|
9
|
+
* the layer tree and hiding it; "favourite always visible" means editing CSS.
|
|
10
|
+
* With it, the card's ROOT carries a handful of booleans (`showBadge`,
|
|
11
|
+
* `favouriteOnHover`, …) the editor draws as toggles, and each child names the
|
|
12
|
+
* one it answers to:
|
|
13
|
+
*
|
|
14
|
+
* - `properties.visibleWhenProperty: 'showBadge'` — the block is dropped when
|
|
15
|
+
* the nearest enclosing block (or the section) that DEFINES `showBadge`
|
|
16
|
+
* has it off. An ancestor that never set the property does not decide, so
|
|
17
|
+
* a child whose switch nobody authored renders as it always did.
|
|
18
|
+
* - `properties.hoverRevealWhenProperty: 'favouriteOnHover'` — when the
|
|
19
|
+
* nearest definer has it on, the block is marked `revealOnHover` and the
|
|
20
|
+
* element hovering reveals it — the nearest enclosing block that declares
|
|
21
|
+
* `properties.hoverScope: true` (the card's root), else the definer itself
|
|
22
|
+
* — is marked `hoverScope`; the block wrapper writes both as data
|
|
23
|
+
* attributes and HOVER_REVEAL_CSS does the rest on pointer devices only.
|
|
24
|
+
*
|
|
25
|
+
* WHO DEFINES A SWITCH IS A CHAIN. The card's root can carry the switches, but
|
|
26
|
+
* so can whatever encloses the card — the product grid, the Product Cards
|
|
27
|
+
* section — and the nearest definer wins. That is the merchandiser's "hide the
|
|
28
|
+
* favourite on the section, then show it again on this one card": the section
|
|
29
|
+
* decides for every card that says nothing, a card that says something decides
|
|
30
|
+
* for itself. A card therefore leaves its switches UNSET by default and only
|
|
31
|
+
* writes one when the designer picks Show or Hide on it.
|
|
32
|
+
*
|
|
33
|
+
* Dropped, not `hidden`: the canvas keeps hidden blocks mounted so they stay
|
|
34
|
+
* selectable, which is right for the eye icon and wrong for a switch — a
|
|
35
|
+
* merchandiser who turns the badge off has to see it go.
|
|
36
|
+
*
|
|
37
|
+
* Pure, run once over the section tree before anything expands: the iterator
|
|
38
|
+
* copies a template's children as they are, so a child dropped here is absent
|
|
39
|
+
* from every clone and a marked one is marked on every clone.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import type { Block } from '../theme-block';
|
|
43
|
+
import type { Section } from '../theme-section';
|
|
44
|
+
import { BREAKPOINT_FALLBACKS } from './breakpoints';
|
|
45
|
+
|
|
46
|
+
export const VISIBLE_WHEN_PROPERTY = 'visibleWhenProperty';
|
|
47
|
+
export const HOVER_REVEAL_WHEN_PROPERTY = 'hoverRevealWhenProperty';
|
|
48
|
+
/** Declared on the block whose hover reveals the descendants that ask for one. */
|
|
49
|
+
export const HOVER_SCOPE_PROPERTY = 'hoverScope';
|
|
50
|
+
|
|
51
|
+
export const HOVER_SCOPE_ATTRIBUTE = 'data-hover-scope';
|
|
52
|
+
export const REVEAL_ON_HOVER_ATTRIBUTE = 'data-reveal-on-hover';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Desktop widths with a hovering pointer only. Below the desktop breakpoint
|
|
56
|
+
* the block is simply visible — what the brands' own cards do below `lg`, and
|
|
57
|
+
* what a designer previewing the mobile canvas on a desktop machine has to
|
|
58
|
+
* see; a touch tablet wide enough to count as desktop has no hover and shows
|
|
59
|
+
* it too. Keyboard users reach it through focus.
|
|
60
|
+
*/
|
|
61
|
+
export const HOVER_REVEAL_MEDIA = '(min-width: 1024px) and (hover: hover)';
|
|
62
|
+
|
|
63
|
+
export const HOVER_REVEAL_CSS =
|
|
64
|
+
`@media ${HOVER_REVEAL_MEDIA}{` +
|
|
65
|
+
`[${HOVER_SCOPE_ATTRIBUTE}] [${REVEAL_ON_HOVER_ATTRIBUTE}]{opacity:0;transition:opacity .2s}` +
|
|
66
|
+
`[${HOVER_SCOPE_ATTRIBUTE}]:hover [${REVEAL_ON_HOVER_ATTRIBUTE}],` +
|
|
67
|
+
`[${HOVER_SCOPE_ATTRIBUTE}]:focus-within [${REVEAL_ON_HOVER_ATTRIBUTE}],` +
|
|
68
|
+
`[${REVEAL_ON_HOVER_ATTRIBUTE}]:focus-within{opacity:1}` +
|
|
69
|
+
'}';
|
|
70
|
+
|
|
71
|
+
type Properties = Record<string, unknown>;
|
|
72
|
+
|
|
73
|
+
interface Scope {
|
|
74
|
+
properties: Properties;
|
|
75
|
+
/**
|
|
76
|
+
* A section defines switches like any block, but it is not an element a
|
|
77
|
+
* pointer can hover — the section renderer writes no such attribute — so a
|
|
78
|
+
* hover reveal whose only candidate scope is the section stays visible.
|
|
79
|
+
*/
|
|
80
|
+
isSection: boolean;
|
|
81
|
+
/** This block declared itself the hover scope of what it encloses. */
|
|
82
|
+
declaresHoverScope: boolean;
|
|
83
|
+
/** Set while the children are walked; written onto the node afterwards. */
|
|
84
|
+
hoverScope: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A per-breakpoint value, read the way the renderers read one. A local reader
|
|
89
|
+
* rather than `getResponsiveValue` from `./index`: that barrel pulls in the
|
|
90
|
+
* React helpers, and this pass is pure.
|
|
91
|
+
*/
|
|
92
|
+
const readResponsive = (value: unknown, breakpoint: string): unknown => {
|
|
93
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const record = value as Properties;
|
|
98
|
+
const chain = [breakpoint, ...(BREAKPOINT_FALLBACKS[breakpoint] ?? []), 'desktop'];
|
|
99
|
+
|
|
100
|
+
for (const key of chain) {
|
|
101
|
+
if (record[key] !== undefined) return record[key];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return undefined;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A switch as authored: a boolean, the strings a select stores, or nothing.
|
|
109
|
+
* `undefined` means "not defined here", which is what lets the nearest
|
|
110
|
+
* definer be found — an empty string counts as undefined for the same reason
|
|
111
|
+
* (an editor control that was cleared).
|
|
112
|
+
*/
|
|
113
|
+
export const readSwitch = (
|
|
114
|
+
value: unknown,
|
|
115
|
+
breakpoint = 'desktop'
|
|
116
|
+
): boolean | undefined => {
|
|
117
|
+
const raw = readResponsive(value, breakpoint);
|
|
118
|
+
if (raw === undefined || raw === null || raw === '') return undefined;
|
|
119
|
+
if (typeof raw === 'boolean') return raw;
|
|
120
|
+
if (typeof raw === 'number') return raw !== 0;
|
|
121
|
+
|
|
122
|
+
const normalized = String(raw).trim().toLowerCase();
|
|
123
|
+
if (normalized === 'true' || normalized === '1' || normalized === 'yes') {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (normalized === 'false' || normalized === '0' || normalized === 'no') {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return undefined;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const readKey = (properties: Properties | undefined, name: string): string => {
|
|
134
|
+
const raw = readResponsive(properties?.[name], 'desktop');
|
|
135
|
+
return typeof raw === 'string' ? raw.trim() : '';
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** Innermost enclosing scope that defines the switch, or none. */
|
|
139
|
+
const findDefiningScope = (
|
|
140
|
+
scopes: Scope[],
|
|
141
|
+
key: string,
|
|
142
|
+
breakpoint: string
|
|
143
|
+
): { scope: Scope; value: boolean } | null => {
|
|
144
|
+
for (let index = scopes.length - 1; index >= 0; index -= 1) {
|
|
145
|
+
const value = readSwitch(scopes[index].properties[key], breakpoint);
|
|
146
|
+
if (value !== undefined) return { scope: scopes[index], value };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return null;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The element whose hover reveals a child: the nearest enclosing block that
|
|
154
|
+
* declared itself the scope, else the block that defined the switch. Never
|
|
155
|
+
* the section — there is nothing to hover.
|
|
156
|
+
*/
|
|
157
|
+
const findHoverScope = (scopes: Scope[], definer: Scope): Scope | null => {
|
|
158
|
+
for (let index = scopes.length - 1; index >= 0; index -= 1) {
|
|
159
|
+
if (scopes[index].declaresHoverScope) return scopes[index];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return definer.isSection ? null : definer;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const applyToBlocks = (
|
|
166
|
+
blocks: Block[] | undefined,
|
|
167
|
+
scopes: Scope[],
|
|
168
|
+
breakpoint: string
|
|
169
|
+
): Block[] | undefined => {
|
|
170
|
+
if (!blocks || blocks.length === 0) return blocks;
|
|
171
|
+
|
|
172
|
+
const next: Block[] = [];
|
|
173
|
+
|
|
174
|
+
for (const block of blocks) {
|
|
175
|
+
const visibleKey = readKey(block.properties, VISIBLE_WHEN_PROPERTY);
|
|
176
|
+
if (visibleKey) {
|
|
177
|
+
const definer = findDefiningScope(scopes, visibleKey, breakpoint);
|
|
178
|
+
if (definer && !definer.value) continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let revealOnHover = false;
|
|
182
|
+
const hoverKey = readKey(block.properties, HOVER_REVEAL_WHEN_PROPERTY);
|
|
183
|
+
if (hoverKey) {
|
|
184
|
+
const definer = findDefiningScope(scopes, hoverKey, breakpoint);
|
|
185
|
+
const scope = definer?.value ? findHoverScope(scopes, definer.scope) : null;
|
|
186
|
+
if (scope) {
|
|
187
|
+
revealOnHover = true;
|
|
188
|
+
scope.hoverScope = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const own: Scope = {
|
|
193
|
+
properties: (block.properties || {}) as Properties,
|
|
194
|
+
isSection: false,
|
|
195
|
+
declaresHoverScope:
|
|
196
|
+
readSwitch(block.properties?.[HOVER_SCOPE_PROPERTY], breakpoint) === true,
|
|
197
|
+
hoverScope: false
|
|
198
|
+
};
|
|
199
|
+
const children = applyToBlocks(block.blocks, [...scopes, own], breakpoint);
|
|
200
|
+
|
|
201
|
+
next.push({
|
|
202
|
+
...block,
|
|
203
|
+
...(children !== block.blocks ? { blocks: children } : {}),
|
|
204
|
+
...(revealOnHover ? { revealOnHover: true } : {}),
|
|
205
|
+
...(own.hoverScope ? { hoverScope: true } : {})
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return next;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The switches applied to every section. Each section is its own outermost
|
|
214
|
+
* scope, so a section property can drive a visibility switch too.
|
|
215
|
+
*/
|
|
216
|
+
export const applyPropertySwitchesToSections = (
|
|
217
|
+
sections: Section[],
|
|
218
|
+
breakpoint = 'desktop'
|
|
219
|
+
): Section[] =>
|
|
220
|
+
sections.map((section) => {
|
|
221
|
+
const scope: Scope = {
|
|
222
|
+
properties: (section.properties || {}) as Properties,
|
|
223
|
+
isSection: true,
|
|
224
|
+
declaresHoverScope: false,
|
|
225
|
+
hoverScope: false
|
|
226
|
+
};
|
|
227
|
+
const blocks = applyToBlocks(section.blocks, [scope], breakpoint);
|
|
228
|
+
|
|
229
|
+
return blocks === section.blocks ? section : { ...section, blocks: blocks ?? [] };
|
|
230
|
+
});
|