@morphika/andami 0.1.2

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 (319) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/admin/assets.ts +4 -0
  4. package/admin/database.ts +4 -0
  5. package/admin/index.ts +6 -0
  6. package/admin/login.ts +4 -0
  7. package/admin/navigation.ts +4 -0
  8. package/admin/pages-editor.ts +4 -0
  9. package/admin/pages.ts +4 -0
  10. package/admin/projects-editor.ts +4 -0
  11. package/admin/projects.ts +4 -0
  12. package/admin/settings.ts +4 -0
  13. package/admin/setup.ts +4 -0
  14. package/admin/storage.ts +4 -0
  15. package/admin/styles.ts +4 -0
  16. package/app/(site)/[slug]/loading.tsx +20 -0
  17. package/app/(site)/[slug]/page.tsx +83 -0
  18. package/app/(site)/error.tsx +32 -0
  19. package/app/(site)/layout.tsx +53 -0
  20. package/app/(site)/loading.tsx +20 -0
  21. package/app/(site)/not-found.tsx +41 -0
  22. package/app/(site)/page.tsx +43 -0
  23. package/app/(site)/preview/page.tsx +99 -0
  24. package/app/(site)/work/[slug]/loading.tsx +23 -0
  25. package/app/(site)/work/[slug]/page.tsx +84 -0
  26. package/app/admin/assets/page.tsx +573 -0
  27. package/app/admin/database/page.tsx +302 -0
  28. package/app/admin/error.tsx +53 -0
  29. package/app/admin/layout.tsx +273 -0
  30. package/app/admin/login/page.tsx +88 -0
  31. package/app/admin/navigation/page.tsx +157 -0
  32. package/app/admin/page.tsx +17 -0
  33. package/app/admin/pages/[slug]/page.tsx +849 -0
  34. package/app/admin/pages/page.tsx +588 -0
  35. package/app/admin/projects/[slug]/page.tsx +3 -0
  36. package/app/admin/projects/page.tsx +669 -0
  37. package/app/admin/settings/page.tsx +132 -0
  38. package/app/admin/setup/page.tsx +64 -0
  39. package/app/admin/storage/page.tsx +518 -0
  40. package/app/admin/styles/page.tsx +243 -0
  41. package/app/api/admin/assets/file/route.ts +81 -0
  42. package/app/api/admin/assets/health/route.ts +170 -0
  43. package/app/api/admin/assets/register/route.ts +163 -0
  44. package/app/api/admin/assets/registry/route.ts +98 -0
  45. package/app/api/admin/assets/relink/confirm/route.ts +242 -0
  46. package/app/api/admin/assets/relink/route.ts +202 -0
  47. package/app/api/admin/assets/scan/route.ts +271 -0
  48. package/app/api/admin/auth/route.ts +160 -0
  49. package/app/api/admin/custom-sections/[slug]/route.ts +159 -0
  50. package/app/api/admin/custom-sections/route.ts +127 -0
  51. package/app/api/admin/database/route.ts +53 -0
  52. package/app/api/admin/pages/[slug]/duplicate/route.ts +91 -0
  53. package/app/api/admin/pages/[slug]/route.ts +617 -0
  54. package/app/api/admin/pages/[slug]/set-home/route.ts +76 -0
  55. package/app/api/admin/pages/route.ts +129 -0
  56. package/app/api/admin/preview/route.ts +53 -0
  57. package/app/api/admin/r2/connect/route.ts +181 -0
  58. package/app/api/admin/r2/delete/route.ts +198 -0
  59. package/app/api/admin/r2/disconnect/route.ts +42 -0
  60. package/app/api/admin/r2/rename/route.ts +265 -0
  61. package/app/api/admin/r2/status/route.ts +106 -0
  62. package/app/api/admin/r2/upload-url/route.ts +148 -0
  63. package/app/api/admin/revalidate/route.ts +55 -0
  64. package/app/api/admin/settings/route.ts +279 -0
  65. package/app/api/admin/setup/complete/route.ts +51 -0
  66. package/app/api/admin/setup/route.ts +118 -0
  67. package/app/api/admin/storage/switch/route.ts +117 -0
  68. package/app/api/admin/styles/fonts/route.ts +97 -0
  69. package/app/api/admin/styles/route.ts +304 -0
  70. package/app/api/assets/[...path]/route.ts +98 -0
  71. package/app/api/custom-sections/[id]/route.ts +43 -0
  72. package/app/api/draft-mode/disable/route.ts +10 -0
  73. package/app/api/draft-mode/enable/route.ts +26 -0
  74. package/app/api/projects/route.ts +42 -0
  75. package/app/api/styles/route.ts +88 -0
  76. package/app/favicon.ico +0 -0
  77. package/app/globals.css +7 -0
  78. package/app/layout.tsx +53 -0
  79. package/app/robots.ts +17 -0
  80. package/app/sitemap.ts +48 -0
  81. package/app/studio/[[...index]]/page.tsx +8 -0
  82. package/components/admin/MetadataEditor.tsx +173 -0
  83. package/components/admin/PublishToggle.tsx +130 -0
  84. package/components/admin/icons.tsx +40 -0
  85. package/components/admin/nav-builder/NavBuilder.tsx +182 -0
  86. package/components/admin/nav-builder/NavBuilderGrid.tsx +326 -0
  87. package/components/admin/nav-builder/NavGeneralSettings.tsx +275 -0
  88. package/components/admin/nav-builder/NavGridCell.tsx +48 -0
  89. package/components/admin/nav-builder/NavGridItem.tsx +189 -0
  90. package/components/admin/nav-builder/NavItemSettings.tsx +288 -0
  91. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -0
  92. package/components/admin/nav-builder/NavLivePreview.tsx +125 -0
  93. package/components/admin/nav-builder/NavSettingsFields.tsx +248 -0
  94. package/components/admin/nav-builder/NavSettingsPanel.tsx +127 -0
  95. package/components/admin/nav-builder/index.ts +10 -0
  96. package/components/admin/nav-builder/nav-builder-utils.ts +238 -0
  97. package/components/admin/setup-wizard/BrandingStep.tsx +218 -0
  98. package/components/admin/setup-wizard/DatabaseStep.tsx +331 -0
  99. package/components/admin/setup-wizard/DoneStep.tsx +187 -0
  100. package/components/admin/setup-wizard/SetupWizard.tsx +166 -0
  101. package/components/admin/setup-wizard/StorageStep.tsx +308 -0
  102. package/components/admin/setup-wizard/WelcomeStep.tsx +96 -0
  103. package/components/admin/setup-wizard/index.ts +9 -0
  104. package/components/admin/styles/ColorsEditor.tsx +214 -0
  105. package/components/admin/styles/FontsEditor.tsx +258 -0
  106. package/components/admin/styles/GridLayoutEditor.tsx +292 -0
  107. package/components/admin/styles/LinksButtonsEditor.tsx +120 -0
  108. package/components/admin/styles/TypographyEditor.tsx +266 -0
  109. package/components/admin/styles/index.ts +9 -0
  110. package/components/admin/styles/shared.tsx +68 -0
  111. package/components/blocks/BlockRenderer.tsx +404 -0
  112. package/components/blocks/ButtonBlockRenderer.tsx +52 -0
  113. package/components/blocks/CoverBlockRenderer.tsx +239 -0
  114. package/components/blocks/CustomSectionInstanceRenderer.tsx +82 -0
  115. package/components/blocks/EnterAnimationWrapper.tsx +140 -0
  116. package/components/blocks/HoverAnimationWrapper.tsx +308 -0
  117. package/components/blocks/ImageBlockRenderer.tsx +61 -0
  118. package/components/blocks/ImageGridBlockRenderer.tsx +545 -0
  119. package/components/blocks/PageBackground.tsx +28 -0
  120. package/components/blocks/PageNavAnimation.tsx +35 -0
  121. package/components/blocks/PageNavColor.tsx +24 -0
  122. package/components/blocks/PageRenderer.tsx +142 -0
  123. package/components/blocks/ParallaxGroupRenderer.tsx +448 -0
  124. package/components/blocks/ParallaxSlideRenderer.tsx +175 -0
  125. package/components/blocks/ProjectGridBlockRenderer.tsx +556 -0
  126. package/components/blocks/SectionRenderer.tsx +170 -0
  127. package/components/blocks/SectionV2Renderer.tsx +330 -0
  128. package/components/blocks/ShaderCanvas.tsx +392 -0
  129. package/components/blocks/SpacerBlockRenderer.tsx +17 -0
  130. package/components/blocks/TextBlockRenderer.tsx +87 -0
  131. package/components/blocks/TypewriterRichText.tsx +464 -0
  132. package/components/blocks/TypewriterWrapper.tsx +149 -0
  133. package/components/blocks/VideoBlockRenderer.tsx +304 -0
  134. package/components/blocks/index.ts +2 -0
  135. package/components/builder/AssetBrowser.tsx +2 -0
  136. package/components/builder/BlockLivePreview.tsx +101 -0
  137. package/components/builder/BlockTypePicker.tsx +178 -0
  138. package/components/builder/BuilderCanvas.tsx +354 -0
  139. package/components/builder/CanvasMinimap.tsx +200 -0
  140. package/components/builder/CanvasToolbar.tsx +202 -0
  141. package/components/builder/ColorPicker.tsx +243 -0
  142. package/components/builder/ColorSwatchPicker.tsx +274 -0
  143. package/components/builder/ColumnDragContext.tsx +51 -0
  144. package/components/builder/ColumnDragOverlay.tsx +110 -0
  145. package/components/builder/CustomSectionInstanceCard.tsx +97 -0
  146. package/components/builder/DeviceFrame.tsx +123 -0
  147. package/components/builder/DndWrapper.tsx +337 -0
  148. package/components/builder/InsertionLines.tsx +186 -0
  149. package/components/builder/ParallaxGroupCanvas.tsx +228 -0
  150. package/components/builder/ParallaxSlideHeader.tsx +113 -0
  151. package/components/builder/ReadOnlyFrame.tsx +417 -0
  152. package/components/builder/SectionEditorBar.tsx +288 -0
  153. package/components/builder/SectionTypePicker.tsx +422 -0
  154. package/components/builder/SectionV2Canvas.tsx +297 -0
  155. package/components/builder/SectionV2Column.tsx +488 -0
  156. package/components/builder/SettingsPanel.tsx +911 -0
  157. package/components/builder/SortableBlock.tsx +230 -0
  158. package/components/builder/SortableRow.tsx +362 -0
  159. package/components/builder/VirtualAssetGrid.tsx +397 -0
  160. package/components/builder/asset-browser/AssetBrowser.tsx +178 -0
  161. package/components/builder/asset-browser/FileLightbox.tsx +116 -0
  162. package/components/builder/asset-browser/FolderTreeItem.tsx +55 -0
  163. package/components/builder/asset-browser/R2BrowserContent.tsx +436 -0
  164. package/components/builder/asset-browser/R2ContextMenu.tsx +98 -0
  165. package/components/builder/asset-browser/VideoThumbnail.tsx +63 -0
  166. package/components/builder/asset-browser/helpers.ts +88 -0
  167. package/components/builder/asset-browser/index.ts +1 -0
  168. package/components/builder/asset-browser/types.ts +49 -0
  169. package/components/builder/asset-browser/useAssetBrowser.ts +344 -0
  170. package/components/builder/asset-browser/useR2DragDrop.ts +116 -0
  171. package/components/builder/asset-browser/useR2Operations.ts +189 -0
  172. package/components/builder/blockStyles.tsx +295 -0
  173. package/components/builder/editors/ButtonBlockEditor.tsx +184 -0
  174. package/components/builder/editors/CoverBlockEditor.tsx +488 -0
  175. package/components/builder/editors/EnterAnimationPicker.tsx +297 -0
  176. package/components/builder/editors/HoverEffectPicker.tsx +209 -0
  177. package/components/builder/editors/ImageBlockEditor.tsx +206 -0
  178. package/components/builder/editors/ImageGridBlockEditor.tsx +386 -0
  179. package/components/builder/editors/ProjectGridEditor.tsx +648 -0
  180. package/components/builder/editors/SpacerBlockEditor.tsx +167 -0
  181. package/components/builder/editors/StaggerSettings.tsx +108 -0
  182. package/components/builder/editors/TextAlignmentIcons.tsx +39 -0
  183. package/components/builder/editors/TextBlockEditor.tsx +462 -0
  184. package/components/builder/editors/TextStylePicker.tsx +183 -0
  185. package/components/builder/editors/VideoBlockEditor.tsx +278 -0
  186. package/components/builder/editors/index.ts +10 -0
  187. package/components/builder/editors/shared.tsx +345 -0
  188. package/components/builder/hooks/useColumnDrag.ts +472 -0
  189. package/components/builder/hooks/useColumnResize.ts +221 -0
  190. package/components/builder/index.ts +12 -0
  191. package/components/builder/live-preview/LiveButtonPreview.tsx +38 -0
  192. package/components/builder/live-preview/LiveCoverPreview.tsx +146 -0
  193. package/components/builder/live-preview/LiveImageGridPreview.tsx +123 -0
  194. package/components/builder/live-preview/LiveImagePreview.tsx +107 -0
  195. package/components/builder/live-preview/LiveProjectGridPreview.tsx +1010 -0
  196. package/components/builder/live-preview/LiveSpacerPreview.tsx +9 -0
  197. package/components/builder/live-preview/LiveTextEditor.tsx +198 -0
  198. package/components/builder/live-preview/LiveVideoPreview.tsx +98 -0
  199. package/components/builder/live-preview/index.ts +10 -0
  200. package/components/builder/live-preview/shared.tsx +153 -0
  201. package/components/builder/settings-panel/BlockLayoutTab.tsx +532 -0
  202. package/components/builder/settings-panel/BlockSettings.tsx +94 -0
  203. package/components/builder/settings-panel/ColumnV2Settings.tsx +160 -0
  204. package/components/builder/settings-panel/LayoutTab.tsx +310 -0
  205. package/components/builder/settings-panel/PageSettings.tsx +200 -0
  206. package/components/builder/settings-panel/ParallaxGroupSettings.tsx +118 -0
  207. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +178 -0
  208. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +103 -0
  209. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +312 -0
  210. package/components/builder/settings-panel/SectionV2Settings.tsx +323 -0
  211. package/components/builder/settings-panel/TRBLInputs.tsx +51 -0
  212. package/components/builder/settings-panel/index.ts +19 -0
  213. package/components/builder/settings-panel/responsive-helpers.ts +524 -0
  214. package/components/ui/CustomCursor.tsx +118 -0
  215. package/components/ui/NavContentLightbox.tsx +152 -0
  216. package/components/ui/Navbar.tsx +582 -0
  217. package/components/ui/PortfolioTracker.tsx +87 -0
  218. package/components/ui/ScrollToTop.tsx +47 -0
  219. package/lib/animation/enter-presets.ts +147 -0
  220. package/lib/animation/enter-resolve.ts +90 -0
  221. package/lib/animation/enter-types.ts +128 -0
  222. package/lib/animation/hover-effect-presets.ts +210 -0
  223. package/lib/animation/hover-effect-types.ts +126 -0
  224. package/lib/asset-retry.ts +111 -0
  225. package/lib/assets.ts +92 -0
  226. package/lib/audit.ts +35 -0
  227. package/lib/auth-token.ts +94 -0
  228. package/lib/auth.ts +13 -0
  229. package/lib/builder/cascade-helpers.ts +51 -0
  230. package/lib/builder/cascade.ts +533 -0
  231. package/lib/builder/constants.ts +103 -0
  232. package/lib/builder/defaults.ts +182 -0
  233. package/lib/builder/history.ts +48 -0
  234. package/lib/builder/index.ts +21 -0
  235. package/lib/builder/layout-styles.ts +344 -0
  236. package/lib/builder/masonry.ts +166 -0
  237. package/lib/builder/responsive.ts +156 -0
  238. package/lib/builder/serializer.ts +845 -0
  239. package/lib/builder/store-blocks.ts +193 -0
  240. package/lib/builder/store-canvas.ts +319 -0
  241. package/lib/builder/store-helpers.ts +490 -0
  242. package/lib/builder/store-sections.ts +709 -0
  243. package/lib/builder/store.ts +333 -0
  244. package/lib/builder/templates.ts +297 -0
  245. package/lib/builder/types.ts +374 -0
  246. package/lib/builder/utils.ts +37 -0
  247. package/lib/color-utils.ts +116 -0
  248. package/lib/config/index.ts +57 -0
  249. package/lib/config/types.ts +122 -0
  250. package/lib/contexts/AssetContext.tsx +79 -0
  251. package/lib/contexts/NavAnimationContext.tsx +44 -0
  252. package/lib/contexts/NavColorContext.tsx +38 -0
  253. package/lib/contexts/PageExitContext.tsx +194 -0
  254. package/lib/contexts/ThumbStatusContext.tsx +83 -0
  255. package/lib/csrf-client.ts +34 -0
  256. package/lib/csrf.ts +68 -0
  257. package/lib/format-utils.ts +24 -0
  258. package/lib/hooks/useViewport.ts +42 -0
  259. package/lib/logger.ts +81 -0
  260. package/lib/revalidate.ts +23 -0
  261. package/lib/sanitize.ts +91 -0
  262. package/lib/sanity/client.ts +8 -0
  263. package/lib/sanity/queries.ts +486 -0
  264. package/lib/sanity/types.ts +869 -0
  265. package/lib/sanity/writeClient.ts +24 -0
  266. package/lib/security.ts +402 -0
  267. package/lib/setup/detect.ts +156 -0
  268. package/lib/shader/glsl/index.ts +27 -0
  269. package/lib/shader/glsl/pixelate.ts +51 -0
  270. package/lib/shader/glsl/rgb-shift.ts +45 -0
  271. package/lib/shader/glsl/ripple.ts +46 -0
  272. package/lib/shader/glsl/vertex.ts +14 -0
  273. package/lib/storage/index.ts +211 -0
  274. package/lib/storage/r2-adapter.ts +286 -0
  275. package/lib/storage/types.ts +125 -0
  276. package/lib/styles/provider.tsx +267 -0
  277. package/lib/thumbnails/generate.ts +151 -0
  278. package/lib/utils.ts +6 -0
  279. package/package.json +212 -0
  280. package/sanity/compose.ts +65 -0
  281. package/sanity/sanity.config.ts +126 -0
  282. package/sanity/schemas/assetRegistry.ts +301 -0
  283. package/sanity/schemas/blocks/blockLayout.ts +90 -0
  284. package/sanity/schemas/blocks/buttonBlock.ts +82 -0
  285. package/sanity/schemas/blocks/coverBlock.ts +229 -0
  286. package/sanity/schemas/blocks/imageBlock.ts +58 -0
  287. package/sanity/schemas/blocks/imageGridBlock.ts +112 -0
  288. package/sanity/schemas/blocks/index.ts +9 -0
  289. package/sanity/schemas/blocks/projectGridBlock.ts +251 -0
  290. package/sanity/schemas/blocks/spacerBlock.ts +41 -0
  291. package/sanity/schemas/blocks/textBlock.ts +139 -0
  292. package/sanity/schemas/blocks/videoBlock.ts +80 -0
  293. package/sanity/schemas/customSection.ts +69 -0
  294. package/sanity/schemas/customSectionInstance.ts +163 -0
  295. package/sanity/schemas/index.ts +111 -0
  296. package/sanity/schemas/objects/enterAnimationConfig.ts +72 -0
  297. package/sanity/schemas/objects/hoverEffectConfig.ts +90 -0
  298. package/sanity/schemas/objects/parallaxGroup.ts +66 -0
  299. package/sanity/schemas/objects/parallaxSlide.ts +217 -0
  300. package/sanity/schemas/objects/typewriterConfig.ts +38 -0
  301. package/sanity/schemas/page.ts +162 -0
  302. package/sanity/schemas/pageSection.ts +157 -0
  303. package/sanity/schemas/pageSectionV2.ts +269 -0
  304. package/sanity/schemas/siteSettings.ts +256 -0
  305. package/sanity/schemas/siteStyles.ts +212 -0
  306. package/site/error.ts +4 -0
  307. package/site/index.ts +8 -0
  308. package/site/not-found.ts +4 -0
  309. package/site/page.ts +4 -0
  310. package/site/preview.ts +4 -0
  311. package/site/robots.ts +4 -0
  312. package/site/sitemap.ts +4 -0
  313. package/site/work.ts +4 -0
  314. package/studio/index.ts +4 -0
  315. package/styles/admin.css +85 -0
  316. package/styles/animations.css +237 -0
  317. package/styles/base.css +148 -0
  318. package/styles/globals.css +10 -0
  319. package/tsconfig.json +25 -0
