@akinon/pz-theme 2.0.35-beta.0 → 2.0.35

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 (39) hide show
  1. package/CHANGELOG.md +9 -3
  2. package/package.json +4 -3
  3. package/src/__tests__/theme-contract.test.ts +130 -0
  4. package/src/blocks/__tests__/text-block.test.tsx +8 -1
  5. package/src/blocks/__tests__/video-block.test.tsx +14 -7
  6. package/src/blocks/counter-block.tsx +44 -56
  7. package/src/blocks/group-block.tsx +23 -22
  8. package/src/blocks/hotspot-block.tsx +171 -80
  9. package/src/blocks/input-block.tsx +15 -17
  10. package/src/blocks/link-block.tsx +18 -21
  11. package/src/blocks/slider-block.tsx +15 -12
  12. package/src/components/with-designer-features.tsx +2 -0
  13. package/src/editor-bridge.ts +160 -0
  14. package/src/hooks/use-designer-features.tsx +30 -37
  15. package/src/placeholder-registry.ts +5 -7
  16. package/src/sections/__tests__/countdown-campaign-banner-section.test.tsx +8 -4
  17. package/src/sections/__tests__/coupon-banner-section.test.tsx +4 -2
  18. package/src/sections/before-after-section.tsx +189 -134
  19. package/src/sections/contact-form-section.tsx +185 -60
  20. package/src/sections/countdown-campaign-banner-section.tsx +73 -27
  21. package/src/sections/coupon-banner-section.tsx +152 -48
  22. package/src/sections/featured-product-spotlight-section.tsx +67 -66
  23. package/src/sections/find-in-store-section.tsx +37 -25
  24. package/src/sections/hover-showcase-section.tsx +221 -89
  25. package/src/sections/image-hotspot-section.tsx +208 -94
  26. package/src/sections/installment-options-section.tsx +268 -259
  27. package/src/sections/notification-banner-section.tsx +111 -25
  28. package/src/sections/order-tracking-lookup-section.tsx +272 -247
  29. package/src/sections/posts-slider-section.tsx +17 -16
  30. package/src/sections/pre-order-launch-banner-section.tsx +31 -50
  31. package/src/sections/section-wrapper.tsx +9 -10
  32. package/src/sections/shipping-threshold-progress-section.tsx +72 -29
  33. package/src/sections/stats-counter-section.tsx +68 -30
  34. package/src/sections/tabs-section.tsx +124 -147
  35. package/src/theme-placeholder-client.tsx +11 -14
  36. package/src/theme-placeholder-wrapper.tsx +42 -51
  37. package/src/theme-section.tsx +27 -34
  38. package/src/utils/__tests__/transform-styles.test.ts +97 -0
  39. package/src/utils/index.ts +9 -0
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { useCallback } from 'react';
4
4
 
