@akinon/pz-theme 2.0.0-beta.21
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 +17 -0
- package/package.json +27 -0
- package/readme.md +23 -0
- package/src/blocks/accordion-block.tsx +136 -0
- package/src/blocks/block-renderer-registry.tsx +77 -0
- package/src/blocks/button-block.tsx +593 -0
- package/src/blocks/counter-block.tsx +348 -0
- package/src/blocks/divider-block.tsx +20 -0
- package/src/blocks/embed-block.tsx +208 -0
- package/src/blocks/group-block.tsx +116 -0
- package/src/blocks/hotspot-block.tsx +147 -0
- package/src/blocks/icon-block.tsx +230 -0
- package/src/blocks/image-block.tsx +142 -0
- package/src/blocks/image-gallery-block.tsx +269 -0
- package/src/blocks/input-block.tsx +123 -0
- package/src/blocks/link-block.tsx +216 -0
- package/src/blocks/lottie-block.tsx +325 -0
- package/src/blocks/map-block.tsx +89 -0
- package/src/blocks/slider-block.tsx +595 -0
- package/src/blocks/tab-block.tsx +10 -0
- package/src/blocks/text-block.tsx +52 -0
- package/src/blocks/video-block.tsx +122 -0
- package/src/components/action-toolbar.tsx +305 -0
- package/src/components/designer-overlay.tsx +74 -0
- package/src/components/with-designer-features.tsx +142 -0
- package/src/dynamic-font-loader.tsx +79 -0
- package/src/hooks/use-designer-features.tsx +100 -0
- package/src/hooks/use-visibility-context.ts +27 -0
- package/src/index.ts +21 -0
- package/src/placeholder-registry.ts +31 -0
- package/src/sections/before-after-section.tsx +245 -0
- package/src/sections/contact-form-section.tsx +564 -0
- package/src/sections/countdown-campaign-banner-section.tsx +433 -0
- package/src/sections/coupon-banner-section.tsx +710 -0
- package/src/sections/divider-section.tsx +62 -0
- package/src/sections/featured-product-spotlight-section.tsx +507 -0
- package/src/sections/find-in-store-section.tsx +1995 -0
- package/src/sections/hover-showcase-section.tsx +326 -0
- package/src/sections/image-hotspot-section.tsx +142 -0
- package/src/sections/installment-options-section.tsx +1065 -0
- package/src/sections/notification-banner-section.tsx +173 -0
- package/src/sections/order-tracking-lookup-section.tsx +1379 -0
- package/src/sections/posts-slider-section.tsx +472 -0
- package/src/sections/pre-order-launch-banner-section.tsx +687 -0
- package/src/sections/section-renderer-registry.tsx +89 -0
- package/src/sections/section-wrapper.tsx +135 -0
- package/src/sections/shipping-threshold-progress-section.tsx +586 -0
- package/src/sections/stats-counter-section.tsx +486 -0
- package/src/sections/tabs-section.tsx +578 -0
- package/src/theme-block.tsx +102 -0
- package/src/theme-page-context.tsx +27 -0
- package/src/theme-placeholder-client.tsx +218 -0
- package/src/theme-placeholder-wrapper.tsx +786 -0
- package/src/theme-placeholder.tsx +305 -0
- package/src/theme-section.tsx +1241 -0
- package/src/theme-settings-context.tsx +13 -0
- package/src/utils/index.ts +791 -0
- package/src/utils/iterator-utils.test.ts +224 -0
- package/src/utils/iterator-utils.ts +617 -0
- package/src/utils/page-context-discovery.ts +119 -0
- package/src/utils/publish-window.ts +86 -0
- package/src/utils/visibility-rules.ts +188 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
type DiscoveryValueType =
|
|
2
|
+
| 'string'
|
|
3
|
+
| 'number'
|
|
4
|
+
| 'boolean'
|
|
5
|
+
| 'null'
|
|
6
|
+
| 'object'
|
|
7
|
+
| 'array';
|
|
8
|
+
|
|
9
|
+
export interface PageContextDiscoveryEntry {
|
|
10
|
+
path: string;
|
|
11
|
+
kind: 'scalar' | 'array';
|
|
12
|
+
valueType: DiscoveryValueType;
|
|
13
|
+
sample?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PageContextDiscovery {
|
|
17
|
+
roots: string[];
|
|
18
|
+
entries: PageContextDiscoveryEntry[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const MAX_DISCOVERY_ENTRIES = 250;
|
|
22
|
+
const MAX_SAMPLE_LENGTH = 60;
|
|
23
|
+
|
|
24
|
+
const getValueType = (value: unknown): DiscoveryValueType => {
|
|
25
|
+
if (value === null) return 'null';
|
|
26
|
+
if (Array.isArray(value)) return 'array';
|
|
27
|
+
if (typeof value === 'string') return 'string';
|
|
28
|
+
if (typeof value === 'number') return 'number';
|
|
29
|
+
if (typeof value === 'boolean') return 'boolean';
|
|
30
|
+
return 'object';
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const formatSample = (value: unknown) => {
|
|
34
|
+
if (value === null || value === undefined) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
return `${value.length} item${value.length === 1 ? '' : 's'}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (typeof value === 'object') {
|
|
43
|
+
return `${Object.keys(value as Record<string, unknown>).length} field${
|
|
44
|
+
Object.keys(value as Record<string, unknown>).length === 1 ? '' : 's'
|
|
45
|
+
}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const stringValue = String(value);
|
|
49
|
+
return stringValue.length > MAX_SAMPLE_LENGTH
|
|
50
|
+
? `${stringValue.slice(0, MAX_SAMPLE_LENGTH - 3)}...`
|
|
51
|
+
: stringValue;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const createPageContextDiscovery = (
|
|
55
|
+
input?: Record<string, unknown> | null
|
|
56
|
+
): PageContextDiscovery => {
|
|
57
|
+
if (!input) {
|
|
58
|
+
return {
|
|
59
|
+
roots: [],
|
|
60
|
+
entries: []
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const entries: PageContextDiscoveryEntry[] = [];
|
|
65
|
+
const seen = new Set<string>();
|
|
66
|
+
|
|
67
|
+
const pushEntry = (entry: PageContextDiscoveryEntry) => {
|
|
68
|
+
if (!entry.path || seen.has(entry.path) || entries.length >= MAX_DISCOVERY_ENTRIES) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
seen.add(entry.path);
|
|
73
|
+
entries.push(entry);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const walk = (value: unknown, path: string) => {
|
|
77
|
+
if (!path || entries.length >= MAX_DISCOVERY_ENTRIES) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (Array.isArray(value)) {
|
|
82
|
+
pushEntry({
|
|
83
|
+
path,
|
|
84
|
+
kind: 'array',
|
|
85
|
+
valueType: 'array',
|
|
86
|
+
sample: formatSample(value)
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (value.length > 0) {
|
|
90
|
+
walk(value[0], `${path}[]`);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (value && typeof value === 'object') {
|
|
96
|
+
Object.entries(value as Record<string, unknown>).forEach(([key, child]) => {
|
|
97
|
+
const nextPath = path ? `${path}.${key}` : key;
|
|
98
|
+
walk(child, nextPath);
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
pushEntry({
|
|
104
|
+
path,
|
|
105
|
+
kind: 'scalar',
|
|
106
|
+
valueType: getValueType(value),
|
|
107
|
+
sample: formatSample(value)
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
Object.entries(input).forEach(([rootKey, value]) => {
|
|
112
|
+
walk(value, rootKey);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
roots: Object.keys(input),
|
|
117
|
+
entries
|
|
118
|
+
};
|
|
119
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export interface PublishWindowValue {
|
|
2
|
+
enabled?: boolean;
|
|
3
|
+
startAt?: string;
|
|
4
|
+
endAt?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type PublishWindowStatus =
|
|
8
|
+
| 'inactive'
|
|
9
|
+
| 'scheduled'
|
|
10
|
+
| 'active'
|
|
11
|
+
| 'expired';
|
|
12
|
+
|
|
13
|
+
const isPublishWindowShape = (value: unknown): value is PublishWindowValue => {
|
|
14
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const candidate = value as Record<string, unknown>;
|
|
19
|
+
return (
|
|
20
|
+
candidate.enabled !== undefined ||
|
|
21
|
+
candidate.startAt !== undefined ||
|
|
22
|
+
candidate.endAt !== undefined
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const normalizePublishWindowValue = (
|
|
27
|
+
value: unknown
|
|
28
|
+
): PublishWindowValue => {
|
|
29
|
+
if (isPublishWindowShape(value)) {
|
|
30
|
+
return {
|
|
31
|
+
enabled: value.enabled === true,
|
|
32
|
+
startAt: typeof value.startAt === 'string' ? value.startAt : '',
|
|
33
|
+
endAt: typeof value.endAt === 'string' ? value.endAt : ''
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
38
|
+
const responsiveValue = value as Record<string, unknown>;
|
|
39
|
+
const nestedValue =
|
|
40
|
+
responsiveValue.desktop ||
|
|
41
|
+
responsiveValue.mobile ||
|
|
42
|
+
responsiveValue.tablet;
|
|
43
|
+
|
|
44
|
+
if (nestedValue && nestedValue !== value) {
|
|
45
|
+
return normalizePublishWindowValue(nestedValue);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
enabled: false,
|
|
51
|
+
startAt: '',
|
|
52
|
+
endAt: ''
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const parsePublishTime = (value?: string): number | null => {
|
|
57
|
+
if (!value) return null;
|
|
58
|
+
const parsed = Date.parse(value);
|
|
59
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const getPublishWindowStatus = (
|
|
63
|
+
value: unknown,
|
|
64
|
+
now: Date = new Date()
|
|
65
|
+
): PublishWindowStatus => {
|
|
66
|
+
const publishWindow = normalizePublishWindowValue(value);
|
|
67
|
+
|
|
68
|
+
if (!publishWindow.enabled) return 'inactive';
|
|
69
|
+
|
|
70
|
+
const currentTime = now.getTime();
|
|
71
|
+
const startAt = parsePublishTime(publishWindow.startAt);
|
|
72
|
+
const endAt = parsePublishTime(publishWindow.endAt);
|
|
73
|
+
|
|
74
|
+
if (startAt !== null && currentTime < startAt) return 'scheduled';
|
|
75
|
+
if (endAt !== null && currentTime > endAt) return 'expired';
|
|
76
|
+
|
|
77
|
+
return 'active';
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const isPublishWindowVisible = (
|
|
81
|
+
value: unknown,
|
|
82
|
+
now: Date = new Date()
|
|
83
|
+
): boolean => {
|
|
84
|
+
const status = getPublishWindowStatus(value, now);
|
|
85
|
+
return status === 'inactive' || status === 'active';
|
|
86
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { Block } from '../theme-block';
|
|
2
|
+
import { Section } from '../theme-section';
|
|
3
|
+
|
|
4
|
+
export type VisibilityAuthState = 'all' | 'authenticated' | 'guest';
|
|
5
|
+
export type VisibilityPathMatchType =
|
|
6
|
+
| 'any'
|
|
7
|
+
| 'equals'
|
|
8
|
+
| 'contains'
|
|
9
|
+
| 'startsWith';
|
|
10
|
+
|
|
11
|
+
export interface VisibilityRules {
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
authState?: VisibilityAuthState;
|
|
14
|
+
allowedBreakpoints?: string[];
|
|
15
|
+
allowedLocales?: string[];
|
|
16
|
+
allowedCurrencies?: string[];
|
|
17
|
+
pathnameMatchType?: VisibilityPathMatchType;
|
|
18
|
+
pathnameValue?: string;
|
|
19
|
+
queryParamKey?: string;
|
|
20
|
+
queryParamValue?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface VisibilityRuleContext {
|
|
24
|
+
authState: 'authenticated' | 'guest' | 'loading';
|
|
25
|
+
breakpoint: string;
|
|
26
|
+
locale: string;
|
|
27
|
+
currency: string;
|
|
28
|
+
pathname: string;
|
|
29
|
+
searchParams?: {
|
|
30
|
+
get: (key: string) => string | null;
|
|
31
|
+
} | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const normalizeStringArray = (value: unknown): string[] => {
|
|
35
|
+
if (!Array.isArray(value)) return [];
|
|
36
|
+
return value
|
|
37
|
+
.map(item => String(item || '').trim())
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const normalizeVisibilityRules = (value: unknown): VisibilityRules => {
|
|
42
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
43
|
+
return {
|
|
44
|
+
enabled: false,
|
|
45
|
+
authState: 'all',
|
|
46
|
+
allowedBreakpoints: [],
|
|
47
|
+
allowedLocales: [],
|
|
48
|
+
allowedCurrencies: [],
|
|
49
|
+
pathnameMatchType: 'any',
|
|
50
|
+
pathnameValue: '',
|
|
51
|
+
queryParamKey: '',
|
|
52
|
+
queryParamValue: ''
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const rules = value as Record<string, unknown>;
|
|
57
|
+
return {
|
|
58
|
+
enabled: rules.enabled === true,
|
|
59
|
+
authState:
|
|
60
|
+
rules.authState === 'authenticated' || rules.authState === 'guest'
|
|
61
|
+
? rules.authState
|
|
62
|
+
: 'all',
|
|
63
|
+
allowedBreakpoints: normalizeStringArray(rules.allowedBreakpoints),
|
|
64
|
+
allowedLocales: normalizeStringArray(rules.allowedLocales),
|
|
65
|
+
allowedCurrencies: normalizeStringArray(rules.allowedCurrencies).map(item =>
|
|
66
|
+
item.toUpperCase()
|
|
67
|
+
),
|
|
68
|
+
pathnameMatchType:
|
|
69
|
+
rules.pathnameMatchType === 'equals' ||
|
|
70
|
+
rules.pathnameMatchType === 'contains' ||
|
|
71
|
+
rules.pathnameMatchType === 'startsWith'
|
|
72
|
+
? rules.pathnameMatchType
|
|
73
|
+
: 'any',
|
|
74
|
+
pathnameValue: String(rules.pathnameValue || '').trim(),
|
|
75
|
+
queryParamKey: String(rules.queryParamKey || '').trim(),
|
|
76
|
+
queryParamValue: String(rules.queryParamValue || '').trim()
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const evaluateVisibilityRules = (
|
|
81
|
+
rawRules: unknown,
|
|
82
|
+
context: VisibilityRuleContext
|
|
83
|
+
): boolean => {
|
|
84
|
+
const rules = normalizeVisibilityRules(rawRules);
|
|
85
|
+
if (!rules.enabled) return true;
|
|
86
|
+
|
|
87
|
+
if (rules.authState && rules.authState !== 'all') {
|
|
88
|
+
if (context.authState === 'loading') return false;
|
|
89
|
+
if (rules.authState === 'authenticated') {
|
|
90
|
+
if (context.authState !== 'authenticated') return false;
|
|
91
|
+
} else if (context.authState !== 'guest') {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
rules.allowedBreakpoints &&
|
|
98
|
+
rules.allowedBreakpoints.length > 0 &&
|
|
99
|
+
!rules.allowedBreakpoints.includes(context.breakpoint)
|
|
100
|
+
) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (
|
|
105
|
+
rules.allowedLocales &&
|
|
106
|
+
rules.allowedLocales.length > 0 &&
|
|
107
|
+
!rules.allowedLocales.includes(context.locale)
|
|
108
|
+
) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
rules.allowedCurrencies &&
|
|
114
|
+
rules.allowedCurrencies.length > 0 &&
|
|
115
|
+
!rules.allowedCurrencies.includes(String(context.currency || '').toUpperCase())
|
|
116
|
+
) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const pathname = String(context.pathname || '');
|
|
121
|
+
if (rules.pathnameMatchType && rules.pathnameMatchType !== 'any') {
|
|
122
|
+
const ruleValue = String(rules.pathnameValue || '').trim();
|
|
123
|
+
if (!ruleValue) return false;
|
|
124
|
+
|
|
125
|
+
if (rules.pathnameMatchType === 'equals' && pathname !== ruleValue) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (
|
|
130
|
+
rules.pathnameMatchType === 'contains' &&
|
|
131
|
+
!pathname.includes(ruleValue)
|
|
132
|
+
) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (
|
|
137
|
+
rules.pathnameMatchType === 'startsWith' &&
|
|
138
|
+
!pathname.startsWith(ruleValue)
|
|
139
|
+
) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (rules.queryParamKey) {
|
|
145
|
+
const actualValue = context.searchParams?.get(rules.queryParamKey) ?? null;
|
|
146
|
+
if (actualValue === null) return false;
|
|
147
|
+
if (rules.queryParamValue && actualValue !== rules.queryParamValue) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return true;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const applyBlockVisibility = (
|
|
156
|
+
block: Block,
|
|
157
|
+
context: VisibilityRuleContext
|
|
158
|
+
): Block => {
|
|
159
|
+
const blocks = block.blocks?.map(child => applyBlockVisibility(child, context));
|
|
160
|
+
const isVisibleByRules = evaluateVisibilityRules(
|
|
161
|
+
block.properties?.visibilityRules,
|
|
162
|
+
context
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
...block,
|
|
167
|
+
hidden: Boolean(block.hidden) || !isVisibleByRules,
|
|
168
|
+
blocks
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const applyVisibilityRulesToSections = (
|
|
173
|
+
sections: Section[],
|
|
174
|
+
context: VisibilityRuleContext
|
|
175
|
+
): Section[] =>
|
|
176
|
+
sections.map(section => {
|
|
177
|
+
const blocks = section.blocks.map(block => applyBlockVisibility(block, context));
|
|
178
|
+
const isVisibleByRules = evaluateVisibilityRules(
|
|
179
|
+
section.properties?.visibilityRules,
|
|
180
|
+
context
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
...section,
|
|
185
|
+
hidden: Boolean(section.hidden) || !isVisibleByRules,
|
|
186
|
+
blocks
|
|
187
|
+
};
|
|
188
|
+
});
|