@@ -0,0 +1,312 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SectionV2LayoutTab — Layout tab content for V2 grid sections.
5
+ *
6
+ * Extracted from SectionV2Settings.tsx in Session 95.
7
+ * Controls: spacing (TRBL padding), background (color/opacity/image),
8
+ * offset (TRBL margins), and border properties.
9
+ */
10
+
11
+ import { useBuilderStore } from "../../../lib/builder/store";
12
+ import type { PageSectionV2, SectionV2Settings as SectionV2SettingsType } from "../../../lib/sanity/types";
13
+ import {
14
+ SettingsField,
15
+ SettingsSection,
16
+ INPUT_CLASS,
17
+ } from "../editors/shared";
18
+ import ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
19
+
20
+ export function SectionV2LayoutTab({ section }: { section: PageSectionV2 }) {
21
+ const store = useBuilderStore();
22
+ const paletteSwatches = usePaletteSwatches();
23
+ const settings = section.settings;
24
+ const activeViewport = store.activeViewport;
25
+
26
+ const viewportLabel = activeViewport !== "desktop"
27
+ ? activeViewport === "tablet" ? "Tablet" : "Phone"
28
+ : null;
29
+
30
+ /**
31
+ * Update setting with viewport awareness.
32
+ * Desktop writes directly to settings; tablet/phone writes to responsive overrides.
33
+ */
34
+ const updateSettingResponsive = (property: string, value: unknown) => {
35
+ if (activeViewport === "desktop") {
36
+ store.updateSectionV2Settings(section._key, { [property]: value } as Partial<SectionV2SettingsType>);
37
+ } else {
38
+ const existing = section.responsive || {};
39
+ const vp = activeViewport as "tablet" | "phone";
40
+ const vpSettings = { ...(existing[vp]?.settings || {}), [property]: value };
41
+ if (value === undefined) delete (vpSettings as Record<string, unknown>)[property];
42
+ const vpOverride = { ...(existing[vp] || {}), settings: Object.keys(vpSettings).length ? vpSettings : undefined };
43
+ const responsive = { ...existing, [vp]: vpOverride };
44
+ // Clean up empty viewport override
45
+ if (!vpOverride.columns?.length && !vpOverride.settings) delete responsive[vp];
46
+ store.updateSectionV2Responsive(section._key, Object.keys(responsive).length ? responsive : undefined);
47
+ }
48
+ };
49
+
50
+ /** Read a setting value respecting viewport overrides */
51
+ const getSettingValue = <T,>(property: string, fallback: T): T => {
52
+ if (activeViewport !== "desktop") {
53
+ const vp = activeViewport as "tablet" | "phone";
54
+ const vpSettings = section.responsive?.[vp]?.settings as Record<string, unknown> | undefined;
55
+ const override = vpSettings?.[property];
56
+ if (override !== undefined) return override as T;
57
+ }
58
+ const val = (settings as unknown as Record<string, unknown>)[property];
59
+ return (val !== undefined ? val : fallback) as T;
60
+ };
61
+
62
+ const hasOverride = (property: string): boolean => {
63
+ if (activeViewport === "desktop") return false;
64
+ const vp = activeViewport as "tablet" | "phone";
65
+ const vpSettings = section.responsive?.[vp]?.settings as Record<string, unknown> | undefined;
66
+ return vpSettings?.[property] !== undefined;
67
+ };
68
+
69
+ return (
70
+ <>
71
+ {viewportLabel && (
72
+ <div className="px-4 pt-3">
73
+ <div className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[#076bff]/8 border border-[#076bff]/15">
74
+ <span className="text-[11px] font-medium text-[#076bff]">
75
+ Editing {viewportLabel} overrides
76
+ </span>
77
+ </div>
78
+ </div>
79
+ )}
80
+
81
+ {/* Spacing (Padding) */}
82
+ <SettingsSection title="Spacing" defaultOpen>
83
+ <div className="space-y-2">
84
+ {(["top", "right", "bottom", "left"] as const).map((side) => {
85
+ const prop = `spacing_${side}`;
86
+ return (
87
+ <SettingsField key={side} label={
88
+ <span className="capitalize">
89
+ {side}
90
+ {hasOverride(prop) && (
91
+ <span className="ml-1 text-[9px] text-[#076bff]">overridden</span>
92
+ )}
93
+ </span>
94
+ }>
95
+ <div className="flex items-center gap-2">
96
+ <input
97
+ type="text"
98
+ value={getSettingValue<string>(prop, "0")}
99
+ onChange={(e) => updateSettingResponsive(prop, e.target.value)}
100
+ placeholder="0"
101
+ className={INPUT_CLASS}
102
+ style={{ width: 80 }}
103
+ />
104
+ <span className="text-[10px] text-neutral-400">px</span>
105
+ {hasOverride(prop) && (
106
+ <button
107
+ onClick={() => updateSettingResponsive(prop, undefined)}
108
+ className="text-[10px] text-neutral-400 hover:text-[var(--admin-error)] transition-colors ml-auto"
109
+ >
110
+ Reset
111
+ </button>
112
+ )}
113
+ </div>
114
+ </SettingsField>
115
+ );
116
+ })}
117
+ </div>
118
+ </SettingsSection>
119
+
120
+ {/* Background (full controls) */}
121
+ <SettingsSection title="Background" defaultOpen>
122
+ <SettingsField label="Color">
123
+ <ColorSwatchPicker
124
+ value={getSettingValue<string>("background_color", "")}
125
+ onChange={(hex) => updateSettingResponsive("background_color", hex)}
126
+ swatches={paletteSwatches}
127
+ />
128
+ </SettingsField>
129
+
130
+ <SettingsField label="Opacity">
131
+ <div className="flex items-center gap-2">
132
+ <input
133
+ type="range"
134
+ min={0}
135
+ max={100}
136
+ value={getSettingValue<number>("background_opacity", 100)}
137
+ onChange={(e) => updateSettingResponsive("background_opacity", parseInt(e.target.value))}
138
+ className="flex-1 accent-[#076bff]"
139
+ />
140
+ <span className="text-xs text-neutral-900 w-10 text-right">
141
+ {getSettingValue<number>("background_opacity", 100)}%
142
+ </span>
143
+ </div>
144
+ </SettingsField>
145
+
146
+ <SettingsField label="Image">
147
+ <input
148
+ type="text"
149
+ value={getSettingValue<string>("background_image", "")}
150
+ onChange={(e) => updateSettingResponsive("background_image", e.target.value)}
151
+ placeholder="path/to/image.jpg"
152
+ className={INPUT_CLASS}
153
+ />
154
+ </SettingsField>
155
+
156
+ {getSettingValue<string>("background_image", "") && (
157
+ <>
158
+ <SettingsField label="Size">
159
+ <select
160
+ value={getSettingValue<string>("background_size", "cover")}
161
+ onChange={(e) => updateSettingResponsive("background_size", e.target.value)}
162
+ className={INPUT_CLASS}
163
+ >
164
+ <option value="cover">Cover</option>
165
+ <option value="contain">Contain</option>
166
+ <option value="auto">Auto</option>
167
+ </select>
168
+ </SettingsField>
169
+
170
+ <SettingsField label="Position">
171
+ <select
172
+ value={getSettingValue<string>("background_position", "center center")}
173
+ onChange={(e) => updateSettingResponsive("background_position", e.target.value)}
174
+ className={INPUT_CLASS}
175
+ >
176
+ <option value="center center">Center</option>
177
+ <option value="top center">Top</option>
178
+ <option value="bottom center">Bottom</option>
179
+ <option value="left center">Left</option>
180
+ <option value="right center">Right</option>
181
+ </select>
182
+ </SettingsField>
183
+
184
+ <SettingsField label="Repeat">
185
+ <select
186
+ value={getSettingValue<string>("background_repeat", "no-repeat")}
187
+ onChange={(e) => updateSettingResponsive("background_repeat", e.target.value)}
188
+ className={INPUT_CLASS}
189
+ >
190
+ <option value="no-repeat">No Repeat</option>
191
+ <option value="repeat">Repeat</option>
192
+ <option value="repeat-x">Repeat X</option>
193
+ <option value="repeat-y">Repeat Y</option>
194
+ </select>
195
+ </SettingsField>
196
+ </>
197
+ )}
198
+ </SettingsSection>
199
+
200
+ {/* Offset (Margin) */}
201
+ <SettingsSection title="Offset">
202
+ <div className="space-y-2">
203
+ {(["top", "right", "bottom", "left"] as const).map((side) => {
204
+ const prop = `offset_${side}`;
205
+ return (
206
+ <SettingsField key={side} label={
207
+ <span className="capitalize">
208
+ {side}
209
+ {hasOverride(prop) && (
210
+ <span className="ml-1 text-[9px] text-[#076bff]">overridden</span>
211
+ )}
212
+ </span>
213
+ }>
214
+ <div className="flex items-center gap-2">
215
+ <input
216
+ type="text"
217
+ value={getSettingValue<string>(prop, "0")}
218
+ onChange={(e) => updateSettingResponsive(prop, e.target.value)}
219
+ placeholder="0"
220
+ className={INPUT_CLASS}
221
+ style={{ width: 80 }}
222
+ />
223
+ <span className="text-[10px] text-neutral-400">px</span>
224
+ {hasOverride(prop) && (
225
+ <button
226
+ onClick={() => updateSettingResponsive(prop, undefined)}
227
+ className="text-[10px] text-neutral-400 hover:text-[var(--admin-error)] transition-colors ml-auto"
228
+ >
229
+ Reset
230
+ </button>
231
+ )}
232
+ </div>
233
+ </SettingsField>
234
+ );
235
+ })}
236
+ </div>
237
+ </SettingsSection>
238
+
239
+ {/* Border */}
240
+ <SettingsSection title="Border">
241
+ <SettingsField label="Color">
242
+ <ColorSwatchPicker
243
+ value={getSettingValue<string>("border_color", "")}
244
+ onChange={(hex) => updateSettingResponsive("border_color", hex)}
245
+ swatches={paletteSwatches}
246
+ />
247
+ </SettingsField>
248
+
249
+ <SettingsField label="Width">
250
+ <div className="flex items-center gap-2">
251
+ <input
252
+ type="range"
253
+ min={0}
254
+ max={20}
255
+ value={parseInt(getSettingValue<string>("border_width", "0"))}
256
+ onChange={(e) => updateSettingResponsive("border_width", e.target.value)}
257
+ className="flex-1 accent-[#076bff]"
258
+ />
259
+ <span className="text-xs text-neutral-900 w-10 text-right">
260
+ {getSettingValue<string>("border_width", "0")}px
261
+ </span>
262
+ </div>
263
+ </SettingsField>
264
+
265
+ <SettingsField label="Style">
266
+ <select
267
+ value={getSettingValue<string>("border_style", "none")}
268
+ onChange={(e) => updateSettingResponsive("border_style", e.target.value)}
269
+ className={INPUT_CLASS}
270
+ >
271
+ <option value="none">None</option>
272
+ <option value="solid">Solid</option>
273
+ <option value="dashed">Dashed</option>
274
+ <option value="dotted">Dotted</option>
275
+ </select>
276
+ </SettingsField>
277
+
278
+ <SettingsField label="Sides">
279
+ <select
280
+ value={getSettingValue<string>("border_sides", "all")}
281
+ onChange={(e) => updateSettingResponsive("border_sides", e.target.value)}
282
+ className={INPUT_CLASS}
283
+ >
284
+ <option value="all">All</option>
285
+ <option value="top">Top</option>
286
+ <option value="right">Right</option>
287
+ <option value="bottom">Bottom</option>
288
+ <option value="left">Left</option>
289
+ <option value="top-bottom">Top & Bottom</option>
290
+ <option value="left-right">Left & Right</option>
291
+ </select>
292
+ </SettingsField>
293
+
294
+ <SettingsField label="Radius">
295
+ <div className="flex items-center gap-2">
296
+ <input
297
+ type="range"
298
+ min={0}
299
+ max={50}
300
+ value={parseInt(getSettingValue<string>("border_radius", "0"))}
301
+ onChange={(e) => updateSettingResponsive("border_radius", e.target.value)}
302
+ className="flex-1 accent-[#076bff]"
303
+ />
304
+ <span className="text-xs text-neutral-900 w-10 text-right">
305
+ {getSettingValue<string>("border_radius", "0")}px
306
+ </span>
307
+ </div>
308
+ </SettingsField>
309
+ </SettingsSection>
310
+ </>
311
+ );
312
+ }
@@ -0,0 +1,323 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SectionV2Settings — Settings panel for V2 grid sections.
5
+ *
6
+ * Three tabs managed by parent SettingsPanel:
7
+ * - Settings: presets grid, col_gap, row_gap, appearance (background color/opacity/image)
8
+ * - Layout: spacing TRBL, offset TRBL, border, background image settings (reuses LayoutTab patterns)
9
+ * - Animation: scroll animation picker, hover animation picker, stagger settings
10
+ *
11
+ * Session 83: Phase 4 of V2 Grid System.
12
+ */
13
+
14
+ import { useBuilderStore } from "../../../lib/builder/store";
15
+ import type { PageSectionV2, SectionV2Settings as SectionV2SettingsType, SectionV2Preset, SectionV2SettingsOverridable } from "../../../lib/sanity/types";
16
+ import {
17
+ SettingsField,
18
+ SettingsSection,
19
+ } from "../editors/shared";
20
+ import ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
21
+ import {
22
+ getSectionV2SettingValue,
23
+ hasSectionV2SettingOverride,
24
+ buildSectionV2SettingOverride,
25
+ hasAnyColumnV2Overrides,
26
+ hasAnySectionV2SettingOverrides,
27
+ buildStackOverride,
28
+ buildColumnV2Overrides,
29
+ } from "./responsive-helpers";
30
+
31
+ // ============================================
32
+ // Preset definitions for the picker grid
33
+ // ============================================
34
+
35
+ interface PresetOption {
36
+ id: SectionV2Preset;
37
+ label: string;
38
+ cols: number[]; // visual representation of column spans
39
+ readonly?: boolean; // "custom" is read-only
40
+ }
41
+
42
+ const PRESETS: PresetOption[] = [
43
+ { id: "full", label: "Full", cols: [12] },
44
+ { id: "halves", label: "½ + ½", cols: [6, 6] },
45
+ { id: "thirds", label: "⅓ × 3", cols: [4, 4, 4] },
46
+ { id: "quarters", label: "¼ × 4", cols: [3, 3, 3, 3] },
47
+ { id: "1/3+2/3", label: "⅓ + ⅔", cols: [4, 8] },
48
+ { id: "2/3+1/3", label: "⅔ + ⅓", cols: [8, 4] },
49
+ ];
50
+
51
+ const CUSTOM_PRESET: PresetOption = { id: "custom", label: "Custom", cols: [], readonly: true };
52
+
53
+ // ============================================
54
+ // Preset Grid Component
55
+ // ============================================
56
+
57
+ function PresetGrid({ section }: { section: PageSectionV2 }) {
58
+ const applyPresetV2 = useBuilderStore((s) => s.applyPresetV2);
59
+ const currentPreset = section.settings.preset;
60
+
61
+ const allPresets = currentPreset === "custom"
62
+ ? [...PRESETS, CUSTOM_PRESET]
63
+ : PRESETS;
64
+
65
+ return (
66
+ <div className="grid grid-cols-3 gap-1.5">
67
+ {allPresets.map((preset) => {
68
+ const isActive = currentPreset === preset.id;
69
+ const isCustom = preset.id === "custom";
70
+
71
+ return (
72
+ <button
73
+ key={preset.id}
74
+ onClick={() => !isCustom && applyPresetV2(section._key, preset.id)}
75
+ className={`flex flex-col items-center gap-1 p-2 rounded-lg border transition-all ${
76
+ isActive
77
+ ? "border-[#076bff] bg-[#076bff]/5"
78
+ : isCustom
79
+ ? "border-neutral-200 bg-neutral-50 opacity-60 cursor-default"
80
+ : "border-neutral-200 bg-white hover:border-neutral-300 hover:bg-neutral-50"
81
+ }`}
82
+ title={isCustom ? "Layout doesn't match any preset" : preset.label}
83
+ >
84
+ {/* Visual representation */}
85
+ <div className="flex gap-0.5 w-full h-4">
86
+ {isCustom ? (
87
+ <div className="flex-1 rounded-sm bg-neutral-200 flex items-center justify-center">
88
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" className="text-neutral-400">
89
+ <path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
90
+ </svg>
91
+ </div>
92
+ ) : (
93
+ preset.cols.map((span, i) => (
94
+ <div
95
+ key={i}
96
+ className={`rounded-sm transition-colors ${
97
+ isActive ? "bg-[#076bff]" : "bg-neutral-300"
98
+ }`}
99
+ style={{ flex: span }}
100
+ />
101
+ ))
102
+ )}
103
+ </div>
104
+ <span className={`text-[9px] font-medium ${
105
+ isActive ? "text-[#076bff]" : "text-neutral-500"
106
+ }`}>
107
+ {preset.label}
108
+ </span>
109
+ </button>
110
+ );
111
+ })}
112
+ </div>
113
+ );
114
+ }
115
+
116
+ // ============================================
117
+ // SectionV2Settings — Settings Tab Content
118
+ // ============================================
119
+
120
+ export default function SectionV2Settings({ section }: { section: PageSectionV2 }) {
121
+ const store = useBuilderStore();
122
+ const paletteSwatches = usePaletteSwatches();
123
+ const settings = section.settings;
124
+ const activeViewport = store.activeViewport;
125
+ const isResponsive = activeViewport !== "desktop";
126
+
127
+ const hasColOverrides = hasAnyColumnV2Overrides(section, activeViewport);
128
+ const hasSettingsOverrides = hasAnySectionV2SettingOverrides(section, activeViewport);
129
+ const hasAnyOverrides = hasColOverrides || hasSettingsOverrides;
130
+
131
+ /** Viewport-aware update: desktop writes to settings, tablet/phone to responsive */
132
+ const updateSettingResponsive = (property: keyof SectionV2SettingsOverridable, value: unknown) => {
133
+ if (activeViewport === "desktop") {
134
+ store.updateSectionV2Settings(section._key, { [property]: value } as Partial<SectionV2SettingsType>);
135
+ } else {
136
+ const responsive = buildSectionV2SettingOverride(section, activeViewport, property, value);
137
+ store.updateSectionV2Responsive(section._key, responsive ?? undefined);
138
+ }
139
+ };
140
+
141
+ const getGapValue = (property: "col_gap" | "row_gap", fallback: number): number => {
142
+ return getSectionV2SettingValue(section, activeViewport, property, fallback);
143
+ };
144
+
145
+ const handleStack = () => {
146
+ const colOverrides = buildStackOverride(section);
147
+ const responsive = buildColumnV2Overrides(section, activeViewport, colOverrides);
148
+ store.updateSectionV2Responsive(section._key, responsive ?? undefined);
149
+ };
150
+
151
+ const handleReset = () => {
152
+ // Clear all responsive overrides for the current viewport
153
+ const existing = section.responsive || {};
154
+ const vp = activeViewport as "tablet" | "phone";
155
+ const responsive = { ...existing };
156
+ delete responsive[vp];
157
+ store.updateSectionV2Responsive(section._key, Object.keys(responsive).length ? responsive : undefined);
158
+ };
159
+
160
+ return (
161
+ <>
162
+ {/* Responsive info banner */}
163
+ {isResponsive && (
164
+ <div className="px-4 pt-3">
165
+ <div className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[#076bff]/8 border border-[#076bff]/15">
166
+ <span className="text-[11px] font-medium text-[#076bff]">
167
+ Editing {activeViewport === "tablet" ? "Tablet" : "Phone"} overrides
168
+ </span>
169
+ </div>
170
+ </div>
171
+ )}
172
+
173
+ {/* Responsive actions — Stack & Reset (only in tablet/phone) */}
174
+ {isResponsive && (
175
+ <div className="px-4 pt-3">
176
+ <div className="flex gap-2">
177
+ <button
178
+ onClick={handleStack}
179
+ className="flex-1 rounded-lg bg-[#076bff]/8 border border-[#076bff]/20 py-2 text-xs font-medium text-[#076bff] hover:bg-[#076bff]/15 transition-colors"
180
+ title="Stack all columns vertically (full width, one per row)"
181
+ >
182
+ Stack Columns
183
+ </button>
184
+ <button
185
+ onClick={handleReset}
186
+ disabled={!hasAnyOverrides}
187
+ className={`flex-1 rounded-lg border py-2 text-xs font-medium transition-colors ${
188
+ hasAnyOverrides
189
+ ? "bg-neutral-100 border-neutral-200 text-neutral-600 hover:bg-neutral-200"
190
+ : "bg-neutral-50 border-neutral-100 text-neutral-300 cursor-not-allowed"
191
+ }`}
192
+ title="Reset all responsive overrides for this viewport"
193
+ >
194
+ Reset Overrides
195
+ </button>
196
+ </div>
197
+ {hasAnyOverrides && (
198
+ <p className="text-[10px] text-[#076bff]/60 mt-1.5">
199
+ {hasColOverrides ? "Column layout" : ""}
200
+ {hasColOverrides && hasSettingsOverrides ? " + " : ""}
201
+ {hasSettingsOverrides ? "settings" : ""}
202
+ {" "}overridden
203
+ </p>
204
+ )}
205
+ </div>
206
+ )}
207
+
208
+ {/* Presets — desktop only (changing structure from responsive would be confusing) */}
209
+ {!isResponsive && (
210
+ <SettingsSection title="Layout Preset" defaultOpen>
211
+ <PresetGrid section={section} />
212
+ <p className="text-[10px] text-neutral-400 mt-1.5">
213
+ {section.columns.length} column{section.columns.length !== 1 ? "s" : ""}
214
+ {" · "}
215
+ {section.columns.map((c) => c.span).join(" + ")}
216
+ {" / "}{settings.grid_columns}
217
+ </p>
218
+ </SettingsSection>
219
+ )}
220
+
221
+ {/* Gaps */}
222
+ <SettingsSection title="Grid Gaps" defaultOpen>
223
+ <SettingsField label={
224
+ <span>
225
+ Col Gap
226
+ {isResponsive && hasSectionV2SettingOverride(section, activeViewport, "col_gap") && (
227
+ <span className="ml-1 text-[9px] text-[#076bff]">overridden</span>
228
+ )}
229
+ </span>
230
+ }>
231
+ <div className="flex items-center gap-2">
232
+ <input
233
+ type="range"
234
+ min={0}
235
+ max={80}
236
+ step={4}
237
+ value={getGapValue("col_gap", 20)}
238
+ onChange={(e) => updateSettingResponsive("col_gap", parseInt(e.target.value))}
239
+ className="flex-1 accent-[#076bff]"
240
+ />
241
+ <span className="text-xs text-neutral-900 w-12 text-right">
242
+ {getGapValue("col_gap", 20)}px
243
+ </span>
244
+ {isResponsive && hasSectionV2SettingOverride(section, activeViewport, "col_gap") && (
245
+ <button
246
+ onClick={() => updateSettingResponsive("col_gap", undefined)}
247
+ className="text-[10px] text-neutral-400 hover:text-[var(--admin-error)] transition-colors"
248
+ >
249
+ Reset
250
+ </button>
251
+ )}
252
+ </div>
253
+ </SettingsField>
254
+
255
+ <SettingsField label={
256
+ <span>
257
+ Row Gap
258
+ {isResponsive && hasSectionV2SettingOverride(section, activeViewport, "row_gap") && (
259
+ <span className="ml-1 text-[9px] text-[#076bff]">overridden</span>
260
+ )}
261
+ </span>
262
+ }>
263
+ <div className="flex items-center gap-2">
264
+ <input
265
+ type="range"
266
+ min={0}
267
+ max={80}
268
+ step={4}
269
+ value={getGapValue("row_gap", 20)}
270
+ onChange={(e) => updateSettingResponsive("row_gap", parseInt(e.target.value))}
271
+ className="flex-1 accent-[#076bff]"
272
+ />
273
+ <span className="text-xs text-neutral-900 w-12 text-right">
274
+ {getGapValue("row_gap", 20)}px
275
+ </span>
276
+ {isResponsive && hasSectionV2SettingOverride(section, activeViewport, "row_gap") && (
277
+ <button
278
+ onClick={() => updateSettingResponsive("row_gap", undefined)}
279
+ className="text-[10px] text-neutral-400 hover:text-[var(--admin-error)] transition-colors"
280
+ >
281
+ Reset
282
+ </button>
283
+ )}
284
+ </div>
285
+ </SettingsField>
286
+ </SettingsSection>
287
+
288
+ {/* Appearance */}
289
+ <SettingsSection title="Appearance">
290
+ <SettingsField label="Background">
291
+ <ColorSwatchPicker
292
+ value={getSectionV2SettingValue(section, activeViewport, "background_color", "")}
293
+ onChange={(hex) => updateSettingResponsive("background_color", hex)}
294
+ swatches={paletteSwatches}
295
+ />
296
+ </SettingsField>
297
+
298
+ {getSectionV2SettingValue(section, activeViewport, "background_color", "") && (
299
+ <SettingsField label="Opacity">
300
+ <div className="flex items-center gap-2">
301
+ <input
302
+ type="range"
303
+ min={0}
304
+ max={100}
305
+ value={getSectionV2SettingValue(section, activeViewport, "background_opacity", 100)}
306
+ onChange={(e) => updateSettingResponsive("background_opacity", parseInt(e.target.value))}
307
+ className="flex-1 accent-[#076bff]"
308
+ />
309
+ <span className="text-xs text-neutral-900 w-10 text-right">
310
+ {getSectionV2SettingValue(section, activeViewport, "background_opacity", 100)}%
311
+ </span>
312
+ </div>
313
+ </SettingsField>
314
+ )}
315
+ </SettingsSection>
316
+
317
+ </>
318
+ );
319
+ }
320
+
321
+ // Re-export extracted components for backward compatibility
322
+ export { SectionV2LayoutTab } from "./SectionV2LayoutTab";
323
+ export { SectionV2AnimationTab } from "./SectionV2AnimationTab";
@@ -0,0 +1,51 @@
1
+ "use client";
2
+
3
+ /**
4
+ * TRBLInputs — Shared 4-field input row for Top/Right/Bottom/Left values.
5
+ * Used by both LayoutTab (row spacing/offset) and BlockLayoutTab (block spacing/offset).
6
+ *
7
+ * Session 65: Extracted from LayoutTab.tsx.
8
+ */
9
+
10
+ import { useBuilderStore } from "../../../lib/builder/store";
11
+
12
+ export function TRBLInputs({
13
+ top,
14
+ right,
15
+ bottom,
16
+ left,
17
+ onChange,
18
+ }: {
19
+ top: string;
20
+ right: string;
21
+ bottom: string;
22
+ left: string;
23
+ onChange: (field: "top" | "right" | "bottom" | "left", value: string) => void;
24
+ }) {
25
+ const store = useBuilderStore();
26
+ const fields = [
27
+ { key: "top" as const, label: "TOP", value: top },
28
+ { key: "right" as const, label: "RIGHT", value: right },
29
+ { key: "bottom" as const, label: "BOTTOM", value: bottom },
30
+ { key: "left" as const, label: "LEFT", value: left },
31
+ ];
32
+
33
+ return (
34
+ <div className="grid grid-cols-4 gap-1.5">
35
+ {fields.map((f) => (
36
+ <div key={f.key} className="flex flex-col items-center gap-1">
37
+ <span className="text-[9px] text-neutral-300 uppercase tracking-wider">
38
+ {f.label}
39
+ </span>
40
+ <input
41
+ type="number"
42
+ value={f.value || "0"}
43
+ onFocus={() => store._pushSnapshot()}
44
+ onChange={(e) => onChange(f.key, e.target.value)}
45
+ className="w-full rounded-lg border border-transparent bg-[#f5f5f5] px-1.5 py-[6px] text-xs text-neutral-900 text-center outline-none transition-all hover:bg-[#efefef] focus:bg-white focus:border-[#076bff] focus:shadow-[0_0_0_3px_rgba(7,107,255,0.06)]"
46
+ />
47
+ </div>
48
+ ))}
49
+ </div>
50
+ );
51
+ }