@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.
Files changed (62) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +27 -0
  3. package/readme.md +23 -0
  4. package/src/blocks/accordion-block.tsx +136 -0
  5. package/src/blocks/block-renderer-registry.tsx +77 -0
  6. package/src/blocks/button-block.tsx +593 -0
  7. package/src/blocks/counter-block.tsx +348 -0
  8. package/src/blocks/divider-block.tsx +20 -0
  9. package/src/blocks/embed-block.tsx +208 -0
  10. package/src/blocks/group-block.tsx +116 -0
  11. package/src/blocks/hotspot-block.tsx +147 -0
  12. package/src/blocks/icon-block.tsx +230 -0
  13. package/src/blocks/image-block.tsx +142 -0
  14. package/src/blocks/image-gallery-block.tsx +269 -0
  15. package/src/blocks/input-block.tsx +123 -0
  16. package/src/blocks/link-block.tsx +216 -0
  17. package/src/blocks/lottie-block.tsx +325 -0
  18. package/src/blocks/map-block.tsx +89 -0
  19. package/src/blocks/slider-block.tsx +595 -0
  20. package/src/blocks/tab-block.tsx +10 -0
  21. package/src/blocks/text-block.tsx +52 -0
  22. package/src/blocks/video-block.tsx +122 -0
  23. package/src/components/action-toolbar.tsx +305 -0
  24. package/src/components/designer-overlay.tsx +74 -0
  25. package/src/components/with-designer-features.tsx +142 -0
  26. package/src/dynamic-font-loader.tsx +79 -0
  27. package/src/hooks/use-designer-features.tsx +100 -0
  28. package/src/hooks/use-visibility-context.ts +27 -0
  29. package/src/index.ts +21 -0
  30. package/src/placeholder-registry.ts +31 -0
  31. package/src/sections/before-after-section.tsx +245 -0
  32. package/src/sections/contact-form-section.tsx +564 -0
  33. package/src/sections/countdown-campaign-banner-section.tsx +433 -0
  34. package/src/sections/coupon-banner-section.tsx +710 -0
  35. package/src/sections/divider-section.tsx +62 -0
  36. package/src/sections/featured-product-spotlight-section.tsx +507 -0
  37. package/src/sections/find-in-store-section.tsx +1995 -0
  38. package/src/sections/hover-showcase-section.tsx +326 -0
  39. package/src/sections/image-hotspot-section.tsx +142 -0
  40. package/src/sections/installment-options-section.tsx +1065 -0
  41. package/src/sections/notification-banner-section.tsx +173 -0
  42. package/src/sections/order-tracking-lookup-section.tsx +1379 -0
  43. package/src/sections/posts-slider-section.tsx +472 -0
  44. package/src/sections/pre-order-launch-banner-section.tsx +687 -0
  45. package/src/sections/section-renderer-registry.tsx +89 -0
  46. package/src/sections/section-wrapper.tsx +135 -0
  47. package/src/sections/shipping-threshold-progress-section.tsx +586 -0
  48. package/src/sections/stats-counter-section.tsx +486 -0
  49. package/src/sections/tabs-section.tsx +578 -0
  50. package/src/theme-block.tsx +102 -0
  51. package/src/theme-page-context.tsx +27 -0
  52. package/src/theme-placeholder-client.tsx +218 -0
  53. package/src/theme-placeholder-wrapper.tsx +786 -0
  54. package/src/theme-placeholder.tsx +305 -0
  55. package/src/theme-section.tsx +1241 -0
  56. package/src/theme-settings-context.tsx +13 -0
  57. package/src/utils/index.ts +791 -0
  58. package/src/utils/iterator-utils.test.ts +224 -0
  59. package/src/utils/iterator-utils.ts +617 -0
  60. package/src/utils/page-context-discovery.ts +119 -0
  61. package/src/utils/publish-window.ts +86 -0
  62. package/src/utils/visibility-rules.ts +188 -0
