sonance-brand-mcp 1.3.15 → 1.3.16

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,190 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useRef } from "react";
4
+ import { X, Sparkles, Eye, Loader2, Save, RefreshCw } from "lucide-react";
5
+ import { cn } from "../../../lib/utils";
6
+ import { PendingEdit } from "../types";
7
+ import { removePreviewStyles, injectPreviewStyles, extractPreviewCSSFromCode } from "../utils";
8
+
9
+ export interface DiffPreviewProps {
10
+ pendingEdit: PendingEdit;
11
+ componentType: string;
12
+ onSave: () => void;
13
+ onCancel: () => void;
14
+ onPreviewToggle: (isActive: boolean) => void;
15
+ isPreviewActive: boolean;
16
+ saveStatus: "idle" | "saving" | "success" | "error";
17
+ saveMessage: string;
18
+ // Variant ID for targeted preview (applies styles to specific variant instances)
19
+ variantId?: string | null;
20
+ }
21
+
22
+ export function DiffPreview({
23
+ pendingEdit,
24
+ componentType,
25
+ onSave,
26
+ onCancel,
27
+ onPreviewToggle,
28
+ isPreviewActive,
29
+ saveStatus,
30
+ saveMessage,
31
+ variantId,
32
+ }: DiffPreviewProps) {
33
+ // Track if we've auto-activated preview for this edit
34
+ const hasAutoActivated = useRef(false);
35
+
36
+ // Auto-activate preview when component mounts or pendingEdit changes
37
+ useEffect(() => {
38
+ if (!hasAutoActivated.current && pendingEdit) {
39
+ // First try AI-provided previewCSS (most reliable - AI knows exactly what it changed)
40
+ // Fall back to extraction if AI didn't provide previewCSS
41
+ let previewCSS = pendingEdit.previewCSS;
42
+
43
+ if (!previewCSS) {
44
+ // Fallback: try to extract CSS from code changes
45
+ previewCSS = extractPreviewCSSFromCode(
46
+ pendingEdit.originalCode,
47
+ pendingEdit.modifiedCode,
48
+ componentType,
49
+ variantId
50
+ );
51
+ }
52
+
53
+ if (previewCSS) {
54
+ injectPreviewStyles(previewCSS);
55
+ onPreviewToggle(true);
56
+ }
57
+ hasAutoActivated.current = true;
58
+ }
59
+ }, [pendingEdit, componentType, variantId, onPreviewToggle]);
60
+
61
+ // Reset auto-activation flag when pendingEdit changes (new edit)
62
+ useEffect(() => {
63
+ hasAutoActivated.current = false;
64
+ }, [pendingEdit.modifiedCode]);
65
+
66
+ // Cleanup preview on unmount or cancel
67
+ useEffect(() => {
68
+ return () => {
69
+ removePreviewStyles();
70
+ };
71
+ }, []);
72
+
73
+ return (
74
+ <div className="space-y-3 p-3 rounded border border-[#00A3E1]/30 bg-[#00A3E1]/5">
75
+ <div className="flex items-center justify-between">
76
+ <div className="flex items-center gap-2">
77
+ <Sparkles className="h-4 w-4 text-[#00A3E1]" />
78
+ <span id="diff-preview-span-ai-changes-ready" className="text-xs font-semibold text-gray-900">AI Changes Ready</span>
79
+ </div>
80
+ <button
81
+ onClick={onCancel}
82
+ className="text-gray-400 hover:text-gray-600"
83
+ >
84
+ <X className="h-4 w-4" />
85
+ </button>
86
+ </div>
87
+
88
+ {/* Explanation */}
89
+ <p id="diff-preview-p-pendingeditexplanati" className="text-xs text-gray-600">{pendingEdit.explanation}</p>
90
+
91
+ {/* Diff Display */}
92
+ <div className="max-h-40 overflow-auto rounded border border-gray-200 bg-white">
93
+ <pre className="p-2 text-[10px] font-mono whitespace-pre-wrap">
94
+ {pendingEdit.diff.split("\n").map((line, i) => (
95
+ <div
96
+ key={i}
97
+ className={cn(
98
+ line.startsWith("+") && !line.startsWith("@@")
99
+ ? "bg-green-50 text-green-700"
100
+ : line.startsWith("-") && !line.startsWith("@@")
101
+ ? "bg-red-50 text-red-700"
102
+ : line.startsWith("@@")
103
+ ? "bg-blue-50 text-blue-600 font-semibold"
104
+ : "text-gray-600"
105
+ )}
106
+ >
107
+ {line}
108
+ </div>
109
+ ))}
110
+ </pre>
111
+ </div>
112
+
113
+ {/* File Path */}
114
+ <div className="text-[10px] text-gray-400">
115
+ File: {pendingEdit.filePath}
116
+ </div>
117
+
118
+ {/* Live Preview Indicator */}
119
+ <div className="flex items-center gap-2 p-2 rounded bg-[#00A3E1]/10 border border-[#00A3E1]/30">
120
+ <Eye className="h-3.5 w-3.5 text-[#00A3E1]" />
121
+ <span className="text-xs text-[#00A3E1]">
122
+ Live preview active - scroll to see changes on the page
123
+ </span>
124
+ </div>
125
+
126
+ {/* Action Buttons */}
127
+ <div className="flex gap-2">
128
+ {/* Save Button */}
129
+ <button
130
+ onClick={onSave}
131
+ disabled={saveStatus === "saving"}
132
+ className={cn(
133
+ "flex-1 flex items-center justify-center gap-2 py-2",
134
+ "text-xs font-medium rounded transition-colors",
135
+ "bg-[#333F48] text-white hover:bg-[#2a343c]",
136
+ "disabled:opacity-50 disabled:cursor-not-allowed"
137
+ )}
138
+ >
139
+ {saveStatus === "saving" ? (
140
+ <>
141
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
142
+ Saving...
143
+ </>
144
+ ) : (
145
+ <>
146
+ <Save className="h-3.5 w-3.5" />
147
+ Save to File
148
+ </>
149
+ )}
150
+ </button>
151
+ {/* Cancel Button */}
152
+ <button
153
+ onClick={onCancel}
154
+ disabled={saveStatus === "saving"}
155
+ className={cn(
156
+ "px-4 py-2 text-xs font-medium rounded transition-colors",
157
+ "border border-gray-200 text-gray-600 hover:bg-gray-50",
158
+ "disabled:opacity-50 disabled:cursor-not-allowed"
159
+ )}
160
+ >
161
+ Cancel
162
+ </button>
163
+ </div>
164
+
165
+ {/* Save Status Message */}
166
+ {saveMessage && (
167
+ <div
168
+ className={cn(
169
+ "p-2 rounded text-xs",
170
+ saveStatus === "success" && "bg-green-50 text-green-700 border border-green-200",
171
+ saveStatus === "error" && "bg-red-50 text-red-700 border border-red-200"
172
+ )}
173
+ >
174
+ {saveMessage}
175
+ </div>
176
+ )}
177
+
178
+ {/* Success: Refresh Hint */}
179
+ {saveStatus === "success" && (
180
+ <div className="flex items-center gap-2 p-2 rounded bg-amber-50 border border-amber-200">
181
+ <RefreshCw className="h-3.5 w-3.5 text-amber-600" />
182
+ <span id="diff-preview-span-refresh-the-page-to-" className="text-xs text-amber-700">
183
+ Refresh the page to see your changes
184
+ </span>
185
+ </div>
186
+ )}
187
+ </div>
188
+ );
189
+ }
190
+
@@ -0,0 +1,353 @@
1
+ "use client";
2
+
3
+ import React, { useEffect } from "react";
4
+ import { Box, Image as ImageIcon, Type, MousePointer, Sparkles, Eye, Check } from "lucide-react";
5
+ import { cn } from "../../../lib/utils";
6
+ import { DetectedElement, DetectedElementType, VisionFocusedElement } from "../types";
7
+
8
+ export interface InspectorOverlayProps {
9
+ elements: DetectedElement[];
10
+ selectedLogoId?: string | null;
11
+ onLogoClick?: (logoId: string) => void;
12
+ selectedTextId?: string | null;
13
+ onTextClick?: (textId: string) => void;
14
+ interactive?: boolean;
15
+ selectedComponentId?: string | null;
16
+ onComponentClick?: (componentName: string) => void;
17
+ componentSelectionMode?: boolean;
18
+ // New: For inspector-first workflow - called when any component is clicked
19
+ onSelectComponentAndClose?: (componentType: string, variantId?: string) => void;
20
+ inspectorClickMode?: boolean; // When true, all components are clickable
21
+ // The currently selected component type (for visual highlighting)
22
+ selectedComponentType?: string;
23
+ // The currently selected component variant ID (for scoped highlighting)
24
+ selectedVariantId?: string | null;
25
+ // Current edit scope
26
+ componentScope?: "all" | "variant" | "page" | "selected";
27
+ // Preview mode - when AI changes are pending, shows green highlights
28
+ previewMode?: boolean;
29
+ // Vision mode props
30
+ visionMode?: boolean;
31
+ visionFocusedElements?: VisionFocusedElement[];
32
+ onVisionElementClick?: (element: DetectedElement) => void;
33
+ // Changed elements - elements that were modified, shown with green highlight until accept/revert
34
+ changedElements?: VisionFocusedElement[];
35
+ }
36
+
37
+ // Color config for different element types
38
+ const inspectorColors: Record<DetectedElementType, { border: string; bg: string; selectedBorder: string; selectedBg: string }> = {
39
+ component: { border: "#00A3E1", bg: "#00A3E1", selectedBorder: "#00A3E1", selectedBg: "#00A3E1" }, // Sonance blue
40
+ logo: { border: "#FC4C02", bg: "#FC4C02", selectedBorder: "#C02B0A", selectedBg: "#C02B0A" }, // IPORT orange / Blaze red for selected
41
+ text: { border: "#8B5CF6", bg: "#8B5CF6", selectedBorder: "#7C3AED", selectedBg: "#7C3AED" }, // Purple for text
42
+ };
43
+
44
+ // Preview mode colors - green to indicate pending changes
45
+ const previewColors = {
46
+ border: "#22C55E", // Green-500
47
+ bg: "#22C55E",
48
+ selectedBorder: "#16A34A", // Green-600
49
+ selectedBg: "#16A34A",
50
+ };
51
+
52
+ // Vision mode colors - purple for AI vision analysis
53
+ const visionColors = {
54
+ border: "#8B5CF6", // Purple-600
55
+ bg: "#8B5CF6",
56
+ selectedBorder: "#7C3AED", // Purple-700 (focused elements)
57
+ selectedBg: "#7C3AED",
58
+ };
59
+
60
+ export function InspectorOverlay({
61
+ elements,
62
+ selectedLogoId,
63
+ onLogoClick,
64
+ selectedTextId,
65
+ onTextClick,
66
+ interactive = false,
67
+ selectedComponentId,
68
+ onComponentClick,
69
+ componentSelectionMode = false,
70
+ onSelectComponentAndClose,
71
+ inspectorClickMode = false,
72
+ selectedComponentType = "all",
73
+ selectedVariantId,
74
+ componentScope,
75
+ previewMode = false,
76
+ visionMode = false,
77
+ visionFocusedElements = [],
78
+ onVisionElementClick,
79
+ changedElements = [],
80
+ }: InspectorOverlayProps) {
81
+ // Use document-level click listener for selection (doesn't block scroll)
82
+ // Disabled in preview mode - user should save or cancel, not select new elements
83
+ useEffect(() => {
84
+ if (previewMode) return; // No interaction in preview mode
85
+ if (!inspectorClickMode && !componentSelectionMode && !interactive && !visionMode) return;
86
+
87
+ const handleDocumentClick = (e: MouseEvent) => {
88
+ const clickX = e.clientX;
89
+ const clickY = e.clientY;
90
+
91
+ // Don't capture clicks on the DevTools panel itself
92
+ const devToolsPanel = document.querySelector('[data-sonance-devtools="true"]');
93
+ if (devToolsPanel?.contains(e.target as Node)) return;
94
+
95
+ // Find ALL elements that contain the click point
96
+ const matchingElements = elements.filter(el =>
97
+ clickX >= el.rect.left &&
98
+ clickX <= el.rect.left + el.rect.width &&
99
+ clickY >= el.rect.top &&
100
+ clickY <= el.rect.top + el.rect.height
101
+ );
102
+
103
+ // Sort by area (smallest first) - smaller elements are more specific
104
+ // This ensures clicking on a nested element picks that element, not its parent
105
+ matchingElements.sort((a, b) => {
106
+ const areaA = a.rect.width * a.rect.height;
107
+ const areaB = b.rect.width * b.rect.height;
108
+ return areaA - areaB;
109
+ });
110
+
111
+ // Process the most specific (smallest) matching element
112
+ for (const el of matchingElements) {
113
+ // Vision mode takes priority - clicking elements adds/removes from focused list
114
+ if (visionMode && onVisionElementClick) {
115
+ e.preventDefault();
116
+ e.stopPropagation();
117
+ onVisionElementClick(el);
118
+ return;
119
+ } else if (interactive && el.type === "logo" && el.logoId && onLogoClick) {
120
+ e.preventDefault();
121
+ e.stopPropagation();
122
+ onLogoClick(el.logoId);
123
+ return;
124
+ } else if (interactive && el.type === "text" && el.textId && onTextClick) {
125
+ e.preventDefault();
126
+ e.stopPropagation();
127
+ onTextClick(el.textId);
128
+ return;
129
+ } else if (inspectorClickMode && el.type === "component" && onSelectComponentAndClose) {
130
+ e.preventDefault();
131
+ e.stopPropagation();
132
+ let componentType = el.name.toLowerCase();
133
+ if (componentType === "button") {
134
+ componentType = "button-primary";
135
+ }
136
+ onSelectComponentAndClose(componentType, el.variantId);
137
+ return;
138
+ } else if (componentSelectionMode && el.type === "component" && onComponentClick) {
139
+ e.preventDefault();
140
+ e.stopPropagation();
141
+ onComponentClick(el.name);
142
+ return;
143
+ }
144
+ }
145
+ };
146
+
147
+ // Use capture phase to intercept before other handlers
148
+ document.addEventListener("click", handleDocumentClick, true);
149
+ return () => document.removeEventListener("click", handleDocumentClick, true);
150
+ }, [elements, inspectorClickMode, componentSelectionMode, interactive, onLogoClick, onTextClick, onSelectComponentAndClose, onComponentClick, previewMode, visionMode, onVisionElementClick]);
151
+
152
+ return (
153
+ <div
154
+ style={{
155
+ position: "fixed",
156
+ top: 0,
157
+ left: 0,
158
+ width: 0,
159
+ height: 0,
160
+ zIndex: 9997,
161
+ overflow: "visible",
162
+ pointerEvents: "none",
163
+ touchAction: "auto", // Ensure touch scrolling works
164
+ }}
165
+ >
166
+ {/* Visual overlays only - no pointer events, scroll works naturally */}
167
+ {elements.map((el, index) => {
168
+ const isLogoSelected = el.type === "logo" && el.logoId === selectedLogoId;
169
+ const isTextSelected = el.type === "text" && el.textId === selectedTextId;
170
+ const isComponentSelected = el.type === "component" && el.name === selectedComponentId;
171
+ const isSelected = isLogoSelected || isComponentSelected || isTextSelected;
172
+
173
+ // Check if this element is focused in vision mode
174
+ const isVisionFocused = visionMode && visionFocusedElements.some(
175
+ (ve) => ve.name === el.name && ve.variantId === el.variantId
176
+ );
177
+
178
+ // Check if this element was changed (Apply-First mode - highlight until accept/revert)
179
+ const isChangedElement = changedElements.length > 0 && changedElements.some(
180
+ (ce) => ce.name === el.name && ce.variantId === el.variantId
181
+ );
182
+
183
+ // In vision mode, only show focused elements (hide non-focused to reduce clutter)
184
+ if (visionMode && !isVisionFocused && visionFocusedElements.length > 0) {
185
+ return null;
186
+ }
187
+
188
+ // When showing changed elements, only show those specific elements
189
+ if (changedElements.length > 0 && !isChangedElement && !visionMode && !inspectorClickMode) {
190
+ return null;
191
+ }
192
+
193
+ // Use appropriate colors based on mode:
194
+ // - Changed elements (green) - highest priority (Apply-First mode)
195
+ // - Preview mode (green) takes priority
196
+ // - Vision mode (purple) for vision-focused elements
197
+ // - Otherwise use type-based colors
198
+ const colors = isChangedElement
199
+ ? previewColors // Green for changed elements
200
+ : previewMode
201
+ ? previewColors
202
+ : visionMode
203
+ ? (isVisionFocused ? visionColors : { ...visionColors, border: "#8B5CF680", bg: "#8B5CF680" })
204
+ : inspectorColors[el.type];
205
+
206
+ // Check if this element matches the currently selected component type
207
+ const elementType = el.name?.toLowerCase() || "";
208
+ const selectedType = selectedComponentType?.toLowerCase() || "all";
209
+
210
+ // Check for type match (used for label visibility)
211
+ const typeMatches = selectedType === "all" ||
212
+ elementType === selectedType ||
213
+ elementType.startsWith(selectedType.replace("-primary", "").replace("-secondary", "").replace("-outline", "").replace("-ghost", ""));
214
+
215
+ // Check for full match (used for border highlighting - considers variant scope)
216
+ let isMatchingType = false;
217
+ if (selectedType === "all") {
218
+ isMatchingType = true;
219
+ } else if (componentScope === "variant" && selectedVariantId) {
220
+ // If we have a selected variant, we must match BOTH type AND variant for full highlight
221
+ isMatchingType = typeMatches && el.variantId === selectedVariantId;
222
+ } else {
223
+ isMatchingType = typeMatches;
224
+ }
225
+
226
+ // In vision mode, don't dim elements - show all with purple, focused ones brighter
227
+ // In other modes, dim non-matching elements when a specific type is selected
228
+ // Never dim changed elements
229
+ const isDimmed = !visionMode && !isChangedElement && selectedType !== "all" && !isMatchingType;
230
+
231
+ const borderColor = isDimmed ? "#6b728080" : (isChangedElement ? colors.selectedBorder : (isVisionFocused ? colors.selectedBorder : (isSelected ? colors.selectedBorder : colors.border)));
232
+ const bgColor = isDimmed ? "#6b728080" : (isChangedElement ? colors.selectedBg : (isVisionFocused ? colors.selectedBg : (isSelected ? colors.selectedBg : colors.bg)));
233
+ const isLogoClickable = interactive && el.type === "logo" && el.logoId && onLogoClick;
234
+ const isTextClickable = interactive && el.type === "text" && el.textId && onTextClick;
235
+ const isComponentClickable = componentSelectionMode && el.type === "component" && onComponentClick;
236
+ const isInspectorClickable = inspectorClickMode && el.type === "component" && onSelectComponentAndClose;
237
+ const isVisionClickable = visionMode && onVisionElementClick;
238
+ const isClickable = isLogoClickable || isComponentClickable || isInspectorClickable || isVisionClickable || isTextClickable;
239
+
240
+ return (
241
+ <div
242
+ key={`${el.type}-${el.logoId || el.name}-${index}`}
243
+ className={cn(
244
+ "absolute",
245
+ isClickable && "cursor-pointer"
246
+ )}
247
+ style={{
248
+ position: "fixed", // Use fixed instead of absolute relative to 0x0 parent
249
+ top: el.rect.top,
250
+ left: el.rect.left,
251
+ width: el.rect.width,
252
+ height: el.rect.height,
253
+ pointerEvents: "none",
254
+ touchAction: "auto", // Allow touch scrolling
255
+ }}
256
+ >
257
+ {/* Border highlight - in preview/changed mode, use only glow (no border) so actual styling is visible */}
258
+ <div
259
+ className={cn(
260
+ "absolute transition-all",
261
+ // In preview/changed mode: no border, just glow - so component's actual styling is visible
262
+ (previewMode && isMatchingType) || isChangedElement ? "inset-[-4px]" : "inset-0 rounded-sm",
263
+ !previewMode && !visionMode && !isChangedElement && (isSelected ? "border-3" : isDimmed ? "border" : "border-2"),
264
+ visionMode && (isVisionFocused ? "border-3" : "border-2"),
265
+ (previewMode && isMatchingType) && "animate-pulse",
266
+ (visionMode && isVisionFocused) && "animate-pulse",
267
+ isChangedElement && "animate-pulse"
268
+ )}
269
+ style={{
270
+ pointerEvents: "none",
271
+ borderColor: (previewMode && isMatchingType) || isChangedElement ? "transparent" : borderColor,
272
+ borderWidth: (previewMode && isMatchingType) || isChangedElement ? 0 : undefined,
273
+ // Preview mode / Changed elements: outer glow only, no border overlay
274
+ // Vision mode: show glow for focused elements
275
+ boxShadow: isChangedElement
276
+ ? `0 0 0 3px ${previewColors.border}, 0 0 20px 8px ${previewColors.border}80`
277
+ : previewMode && isMatchingType
278
+ ? `0 0 0 3px ${previewColors.border}, 0 0 20px 8px ${previewColors.border}80`
279
+ : visionMode && isVisionFocused
280
+ ? `0 0 0 3px ${visionColors.border}, 0 0 15px 5px ${visionColors.border}60`
281
+ : isSelected
282
+ ? `0 0 0 2px ${borderColor}40`
283
+ : undefined,
284
+ opacity: isDimmed ? 0.4 : (visionMode && !isVisionFocused ? 0.6 : 1),
285
+ borderRadius: previewMode && isMatchingType ? "inherit" : undefined,
286
+ }}
287
+ />
288
+ {/* Label - show for matching elements, changed elements, or vision mode */}
289
+ {(typeMatches || isChangedElement || (visionMode && isVisionFocused)) && (
290
+ <div
291
+ className={cn(
292
+ "absolute -top-6 left-0",
293
+ "px-1.5 py-0.5 text-[10px] font-medium",
294
+ "text-white rounded-t-sm",
295
+ "whitespace-nowrap shadow-sm",
296
+ "flex items-center gap-1",
297
+ "transition-all",
298
+ (previewMode && isMatchingType) && "animate-pulse",
299
+ (visionMode && isVisionFocused) && "animate-pulse",
300
+ isChangedElement && "animate-pulse"
301
+ )}
302
+ style={{ backgroundColor: bgColor }}
303
+ >
304
+ {isChangedElement ? (
305
+ <>
306
+ <Check className="h-3 w-3" />
307
+ <span>Changed: {el.name}</span>
308
+ {el.variantId && <span className="ml-1 opacity-80 font-mono text-[9px]">#{el.variantId.substring(0, 4)}</span>}
309
+ </>
310
+ ) : previewMode && isMatchingType ? (
311
+ <>
312
+ <Sparkles className="h-3 w-3" />
313
+ <span>Preview: {el.name}</span>
314
+ {el.variantId && <span className="ml-1 opacity-80 font-mono text-[9px]">#{el.variantId.substring(0, 4)}</span>}
315
+ </>
316
+ ) : visionMode && isVisionFocused ? (
317
+ <>
318
+ <Eye className="h-3 w-3" />
319
+ <span>Focused: {el.name}</span>
320
+ {el.variantId && <span className="ml-1 opacity-80 font-mono text-[9px]">#{el.variantId.substring(0, 4)}</span>}
321
+ </>
322
+ ) : visionMode ? (
323
+ <>
324
+ <Eye className="h-3 w-3 opacity-60" />
325
+ {el.name}
326
+ {el.variantId && <span className="ml-1 opacity-80 font-mono text-[9px] border-l border-white/30 pl-1">#{el.variantId.substring(0, 4)}</span>}
327
+ </>
328
+ ) : (
329
+ <>
330
+ {el.type === "logo" && <ImageIcon className="h-3 w-3" />}
331
+ {el.type === "component" && <Box className="h-3 w-3" />}
332
+ {el.type === "text" && <Type className="h-3 w-3" />}
333
+ {el.name}
334
+ {el.variantId && <span className="ml-1 opacity-80 font-mono text-[9px] border-l border-white/30 pl-1">#{el.variantId.substring(0, 4)}</span>}
335
+ {isSelected && <span className="ml-1">✓</span>}
336
+ </>
337
+ )}
338
+ </div>
339
+ )}
340
+ {/* Click hint for logos */}
341
+ {isClickable && !isSelected && (
342
+ <div className="absolute inset-0 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
343
+ <span id="span-click-to-select" className="px-2 py-1 text-[10px] font-medium text-white bg-black/70 rounded">
344
+ Click to select
345
+ </span>
346
+ </div>
347
+ )}
348
+ </div>
349
+ );
350
+ })}
351
+ </div>
352
+ );
353
+ }