@akinon/pz-theme 2.0.103-rc.0 → 2.0.104-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.
@@ -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
+ });