@@ -0,0 +1,617 @@
1
+ import { Block } from '../theme-block';
2
+
3
+ interface BindingContext {
4
+ sectionDataSource?: any;
5
+ pageContext?: Record<string, unknown> | null;
6
+ isDesigner?: boolean;
7
+ }
8
+
9
+ interface IteratorOptions extends BindingContext {
10
+ iteratorBlock: Block;
11
+ forceIteratorCount?: number;
12
+ forceIteratorOffset?: number;
13
+ }
14
+
15
+ const PAGE_CONTEXT_DATA_SOURCE_TYPE = 'page-context';
16
+
17
+ const DEFAULT_BINDING_TARGETS: Record<string, string> = {
18
+ text: 'value',
19
+ image: 'value',
20
+ link: 'href',
21
+ button: 'value',
22
+ input: 'placeholder'
23
+ };
24
+
25
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
26
+ typeof value === 'object' && value !== null && !Array.isArray(value);
27
+
28
+ const getValueAtPath = (source: unknown, path: string): unknown => {
29
+ if (!path.trim()) return source;
30
+
31
+ const tokens = path.match(/[^.[\]]+|\[(\d+)\]/g) || [];
32
+ let value: unknown = source;
33
+
34
+ for (const token of tokens) {
35
+ if (value === undefined || value === null) {
36
+ return undefined;
37
+ }
38
+
39
+ if (token.startsWith('[') && token.endsWith(']')) {
40
+ const index = Number.parseInt(token.slice(1, -1), 10);
41
+ value = Array.isArray(value) ? value[index] : undefined;
42
+ continue;
43
+ }
44
+
45
+ value = (value as Record<string, unknown>)?.[token];
46
+ }
47
+
48
+ return value;
49
+ };
50
+
51
+ const getItemBindingTarget = (block: Block): string => {
52
+ const explicitTarget = block.properties?.dataBindingTarget;
53
+ if (typeof explicitTarget === 'string' && explicitTarget.trim()) {
54
+ return explicitTarget.trim();
55
+ }
56
+
57
+ if (block.properties?.tag === 'a') {
58
+ return 'href';
59
+ }
60
+
61
+ return DEFAULT_BINDING_TARGETS[block.type] || 'value';
62
+ };
63
+
64
+ const getCollectionBindingRoot = (
65
+ sectionDataSource: any,
66
+ isDesigner: boolean
67
+ ): Record<string, unknown> | null => {
68
+ if (!sectionDataSource?.details?.collection) {
69
+ return null;
70
+ }
71
+
72
+ const isEditorMode =
73
+ isDesigner && typeof window !== 'undefined' && window.parent !== window;
74
+ const collection = sectionDataSource.details.collection;
75
+ const candidates = isEditorMode
76
+ ? [collection?.products, collection?.data]
77
+ : [collection?.data, collection?.products];
78
+
79
+ for (const candidate of candidates) {
80
+ if (Array.isArray(candidate)) {
81
+ return {
82
+ products: candidate,
83
+ items: candidate,
84
+ data: candidate
85
+ };
86
+ }
87
+
88
+ if (isRecord(candidate)) {
89
+ return candidate;
90
+ }
91
+ }
92
+
93
+ return null;
94
+ };
95
+
96
+ const getStaticBindingRoot = (
97
+ sectionDataSource: any
98
+ ): Record<string, unknown> | null => {
99
+ const staticSource = sectionDataSource?.details?.static;
100
+ if (!staticSource) {
101
+ return null;
102
+ }
103
+
104
+ return {
105
+ items: staticSource.data || [],
106
+ data: staticSource.data || [],
107
+ name: staticSource.name
108
+ };
109
+ };
110
+
111
+ const getSectionBindingRoot = ({
112
+ sectionDataSource,
113
+ pageContext,
114
+ isDesigner = false
115
+ }: BindingContext): Record<string, unknown> | null => {
116
+ if (
117
+ sectionDataSource?.type === PAGE_CONTEXT_DATA_SOURCE_TYPE &&
118
+ isRecord(pageContext)
119
+ ) {
120
+ return pageContext;
121
+ }
122
+
123
+ const collectionRoot = getCollectionBindingRoot(
124
+ sectionDataSource,
125
+ isDesigner
126
+ );
127
+ if (collectionRoot) {
128
+ return collectionRoot;
129
+ }
130
+
131
+ const staticRoot = getStaticBindingRoot(sectionDataSource);
132
+ if (staticRoot) {
133
+ return staticRoot;
134
+ }
135
+
136
+ return null;
137
+ };
138
+
139
+ const normalizeScalarValue = (
140
+ value: unknown,
141
+ target: string,
142
+ blockType: string
143
+ ): string | undefined => {
144
+ if (value == null) {
145
+ return undefined;
146
+ }
147
+
148
+ if (typeof value === 'string') {
149
+ return value;
150
+ }
151
+
152
+ if (typeof value === 'number' || typeof value === 'boolean') {
153
+ return String(value);
154
+ }
155
+
156
+ if (Array.isArray(value)) {
157
+ return undefined;
158
+ }
159
+
160
+ if (!isRecord(value)) {
161
+ return undefined;
162
+ }
163
+
164
+ if (target === 'href' || target === 'url' || blockType === 'image') {
165
+ const urlCandidate = [value.url, value.absolute_url, value.image].find(
166
+ (candidate) => typeof candidate === 'string'
167
+ );
168
+
169
+ if (typeof urlCandidate === 'string') {
170
+ return urlCandidate;
171
+ }
172
+ }
173
+
174
+ const scalarCandidate = [
175
+ value.title,
176
+ value.name,
177
+ value.label,
178
+ value.value,
179
+ value.alt,
180
+ value.description,
181
+ value.price,
182
+ value.retailPrice,
183
+ value.url,
184
+ value.absolute_url
185
+ ].find(
186
+ (candidate) =>
187
+ typeof candidate === 'string' ||
188
+ typeof candidate === 'number' ||
189
+ typeof candidate === 'boolean'
190
+ );
191
+
192
+ if (
193
+ typeof scalarCandidate === 'string' ||
194
+ typeof scalarCandidate === 'number' ||
195
+ typeof scalarCandidate === 'boolean'
196
+ ) {
197
+ return String(scalarCandidate);
198
+ }
199
+
200
+ return undefined;
201
+ };
202
+
203
+ const parsePriceValue = (value: unknown): number => {
204
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
205
+ if (typeof value !== 'string') return 0;
206
+
207
+ const cleaned = value.trim().replace(/[^\d,.-]/g, '');
208
+ if (!cleaned) return 0;
209
+
210
+ let normalized = cleaned;
211
+ const hasComma = normalized.includes(',');
212
+ const hasDot = normalized.includes('.');
213
+
214
+ if (hasComma && hasDot) {
215
+ const lastComma = normalized.lastIndexOf(',');
216
+ const lastDot = normalized.lastIndexOf('.');
217
+ normalized =
218
+ lastComma > lastDot
219
+ ? normalized.replace(/\./g, '').replace(',', '.')
220
+ : normalized.replace(/,/g, '');
221
+ } else if (hasComma) {
222
+ const unsigned = normalized.replace(/^-/, '');
223
+ const isThousandsPattern = /^\d{1,3}(,\d{3})+$/.test(unsigned);
224
+ normalized = isThousandsPattern
225
+ ? normalized.replace(/,/g, '')
226
+ : normalized.replace(/,/g, '.');
227
+ }
228
+
229
+ const parsed = Number(normalized);
230
+ return Number.isFinite(parsed) ? parsed : 0;
231
+ };
232
+
233
+ const escapeHtml = (value: string): string =>
234
+ value
235
+ .replace(/&/g, '&amp;')
236
+ .replace(/</g, '&lt;')
237
+ .replace(/>/g, '&gt;')
238
+ .replace(/"/g, '&quot;')
239
+ .replace(/'/g, '&#39;');
240
+
241
+ const formatDisplayPrice = (value: unknown): string => {
242
+ if (typeof value === 'number' && Number.isFinite(value)) {
243
+ const hasDecimals = Math.abs(value % 1) > 0.00001;
244
+ return value.toLocaleString('tr-TR', {
245
+ minimumFractionDigits: hasDecimals ? 2 : 0,
246
+ maximumFractionDigits: 2
247
+ });
248
+ }
249
+
250
+ if (typeof value !== 'string') return '0';
251
+ const raw = value.trim();
252
+ if (!raw) return '0';
253
+ if (/[A-Za-z]/.test(raw)) return raw;
254
+
255
+ const parsed = parsePriceValue(raw);
256
+ const hasDecimals = Math.abs(parsed % 1) > 0.00001;
257
+ return parsed.toLocaleString('tr-TR', {
258
+ minimumFractionDigits: hasDecimals ? 2 : 0,
259
+ maximumFractionDigits: 2
260
+ });
261
+ };
262
+
263
+ const normalizeCurrencyLabel = (rawCurrency: unknown): string => {
264
+ if (typeof rawCurrency !== 'string') return 'TL';
265
+ const normalized = rawCurrency.trim().toUpperCase();
266
+ if (!normalized || normalized === 'TRY' || normalized === 'TL') return 'TL';
267
+ return normalized;
268
+ };
269
+
270
+ const getPriceWithCurrency = (value: unknown, currency: unknown): string => {
271
+ const amount = formatDisplayPrice(value);
272
+ const currencyLabel = normalizeCurrencyLabel(currency);
273
+
274
+ if (
275
+ amount.toUpperCase().includes(currencyLabel) ||
276
+ /[A-Za-z]{2,}/.test(amount)
277
+ ) {
278
+ return amount;
279
+ }
280
+
281
+ return `${amount} ${currencyLabel}`;
282
+ };
283
+
284
+ const buildProductPriceHtml = (
285
+ productData: Record<string, unknown>
286
+ ): string => {
287
+ const activePrice =
288
+ ((productData as any)?.active_price as
289
+ | Record<string, unknown>
290
+ | undefined) || {};
291
+ const currentRaw = activePrice?.price ?? (productData as any)?.price;
292
+ const retailRaw =
293
+ activePrice?.retail_price ?? (productData as any)?.retail_price;
294
+ const currency =
295
+ activePrice?.currency_type ??
296
+ (productData as any)?.currency_type ??
297
+ (productData as any)?.currency;
298
+
299
+ const currentNumeric = parsePriceValue(currentRaw);
300
+ const retailNumeric = parsePriceValue(retailRaw);
301
+
302
+ const currentText = escapeHtml(getPriceWithCurrency(currentRaw, currency));
303
+ const retailText = escapeHtml(getPriceWithCurrency(retailRaw, currency));
304
+
305
+ if (retailNumeric > 0 && retailNumeric > currentNumeric) {
306
+ return `<span style="display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap;"><span style='color:#6b7280;text-decoration:line-through;font-weight:400;'>${retailText}</span><span style='font-weight:600;'>${currentText}</span></span>`;
307
+ }
308
+
309
+ return currentText;
310
+ };
311
+
312
+ const isPriceBinding = (dataBinding: string): boolean => {
313
+ const path = dataBinding.replace('item.', '');
314
+ return (
315
+ path === 'active_price.price' || path === 'price' || path === 'retail_price'
316
+ );
317
+ };
318
+
319
+ const applyResolvedBinding = (
320
+ block: Block,
321
+ resolvedValue: unknown,
322
+ bindingPath: string
323
+ ): Block => {
324
+ const target = getItemBindingTarget(block);
325
+ const nextBlock: Block = {
326
+ ...block,
327
+ properties: block.properties ? { ...block.properties } : block.properties
328
+ };
329
+
330
+ if (
331
+ target === 'value' &&
332
+ block.type === 'text' &&
333
+ bindingPath.startsWith('item.') &&
334
+ isPriceBinding(bindingPath) &&
335
+ isRecord(resolvedValue)
336
+ ) {
337
+ nextBlock.value = buildProductPriceHtml(resolvedValue);
338
+ return nextBlock;
339
+ }
340
+
341
+ const normalizedValue = normalizeScalarValue(
342
+ resolvedValue,
343
+ target,
344
+ block.type
345
+ );
346
+ if (normalizedValue === undefined) {
347
+ return nextBlock;
348
+ }
349
+
350
+ switch (target) {
351
+ case 'href':
352
+ nextBlock.properties = {
353
+ ...(nextBlock.properties || {}),
354
+ href: normalizedValue
355
+ };
356
+ return nextBlock;
357
+
358
+ case 'url':
359
+ nextBlock.properties = {
360
+ ...(nextBlock.properties || {}),
361
+ url: normalizedValue
362
+ };
363
+ return nextBlock;
364
+
365
+ case 'placeholder':
366
+ nextBlock.properties = {
367
+ ...(nextBlock.properties || {}),
368
+ placeholder: normalizedValue
369
+ };
370
+ return nextBlock;
371
+
372
+ case 'alt':
373
+ nextBlock.properties = {
374
+ ...(nextBlock.properties || {}),
375
+ alt: normalizedValue
376
+ };
377
+ return nextBlock;
378
+
379
+ default:
380
+ nextBlock.value = normalizedValue;
381
+ return nextBlock;
382
+ }
383
+ };
384
+
385
+ export const resolveBlockBindings = ({
386
+ block,
387
+ sectionDataSource,
388
+ pageContext,
389
+ isDesigner = false
390
+ }: BindingContext & { block: Block }): Block => {
391
+ const bindingRoot = getSectionBindingRoot({
392
+ sectionDataSource,
393
+ pageContext,
394
+ isDesigner
395
+ });
396
+
397
+ const walk = (sourceBlock: Block): Block => {
398
+ const nextBlock: Block = {
399
+ ...sourceBlock,
400
+ properties: sourceBlock.properties
401
+ ? { ...sourceBlock.properties }
402
+ : sourceBlock.properties
403
+ };
404
+
405
+ const bindingPath =
406
+ typeof nextBlock.properties?.dataBinding === 'string'
407
+ ? nextBlock.properties.dataBinding.trim()
408
+ : '';
409
+
410
+ let resolvedBlock = nextBlock;
411
+ if (bindingRoot && bindingPath && !bindingPath.startsWith('item.')) {
412
+ const resolvedValue = getValueAtPath(bindingRoot, bindingPath);
413
+ if (resolvedValue !== undefined) {
414
+ resolvedBlock = applyResolvedBinding(
415
+ nextBlock,
416
+ resolvedValue,
417
+ bindingPath
418
+ );
419
+ }
420
+ }
421
+
422
+ if (resolvedBlock.blocks && resolvedBlock.blocks.length > 0) {
423
+ resolvedBlock.blocks = resolvedBlock.blocks.map(walk);
424
+ }
425
+
426
+ return resolvedBlock;
427
+ };
428
+
429
+ return walk(block);
430
+ };
431
+
432
+ const parsePositiveNumber = (value: unknown, fallback: number): number => {
433
+ if (typeof value === 'number' && Number.isFinite(value)) {
434
+ return Math.max(0, Math.floor(value));
435
+ }
436
+ if (typeof value === 'string') {
437
+ const parsed = Number(value);
438
+ if (Number.isFinite(parsed)) {
439
+ return Math.max(0, Math.floor(parsed));
440
+ }
441
+ }
442
+ return fallback;
443
+ };
444
+
445
+ const getIteratorItems = ({
446
+ sectionDataSource,
447
+ dataPath,
448
+ isDesigner = false,
449
+ pageContext
450
+ }: BindingContext & { dataPath?: string }): Record<string, unknown>[] => {
451
+ const bindingRoot = getSectionBindingRoot({
452
+ sectionDataSource,
453
+ pageContext,
454
+ isDesigner
455
+ });
456
+
457
+ if (!bindingRoot) {
458
+ return [];
459
+ }
460
+
461
+ if (dataPath) {
462
+ const resolvedValue = getValueAtPath(bindingRoot, dataPath);
463
+ return Array.isArray(resolvedValue)
464
+ ? (resolvedValue as Record<string, unknown>[])
465
+ : [];
466
+ }
467
+
468
+ if (Array.isArray((bindingRoot as any).items)) {
469
+ return (bindingRoot as any).items;
470
+ }
471
+
472
+ if (Array.isArray((bindingRoot as any).products)) {
473
+ return (bindingRoot as any).products;
474
+ }
475
+
476
+ return [];
477
+ };
478
+
479
+ const replaceBlockValues = (
480
+ blockToReplace: Block,
481
+ itemData: Record<string, unknown>,
482
+ itemIndex: number
483
+ ): Block => {
484
+ const nextBlock: Block = {
485
+ ...blockToReplace,
486
+ properties: blockToReplace.properties
487
+ ? { ...blockToReplace.properties }
488
+ : blockToReplace.properties
489
+ };
490
+
491
+ const isPlaceholderMode = !itemData || Object.keys(itemData).length === 0;
492
+ const bindingPath =
493
+ typeof nextBlock.properties?.dataBinding === 'string'
494
+ ? nextBlock.properties.dataBinding.trim()
495
+ : '';
496
+
497
+ if (bindingPath && !isPlaceholderMode) {
498
+ const itemBindingPath = bindingPath.replace(/^item\./, '');
499
+ const resolvedValue = getValueAtPath(itemData, itemBindingPath);
500
+
501
+ if (resolvedValue !== undefined) {
502
+ if (
503
+ nextBlock.type === 'text' &&
504
+ bindingPath.startsWith('item.') &&
505
+ isPriceBinding(bindingPath)
506
+ ) {
507
+ nextBlock.value = buildProductPriceHtml(itemData);
508
+ } else {
509
+ return {
510
+ ...applyResolvedBinding(nextBlock, resolvedValue, bindingPath),
511
+ blocks: nextBlock.blocks?.map((childBlock) =>
512
+ replaceBlockValues(childBlock, itemData, itemIndex)
513
+ )
514
+ };
515
+ }
516
+ }
517
+ } else if (isPlaceholderMode && nextBlock.type === 'image') {
518
+ const placeholderImages = [
519
+ '/assets/images/product-placeholder-1.jpg',
520
+ '/assets/images/product-placeholder-2.jpg',
521
+ '/assets/images/product-placeholder-3.jpg',
522
+ '/assets/images/product-placeholder-4.jpg'
523
+ ];
524
+
525
+ const placeholderIndex = itemIndex % placeholderImages.length;
526
+
527
+ if (
528
+ nextBlock.value &&
529
+ typeof nextBlock.value === 'string' &&
530
+ nextBlock.value.includes('product-placeholder')
531
+ ) {
532
+ nextBlock.value = placeholderImages[placeholderIndex];
533
+ }
534
+ }
535
+
536
+ if (nextBlock.blocks && nextBlock.blocks.length > 0) {
537
+ nextBlock.blocks = nextBlock.blocks.map((childBlock) =>
538
+ replaceBlockValues(childBlock, itemData, itemIndex)
539
+ );
540
+ }
541
+
542
+ return nextBlock;
543
+ };
544
+
545
+ export const buildIteratorBlock = ({
546
+ iteratorBlock,
547
+ sectionDataSource,
548
+ pageContext,
549
+ isDesigner = false,
550
+ forceIteratorCount,
551
+ forceIteratorOffset
552
+ }: IteratorOptions): Block => {
553
+ if (
554
+ !iteratorBlock.isIterator ||
555
+ !iteratorBlock.blocks ||
556
+ iteratorBlock.blocks.length === 0
557
+ ) {
558
+ return iteratorBlock;
559
+ }
560
+
561
+ const template = iteratorBlock.blocks[0];
562
+ const dataPath =
563
+ iteratorBlock.iteratorDataPath ||
564
+ iteratorBlock.properties?.iteratorDataPath;
565
+ const items = getIteratorItems({
566
+ sectionDataSource,
567
+ pageContext,
568
+ dataPath,
569
+ isDesigner
570
+ });
571
+
572
+ const useIteratorCount =
573
+ forceIteratorCount !== undefined
574
+ ? true
575
+ : iteratorBlock.properties?.useIteratorCount === true ||
576
+ iteratorBlock.properties?.useIteratorCount === 'true';
577
+ const iteratorCount =
578
+ forceIteratorCount !== undefined
579
+ ? Math.max(1, Math.floor(forceIteratorCount))
580
+ : parsePositiveNumber(iteratorBlock.properties?.iteratorCount, 1);
581
+ const iteratorOffset =
582
+ forceIteratorOffset !== undefined
583
+ ? Math.max(0, Math.floor(forceIteratorOffset))
584
+ : parsePositiveNumber(iteratorBlock.properties?.iteratorOffset, 0);
585
+
586
+ const displayItems =
587
+ items.length > 0
588
+ ? useIteratorCount
589
+ ? items.slice(iteratorOffset, iteratorOffset + iteratorCount)
590
+ : items
591
+ : [];
592
+
593
+ const actualCount =
594
+ displayItems.length > 0 ? displayItems.length : Math.max(iteratorCount, 1);
595
+
596
+ const clonedBlocks = Array.from({ length: actualCount }, (_, index) => {
597
+ const item = displayItems[index] || {};
598
+
599
+ const templateCopy: Block = {
600
+ ...template,
601
+ id: `${template.id}-clone-${index}`,
602
+ properties: template.properties
603
+ ? { ...template.properties }
604
+ : template.properties,
605
+ blocks: template.blocks
606
+ ? template.blocks.map((childBlock) => ({ ...childBlock }))
607
+ : undefined
608
+ };
609
+
610
+ return replaceBlockValues(templateCopy, item, index);
611
+ });
612
+
613
+ return {
614
+ ...iteratorBlock,
615
+ blocks: clonedBlocks
616
+ };
617
+ };