5
+ import { postToEditor } from '../editor-bridge';
6
+ import { PreviewToEditorMessageType } from '@akinon/theme-contract';
5
7
  interface BlockInfo {
6
8
  id: string;
7
9
  type: string;
@@ -30,19 +32,16 @@ export function useDesignerFeatures({
30
32
  e.stopPropagation();
31
33
 
32
34
  if (window.parent) {
33
- window.parent.postMessage(
34
- {
35
- type: 'SELECT_BLOCK',
36
- data: {
37
- placeholderId,
38
- sectionId,
39
- blockId,
40
- blockType: blockInfo?.type,
41
- blockLabel: blockInfo?.label
42
- }
43
- },
44
- '*'
45
- );
35
+ postToEditor({
36
+ type: 'SELECT_BLOCK',
37
+ data: {
38
+ placeholderId,
39
+ sectionId,
40
+ blockId,
41
+ blockType: blockInfo?.type,
42
+ blockLabel: blockInfo?.label
43
+ }
44
+ });
46
45
  }
47
46
  }
48
47
  },
@@ -50,19 +49,16 @@ export function useDesignerFeatures({
50
49
  );
51
50
 
52
51
  const createActionHandler = useCallback(
53
- (actionType: string) => () => {
52
+ (actionType: PreviewToEditorMessageType) => () => {
54
53
  if (window.parent) {
55
- window.parent.postMessage(
56
- {
57
- type: actionType,
58
- data: {
59
- placeholderId,
60
- sectionId,
61
- blockId
62
- }
63
- },
64
- '*'
65
- );
54
+ postToEditor({
55
+ type: actionType,
56
+ data: {
57
+ placeholderId,
58
+ sectionId,
59
+ blockId
60
+ }
61
+ });
66
62
  }
67
63
  },
68
64
  [blockId, placeholderId, sectionId]
@@ -71,18 +67,15 @@ export function useDesignerFeatures({
71
67
  const handleRename = useCallback(
72
68
  (newLabel: string) => {
73
69
  if (window.parent) {
74
- window.parent.postMessage(
75
- {
76
- type: 'RENAME_BLOCK',
77
- data: {
78
- placeholderId,
79
- sectionId,
80
- blockId,
81
- label: newLabel
82
- }
83
- },
84
- '*'
85
- );
70
+ postToEditor({
71
+ type: 'RENAME_BLOCK',
72
+ data: {
73
+ placeholderId,
74
+ sectionId,
75
+ blockId,
76
+ label: newLabel
77
+ }
78
+ });
86
79
  }
87
80
  },
88
81
  [blockId, placeholderId, sectionId]
@@ -1,3 +1,4 @@
1
+ import { postToEditor } from './editor-bridge';
1
2
  const placeholderRegistry = new Set<string>();
2
3
  let registryTimeout: NodeJS.Timeout | null = null;
3
4
 
@@ -15,13 +16,10 @@ export const registerPlaceholder = (slug: string) => {
15
16
  window.self !== window.top
16
17
  ) {
17
18
  const availablePlaceholders = Array.from(placeholderRegistry);
18
- window.parent.postMessage(
19
- {
20
- type: 'PLACEHOLDERS_AVAILABLE',
21
- data: { placeholders: availablePlaceholders }
22
- },
23
- '*'
24
- );
19
+ postToEditor({
20
+ type: 'PLACEHOLDERS_AVAILABLE',
21
+ data: { placeholders: availablePlaceholders }
22
+ });
25
23
  }
26
24
  }, 100);
27
25
  };
@@ -25,17 +25,19 @@ const renderSection = (props: any = {}) => {
25
25
  let utils: ReturnType<typeof render>;
26
26
  act(() => {
27
27
  utils = render(
28
- <CountdownCampaignBannerSection section={makeSection(props.section)} {...props} />
28
+ <CountdownCampaignBannerSection
29
+ section={makeSection(props.section)}
30
+ {...props}
31
+ />
29
32
  );
30
33
  });
31
- // @ts-expect-error assigned inside act
32
34
  return utils;
33
35
  };
34
36
 
35
37
  // Fixed "now" so countdown math is deterministic.
36
38
  const NOW = new Date('2026-06-26T00:00:00.000Z').getTime();
37
39
  // now + 1d 2h 3m 4s
38
- const DELTA_MS = ((((1 * 24 + 2) * 60) + 3) * 60 + 4) * 1000;
40
+ const DELTA_MS = (((1 * 24 + 2) * 60 + 3) * 60 + 4) * 1000;
39
41
  const END_MS = NOW + DELTA_MS;
40
42
 
41
43
  beforeEach(() => {
@@ -92,7 +94,9 @@ describe('CountdownCampaignBannerSection - parseEndDate', () => {
92
94
 
93
95
  describe('CountdownCampaignBannerSection - expired', () => {
94
96
  it('renders the default expired message for a past date', () => {
95
- renderSection({ section: { properties: { 'end-date': '2000-01-01T00:00:00.000Z' } } });
97
+ renderSection({
98
+ section: { properties: { 'end-date': '2000-01-01T00:00:00.000Z' } }
99
+ });
96
100
 
97
101
  expect(screen.getByText('Campaign ended')).toBeInTheDocument();
98
102
  // No timer cells when expired.
@@ -42,7 +42,7 @@ const renderSection = (section: AnyBlock, props: AnyBlock = {}) =>
42
42
  <CouponBannerSection
43
43
  section={section as never}
44
44
  currentBreakpoint="desktop"
45
- {...(props as never)}
45
+ {...(props as object)}
46
46
  />
47
47
  );
48
48
 
@@ -62,7 +62,9 @@ describe('CouponBannerSection — role resolution', () => {
62
62
 
63
63
  renderSection(section);
64
64
 
65
- const ids = screen.getAllByTestId('tb').map((el) => el.getAttribute('data-id'));
65
+ const ids = screen
66
+ .getAllByTestId('tb')
67
+ .map((el) => el.getAttribute('data-id'));
66
68
  // label block rendered through ThemeBlock (role slot) ...
67
69
  expect(ids).toContain('label-1');
68
70
  // ... and the plain content block kept in the content column.
@@ -8,6 +8,7 @@ import { twMerge } from 'tailwind-merge';
8
8
  import clsx from 'clsx';
9
9
  import ThemeBlock from '../theme-block';
10
10
 
11
+ import { postToEditor } from '../editor-bridge';
11
12
  interface BeforeAfterSectionProps {
12
13
  section: Section;
13
14
  currentBreakpoint?: string;
@@ -32,54 +33,99 @@ export default function BeforeAfterSection({
32
33
  const styles = section.styles || {};
33
34
  const properties = section.properties || {};
34
35
 
35
- const beforeBlock = section.blocks?.find(b => b.label === 'Before Image');
36
- const afterBlock = section.blocks?.find(b => b.label === 'After Image');
36
+ const beforeBlock = section.blocks?.find((b) => b.label === 'Before Image');
37
+ const afterBlock = section.blocks?.find((b) => b.label === 'After Image');
38
+
39
+ const height = getResponsiveValue(
40
+ styles.height,
41
+ currentBreakpoint,
42
+ '500px'
43
+ ) as string | number;
44
+ const width = getResponsiveValue(
45
+ styles.width,
46
+ currentBreakpoint,
47
+ '100%'
48
+ ) as string;
49
+ const maxWidth = getResponsiveValue(
50
+ styles['max-width'],
51
+ currentBreakpoint,
52
+ 'normal'
53
+ ) as string;
37
54
 
38
- const height = getResponsiveValue(styles.height, currentBreakpoint, '500px') as string | number;
39
- const width = getResponsiveValue(styles.width, currentBreakpoint, '100%') as string;
40
- const maxWidth = getResponsiveValue(styles['max-width'], currentBreakpoint, 'normal') as string;
41
-
42
55
  const sliderColor = resolveThemeCssVariables(
43
- getResponsiveValue(styles['slider-color'], currentBreakpoint, '#ffffff') as string,
56
+ getResponsiveValue(
57
+ styles['slider-color'],
58
+ currentBreakpoint,
59
+ '#ffffff'
60
+ ) as string,
44
61
  themeSettings
45
62
  );
46
63
 
47
64
  const renderBlock = (block: any) => {
48
- return (
49
- <ThemeBlock
50
- key={block.id}
51
- block={block}
52
- placeholderId={placeholderId}
53
- sectionId={section.id}
54
- isDesigner={isDesigner}
55
- isSelected={selectedBlockId === block.id}
56
- selectedBlockId={selectedBlockId}
57
- currentBreakpoint={currentBreakpoint}
58
- onMoveUp={() => {
59
- if (window.parent) window.parent.postMessage({ type: 'MOVE_BLOCK_UP', data: { placeholderId, sectionId: section.id, blockId: block.id } }, '*');
60
- }}
61
- onMoveDown={() => {
62
- if (window.parent) window.parent.postMessage({ type: 'MOVE_BLOCK_DOWN', data: { placeholderId, sectionId: section.id, blockId: block.id } }, '*');
63
- }}
64
- onDuplicate={() => {
65
- if (window.parent) window.parent.postMessage({ type: 'DUPLICATE_BLOCK', data: { placeholderId, sectionId: section.id, blockId: block.id } }, '*');
66
- }}
67
- onToggleVisibility={() => {
68
- if (window.parent) window.parent.postMessage({ type: 'TOGGLE_BLOCK_VISIBILITY', data: { placeholderId, sectionId: section.id, blockId: block.id } }, '*');
69
- }}
70
- onDelete={() => {
71
- if (window.parent) window.parent.postMessage({ type: 'DELETE_BLOCK', data: { placeholderId, sectionId: section.id, blockId: block.id } }, '*');
72
- }}
73
- onRename={(newLabel) => {
74
- if (window.parent) window.parent.postMessage({ type: 'RENAME_BLOCK', data: { placeholderId, sectionId: section.id, blockId: block.id, label: newLabel } }, '*');
75
- }}
76
- />
77
- );
65
+ return (
66
+ <ThemeBlock
67
+ key={block.id}
68
+ block={block}
69
+ placeholderId={placeholderId}
70
+ sectionId={section.id}
71
+ isDesigner={isDesigner}
72
+ isSelected={selectedBlockId === block.id}
73
+ selectedBlockId={selectedBlockId}
74
+ currentBreakpoint={currentBreakpoint}
75
+ onMoveUp={() => {
76
+ if (window.parent)
77
+ postToEditor({
78
+ type: 'MOVE_BLOCK_UP',
79
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
80
+ });
81
+ }}
82
+ onMoveDown={() => {
83
+ if (window.parent)
84
+ postToEditor({
85
+ type: 'MOVE_BLOCK_DOWN',
86
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
87
+ });
88
+ }}
89
+ onDuplicate={() => {
90
+ if (window.parent)
91
+ postToEditor({
92
+ type: 'DUPLICATE_BLOCK',
93
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
94
+ });
95
+ }}
96
+ onToggleVisibility={() => {
97
+ if (window.parent)
98
+ postToEditor({
99
+ type: 'TOGGLE_BLOCK_VISIBILITY',
100
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
101
+ });
102
+ }}
103
+ onDelete={() => {
104
+ if (window.parent)
105
+ postToEditor({
106
+ type: 'DELETE_BLOCK',
107
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
108
+ });
109
+ }}
110
+ onRename={(newLabel) => {
111
+ if (window.parent)
112
+ postToEditor({
113
+ type: 'RENAME_BLOCK',
114
+ data: {
115
+ placeholderId,
116
+ sectionId: section.id,
117
+ blockId: block.id,
118
+ label: newLabel
119
+ }
120
+ });
121
+ }}
122
+ />
123
+ );
78
124
  };
79
125
 
80
126
  useEffect(() => {
81
127
  if (!containerRef.current) return;
82
-
128
+
83
129
  setContainerWidth(containerRef.current.offsetWidth);
84
130
 
85
131
  const observer = new ResizeObserver((entries) => {
@@ -87,32 +133,33 @@ export default function BeforeAfterSection({
87
133
  setContainerWidth(entry.contentRect.width);
88
134
  }
89
135
  });
90
-
136
+
91
137
  observer.observe(containerRef.current);
92
-
138
+
93
139
  return () => observer.disconnect();
94
140
  }, []);
95
141
 
96
142
  const handleStart = (clientX: number) => {
97
- setIsResizing(true);
98
- if (containerRef.current) {
99
- const rect = containerRef.current.getBoundingClientRect();
100
- const x = Math.max(0, Math.min(clientX - rect.left, rect.width));
101
- setSliderPosition((x / rect.width) * 100);
102
- }
143
+ setIsResizing(true);
144
+ if (containerRef.current) {
145
+ const rect = containerRef.current.getBoundingClientRect();
146
+ const x = Math.max(0, Math.min(clientX - rect.left, rect.width));
147
+ setSliderPosition((x / rect.width) * 100);
148
+ }
103
149
  };
104
150
 
105
151
  const handleMouseDown = (e: React.MouseEvent) => handleStart(e.clientX);
106
- const handleTouchStart = (e: React.TouchEvent) => handleStart(e.touches[0].clientX);
152
+ const handleTouchStart = (e: React.TouchEvent) =>
153
+ handleStart(e.touches[0].clientX);
107
154
 
108
155
  const handleMove = useCallback(
109
156
  (clientX: number) => {
110
157
  if (!isResizing || !containerRef.current) return;
111
-
158
+
112
159
  const rect = containerRef.current.getBoundingClientRect();
113
160
  const x = Math.max(0, Math.min(clientX - rect.left, rect.width));
114
161
  const percentage = (x / rect.width) * 100;
115
-
162
+
116
163
  setSliderPosition(percentage);
117
164
  },
118
165
  [isResizing]
@@ -121,42 +168,43 @@ export default function BeforeAfterSection({
121
168
  const handleMouseMove = (e: React.MouseEvent) => {
122
169
  if (isResizing) handleMove(e.clientX);
123
170
  };
124
-
171
+
125
172
  const handleTouchMove = (e: React.TouchEvent) => {
126
173
  if (isResizing) handleMove(e.touches[0].clientX);
127
174
  };
128
-
175
+
129
176
  useEffect(() => {
130
177
  const handleGlobalEnd = () => setIsResizing(false);
131
178
  const handleGlobalMove = (e: MouseEvent) => {
132
- if (isResizing) {
133
- e.preventDefault();
134
- handleMove(e.clientX);
135
- }
179
+ if (isResizing) {
180
+ e.preventDefault();
181
+ handleMove(e.clientX);
182
+ }
136
183
  };
137
-
184
+
138
185
  const handleGlobalTouchMove = (e: TouchEvent) => {
139
- if (isResizing) {
140
- handleMove(e.touches[0].clientX);
141
- }
142
- }
186
+ if (isResizing) {
187
+ handleMove(e.touches[0].clientX);
188
+ }
189
+ };
143
190
 
144
191
  if (isResizing) {
145
- window.addEventListener('mouseup', handleGlobalEnd);
146
- window.addEventListener('touchend', handleGlobalEnd);
147
- window.addEventListener('mousemove', handleGlobalMove);
148
- window.addEventListener('touchmove', handleGlobalTouchMove, { passive: false });
192
+ window.addEventListener('mouseup', handleGlobalEnd);
193
+ window.addEventListener('touchend', handleGlobalEnd);
194
+ window.addEventListener('mousemove', handleGlobalMove);
195
+ window.addEventListener('touchmove', handleGlobalTouchMove, {
196
+ passive: false
197
+ });
149
198
  }
150
199
 
151
200
  return () => {
152
- window.removeEventListener('mouseup', handleGlobalEnd);
153
- window.removeEventListener('touchend', handleGlobalEnd);
154
- window.removeEventListener('mousemove', handleGlobalMove);
155
- window.removeEventListener('touchmove', handleGlobalTouchMove);
201
+ window.removeEventListener('mouseup', handleGlobalEnd);
202
+ window.removeEventListener('touchend', handleGlobalEnd);
203
+ window.removeEventListener('mousemove', handleGlobalMove);
204
+ window.removeEventListener('touchmove', handleGlobalTouchMove);
156
205
  };
157
206
  }, [isResizing, handleMove]);
158
207
 
159
-
160
208
  const maxWidthClass =
161
209
  maxWidth === 'narrow'
162
210
  ? 'max-w-4xl'
@@ -165,81 +213,88 @@ export default function BeforeAfterSection({
165
213
  : maxWidth === 'full'
166
214
  ? 'w-full'
167
215
  : '';
168
-
216
+
169
217
  const hasMaxWidth = maxWidth !== 'none' && maxWidth !== 'full';
170
218
 
171
219
  return (
172
- <div
173
- className={twMerge(
174
- clsx(
175
- 'relative mx-auto select-none overflow-hidden group/before-after',
176
- hasMaxWidth && 'mx-auto',
177
- maxWidthClass
178
- )
179
- )}
180
- style={{
181
- height: typeof height === 'number' ? `${height}px` : height,
182
- width: width === 'fill' ? '100%' : width,
183
- maxWidth: maxWidth === 'none' ? 'none' : undefined
184
- }}
185
- ref={containerRef}
186
- onMouseDown={handleMouseDown}
187
- onTouchStart={handleTouchStart}
188
- onMouseMove={handleMouseMove}
189
- onTouchMove={handleTouchMove}
220
+ <div
221
+ className={twMerge(
222
+ clsx(
223
+ 'relative mx-auto select-none overflow-hidden group/before-after',
224
+ hasMaxWidth && 'mx-auto',
225
+ maxWidthClass
226
+ )
227
+ )}
228
+ style={{
229
+ height: typeof height === 'number' ? `${height}px` : height,
230
+ width: width === 'fill' ? '100%' : width,
231
+ maxWidth: maxWidth === 'none' ? 'none' : undefined
232
+ }}
233
+ ref={containerRef}
234
+ onMouseDown={handleMouseDown}
235
+ onTouchStart={handleTouchStart}
236
+ onMouseMove={handleMouseMove}
237
+ onTouchMove={handleTouchMove}
190
238
  >
191
- <div className="absolute inset-0 w-full h-full">
192
- {afterBlock ? (
193
- <div className="w-full h-full pointer-events-none">
194
- {renderBlock(afterBlock)}
195
- </div>
196
- ) : (
197
- <div className="w-full h-full bg-gray-200 flex items-center justify-center text-gray-400 select-none pointer-events-none">
198
- No After Image
199
- </div>
200
- )}
201
- </div>
239
+ <div className="absolute inset-0 w-full h-full">
240
+ {afterBlock ? (
241
+ <div className="w-full h-full pointer-events-none">
242
+ {renderBlock(afterBlock)}
243
+ </div>
244
+ ) : (
245
+ <div className="w-full h-full bg-gray-200 flex items-center justify-center text-gray-400 select-none pointer-events-none">
246
+ No After Image
247
+ </div>
248
+ )}
249
+ </div>
202
250
 
203
- <div
204
- className="absolute top-0 left-0 h-full overflow-hidden border-r"
205
- style={{
206
- width: `${sliderPosition}%`,
207
- borderColor: sliderColor
208
- }}
251
+ <div
252
+ className="absolute top-0 left-0 h-full overflow-hidden border-r"
253
+ style={{
254
+ width: `${sliderPosition}%`,
255
+ borderColor: sliderColor
256
+ }}
257
+ >
258
+ <div
259
+ className="relative h-full"
260
+ style={{ width: containerWidth ? `${containerWidth}px` : '100%' }}
209
261
  >
210
- <div
211
- className="relative h-full"
212
- style={{ width: containerWidth ? `${containerWidth}px` : '100%' }}
213
- >
214
- {beforeBlock ? (
215
- <div className="absolute top-0 left-0 w-full h-full pointer-events-none">
216
- {renderBlock(beforeBlock)}
217
- </div>
218
- ) : (
219
- <div className="absolute top-0 left-0 w-full h-full bg-gray-300 flex items-center justify-center text-gray-500 select-none pointer-events-none">
220
- No Before Image
221
- </div>
222
- )}
262
+ {beforeBlock ? (
263
+ <div className="absolute top-0 left-0 w-full h-full pointer-events-none">
264
+ {renderBlock(beforeBlock)}
223
265
  </div>
266
+ ) : (
267
+ <div className="absolute top-0 left-0 w-full h-full bg-gray-300 flex items-center justify-center text-gray-500 select-none pointer-events-none">
268
+ No Before Image
269
+ </div>
270
+ )}
224
271
  </div>
272
+ </div>
225
273
 
226
- <div
227
- className={clsx(
228
- "absolute top-0 bottom-0 w-1 z-20 cursor-ew-resize"
229
- )}
230
- style={{
231
- left: `${sliderPosition}%`,
232
- backgroundColor: sliderColor,
233
- transform: 'translateX(-50%)'
234
- }}
235
- >
236
- <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 bg-white rounded-full shadow-lg flex items-center justify-center text-gray-800 pointer-events-none">
237
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
238
- <path d="M10 15l-3-3 3-3" />
239
- <path d="M14 15l3-3-3-3" />
240
- </svg>
241
- </div>
274
+ <div
275
+ className={clsx('absolute top-0 bottom-0 w-1 z-20 cursor-ew-resize')}
276
+ style={{
277
+ left: `${sliderPosition}%`,
278
+ backgroundColor: sliderColor,
279
+ transform: 'translateX(-50%)'
280
+ }}
281
+ >
282
+ <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 bg-white rounded-full shadow-lg flex items-center justify-center text-gray-800 pointer-events-none">
283
+ <svg
284
+ width="24"
285
+ height="24"
286
+ viewBox="0 0 24 24"
287
+ fill="none"
288
+ stroke="currentColor"
289
+ strokeWidth="2"
290
+ strokeLinecap="round"
291
+ strokeLinejoin="round"
292
+ >
293
+ <path d="M10 15l-3-3 3-3" />
294
+ <path d="M14 15l3-3-3-3" />
295
+ </svg>
242
296
  </div>
297
+ </div>
243
298
  </div>
244
299
  );
245
300
  }