@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,170 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SectionRenderer — renders a first-class PageSection on the public site.
5
+ *
6
+ * Unlike V2 sections (which have columns → blocks), SectionRenderer
7
+ * renders the section block directly — no row/column wrapper needed.
8
+ *
9
+ * Session 76: Created as part of the matryoshka → first-class section refactor.
10
+ * Session 120: Updated for new enter animation system.
11
+ */
12
+
13
+ import type { PageSection, SectionBlock } from "../../lib/sanity/types";
14
+ import type { EnterAnimationConfig } from "../../lib/animation/enter-types";
15
+ import { resolveEnterAnimation } from "../../lib/animation/enter-resolve";
16
+ import BlockRenderer from "./BlockRenderer";
17
+ import EnterAnimationWrapper from "./EnterAnimationWrapper";
18
+ import { getRowLayoutStyles, hexToRgba } from "../../lib/builder/layout-styles";
19
+ import { BREAKPOINTS } from "../../lib/builder/constants";
20
+
21
+ /**
22
+ * BUG-013 fix: Build responsive CSS overrides for section layout settings.
23
+ * Same approach as RowRenderer's buildRowResponsiveCss.
24
+ */
25
+ function buildSectionResponsiveCss(section: PageSection): string | null {
26
+ const responsive = section.responsive;
27
+ if (!responsive) return null;
28
+
29
+ const key = section._key;
30
+ const cssRules: string[] = [];
31
+
32
+ for (const [vp, breakpoint] of [["tablet", BREAKPOINTS.tablet], ["phone", BREAKPOINTS.phone]] as const) {
33
+ const overrides = responsive[vp];
34
+ if (!overrides) continue;
35
+ const rules: string[] = [];
36
+
37
+ // px-value properties
38
+ const pxMap: Record<string, string> = {
39
+ spacing_top: "padding-top", spacing_right: "padding-right",
40
+ spacing_bottom: "padding-bottom", spacing_left: "padding-left",
41
+ offset_top: "margin-top", offset_right: "margin-right",
42
+ offset_bottom: "margin-bottom", offset_left: "margin-left",
43
+ border_radius: "border-radius",
44
+ };
45
+
46
+ for (const [field, cssProp] of Object.entries(pxMap)) {
47
+ const val = overrides[field as keyof typeof overrides];
48
+ if (val !== undefined && val !== null && val !== "") {
49
+ rules.push(`${cssProp}:${val}px!important`);
50
+ }
51
+ }
52
+
53
+ // Border width — side-aware
54
+ if (overrides.border_width !== undefined && overrides.border_width !== null && overrides.border_width !== "") {
55
+ const bw = overrides.border_width;
56
+ const sides = (overrides.border_sides as string) || "all";
57
+ switch (sides) {
58
+ case "top":
59
+ rules.push(`border-top-width:${bw}px!important`);
60
+ break;
61
+ case "right":
62
+ rules.push(`border-right-width:${bw}px!important`);
63
+ break;
64
+ case "bottom":
65
+ rules.push(`border-bottom-width:${bw}px!important`);
66
+ break;
67
+ case "left":
68
+ rules.push(`border-left-width:${bw}px!important`);
69
+ break;
70
+ case "top-bottom":
71
+ rules.push(`border-top-width:${bw}px!important`);
72
+ rules.push(`border-bottom-width:${bw}px!important`);
73
+ break;
74
+ case "left-right":
75
+ rules.push(`border-left-width:${bw}px!important`);
76
+ rules.push(`border-right-width:${bw}px!important`);
77
+ break;
78
+ default:
79
+ rules.push(`border-width:${bw}px!important`);
80
+ break;
81
+ }
82
+ }
83
+
84
+ // Border color
85
+ if (overrides.border_color) {
86
+ rules.push(`border-color:${overrides.border_color}!important`);
87
+ }
88
+
89
+ // Border style
90
+ if (overrides.border_style && overrides.border_style !== "none") {
91
+ rules.push(`border-style:${overrides.border_style}!important`);
92
+ }
93
+
94
+ // Background color + opacity → rgba()
95
+ if (overrides.background_color) {
96
+ const opacity = overrides.background_opacity as number | undefined;
97
+ if (opacity !== undefined && opacity < 100) {
98
+ rules.push(`background-color:${hexToRgba(overrides.background_color as string, opacity / 100)}!important`);
99
+ } else {
100
+ rules.push(`background-color:${overrides.background_color}!important`);
101
+ }
102
+ }
103
+
104
+ // Background image + sub-properties
105
+ if (overrides.background_image) {
106
+ const imgUrl = process.env.NEXT_PUBLIC_ASSET_BASE_URL
107
+ ? `${(process.env.NEXT_PUBLIC_ASSET_BASE_URL as string).replace(/\/$/, "")}/${overrides.background_image}`
108
+ : overrides.background_image;
109
+ rules.push(`background-image:url(${imgUrl})!important`);
110
+ rules.push(`background-size:${overrides.background_size || "cover"}!important`);
111
+ rules.push(`background-position:${overrides.background_position || "center center"}!important`);
112
+ rules.push(`background-repeat:${overrides.background_repeat || "no-repeat"}!important`);
113
+ } else {
114
+ if (overrides.background_size) rules.push(`background-size:${overrides.background_size}!important`);
115
+ if (overrides.background_position) rules.push(`background-position:${overrides.background_position}!important`);
116
+ if (overrides.background_repeat) rules.push(`background-repeat:${overrides.background_repeat}!important`);
117
+ }
118
+
119
+ if (rules.length > 0) {
120
+ cssRules.push(`@media(max-width:${breakpoint}px){.section-${key}{${rules.join(";")}}}`);
121
+ }
122
+ }
123
+
124
+ return cssRules.length > 0 ? cssRules.join("") : null;
125
+ }
126
+
127
+ interface SectionRendererProps {
128
+ section: PageSection;
129
+ /** Page-level enter animation config (from page_settings.enter_animation) */
130
+ pageEnterAnimation?: EnterAnimationConfig;
131
+ }
132
+
133
+ export default function SectionRenderer({ section, pageEnterAnimation }: SectionRendererProps) {
134
+ const s = section.settings ?? {};
135
+
136
+ // Get the section block
137
+ const block = Array.isArray(section.block) ? section.block[0] : undefined;
138
+ if (!block) return null;
139
+
140
+ // Resolve enter animation (section settings → page default → none)
141
+ const sectionEnterConfig = s.enter_animation;
142
+ const resolvedEnter = resolveEnterAnimation(undefined, undefined, sectionEnterConfig, pageEnterAnimation);
143
+ const hasAnimation = resolvedEnter !== null && resolvedEnter.preset !== "none";
144
+
145
+ // Section layout styles (background, spacing, border, etc.)
146
+ const layoutStyles = getRowLayoutStyles(s as Record<string, unknown>);
147
+
148
+ // BUG-013 fix: build responsive CSS overrides for section
149
+ const responsiveCss = buildSectionResponsiveCss(section);
150
+
151
+ // Render the section block directly — no columns, no grid
152
+ // V1 PageSections contain section blocks (ProjectGrid, Parallax) which
153
+ // don't use the block-level enter/hover cascade — they have their own systems.
154
+ let content: React.ReactNode = (
155
+ <section className={`section-${section._key}`} style={layoutStyles}>
156
+ {responsiveCss && <style dangerouslySetInnerHTML={{ __html: responsiveCss }} />}
157
+ <BlockRenderer block={block} pageEnterAnimation={pageEnterAnimation} sectionEnterAnimation={sectionEnterConfig} />
158
+ </section>
159
+ );
160
+
161
+ if (hasAnimation && resolvedEnter) {
162
+ content = (
163
+ <EnterAnimationWrapper config={resolvedEnter}>
164
+ {content}
165
+ </EnterAnimationWrapper>
166
+ );
167
+ }
168
+
169
+ return content;
170
+ }
@@ -0,0 +1,330 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SectionV2Renderer — renders a first-class PageSectionV2 on the public site.
5
+ *
6
+ * Pure CSS Grid renderer with explicit column positions stored in Sanity.
7
+ * No layout computation needed — grid_column, grid_row, and span map directly
8
+ * to CSS Grid properties.
9
+ *
10
+ * Session 83: Created as part of V2 Grid System Phase 6.
11
+ * Session 120: Updated for new enter animation system (4-level cascade).
12
+ */
13
+
14
+ import type {
15
+ PageSectionV2,
16
+ SectionColumn,
17
+ ColumnOverride,
18
+ SectionV2Settings,
19
+ SectionV2ResponsiveOverride,
20
+ ContentBlock,
21
+ BlockLayout,
22
+ } from "../../lib/sanity/types";
23
+ import type { EnterAnimationConfig } from "../../lib/animation/enter-types";
24
+ import { resolveEnterAnimation } from "../../lib/animation/enter-resolve";
25
+ import BlockRenderer from "./BlockRenderer";
26
+ import EnterAnimationWrapper from "./EnterAnimationWrapper";
27
+ import { getRowLayoutStyles, getBlockAlignmentStyles, hasBlockAlignment, getColumnVerticalAlign, hexToRgba } from "../../lib/builder/layout-styles";
28
+ import { BREAKPOINTS } from "../../lib/builder/constants";
29
+
30
+ // ── Responsive CSS generation ──
31
+
32
+ /**
33
+ * Build CSS override rules for section-level settings at a specific viewport.
34
+ * Covers: col_gap, row_gap, spacing (TRBL), offset (TRBL), border, background.
35
+ */
36
+ function buildSettingsOverrideRules(
37
+ overrides?: Partial<SectionV2Settings>,
38
+ sectionKey?: string,
39
+ ): string[] {
40
+ if (!overrides) return [];
41
+ const rules: string[] = [];
42
+
43
+ // Grid-specific: col_gap → column-gap, row_gap → row-gap
44
+ if (overrides.col_gap !== undefined) {
45
+ rules.push(`column-gap:${overrides.col_gap}px!important`);
46
+ }
47
+ if (overrides.row_gap !== undefined) {
48
+ rules.push(`row-gap:${overrides.row_gap}px!important`);
49
+ }
50
+
51
+ // px-value properties (spacing, offset, border-radius)
52
+ const pxMap: Record<string, string> = {
53
+ spacing_top: "padding-top",
54
+ spacing_right: "padding-right",
55
+ spacing_bottom: "padding-bottom",
56
+ spacing_left: "padding-left",
57
+ offset_top: "margin-top",
58
+ offset_right: "margin-right",
59
+ offset_bottom: "margin-bottom",
60
+ offset_left: "margin-left",
61
+ border_radius: "border-radius",
62
+ };
63
+
64
+ for (const [field, cssProp] of Object.entries(pxMap)) {
65
+ const val = (overrides as Record<string, unknown>)[field];
66
+ if (val !== undefined && val !== null && val !== "") {
67
+ rules.push(`${cssProp}:${val}px!important`);
68
+ }
69
+ }
70
+
71
+ // Border width — side-aware
72
+ if (overrides.border_width !== undefined && overrides.border_width !== null && overrides.border_width !== "") {
73
+ const bw = overrides.border_width;
74
+ const sides = overrides.border_sides || "all";
75
+ switch (sides) {
76
+ case "top":
77
+ rules.push(`border-top-width:${bw}px!important`);
78
+ break;
79
+ case "right":
80
+ rules.push(`border-right-width:${bw}px!important`);
81
+ break;
82
+ case "bottom":
83
+ rules.push(`border-bottom-width:${bw}px!important`);
84
+ break;
85
+ case "left":
86
+ rules.push(`border-left-width:${bw}px!important`);
87
+ break;
88
+ case "top-bottom":
89
+ rules.push(`border-top-width:${bw}px!important`);
90
+ rules.push(`border-bottom-width:${bw}px!important`);
91
+ break;
92
+ case "left-right":
93
+ rules.push(`border-left-width:${bw}px!important`);
94
+ rules.push(`border-right-width:${bw}px!important`);
95
+ break;
96
+ default:
97
+ rules.push(`border-width:${bw}px!important`);
98
+ break;
99
+ }
100
+ }
101
+
102
+ // Border color
103
+ if (overrides.border_color) {
104
+ rules.push(`border-color:${overrides.border_color}!important`);
105
+ }
106
+
107
+ // Border style
108
+ if (overrides.border_style && overrides.border_style !== "none") {
109
+ rules.push(`border-style:${overrides.border_style}!important`);
110
+ }
111
+
112
+ // Background color + opacity
113
+ if (overrides.background_color) {
114
+ const opacity = overrides.background_opacity;
115
+ if (opacity !== undefined && opacity < 100) {
116
+ rules.push(`background-color:${hexToRgba(overrides.background_color, opacity / 100)}!important`);
117
+ } else {
118
+ rules.push(`background-color:${overrides.background_color}!important`);
119
+ }
120
+ }
121
+
122
+ return rules;
123
+ }
124
+
125
+ /**
126
+ * Build responsive column override CSS for a specific viewport.
127
+ * Each column override replaces grid-column and/or grid-row and/or span.
128
+ */
129
+ function buildColumnOverrideCss(
130
+ sectionKey: string,
131
+ columnOverrides: ColumnOverride[] | undefined,
132
+ breakpoint: number,
133
+ ): string {
134
+ if (!columnOverrides || columnOverrides.length === 0) return "";
135
+
136
+ const rules: string[] = [];
137
+ for (const co of columnOverrides) {
138
+ const colRules: string[] = [];
139
+ if (co.grid_column !== undefined && co.span !== undefined) {
140
+ colRules.push(`grid-column:${co.grid_column}/span ${co.span}!important`);
141
+ } else if (co.grid_column !== undefined) {
142
+ colRules.push(`grid-column-start:${co.grid_column}!important`);
143
+ } else if (co.span !== undefined) {
144
+ // span-only: need to reconstruct grid-column with original start
145
+ colRules.push(`grid-column-end:span ${co.span}!important`);
146
+ }
147
+ if (co.grid_row !== undefined) {
148
+ colRules.push(`grid-row:${co.grid_row}!important`);
149
+ }
150
+ if (colRules.length > 0) {
151
+ rules.push(`.sv2-col-${sectionKey}-${co._key}{${colRules.join(";")}}`);
152
+ }
153
+ }
154
+
155
+ return rules.length > 0
156
+ ? `@media(max-width:${breakpoint}px){${rules.join("")}}`
157
+ : "";
158
+ }
159
+
160
+ /**
161
+ * Generate all responsive CSS for a V2 section (section settings + column overrides).
162
+ */
163
+ function buildSectionV2ResponsiveCss(section: PageSectionV2): string | null {
164
+ const responsive = section.responsive;
165
+ if (!responsive) return null;
166
+
167
+ const key = section._key;
168
+ const cssParts: string[] = [];
169
+
170
+ for (const [vp, breakpoint] of [["tablet", BREAKPOINTS.tablet], ["phone", BREAKPOINTS.phone]] as const) {
171
+ const override = responsive[vp];
172
+ if (!override) continue;
173
+
174
+ // Section-level settings overrides
175
+ const settingsRules = buildSettingsOverrideRules(override.settings as Partial<SectionV2Settings> | undefined);
176
+ if (settingsRules.length > 0) {
177
+ cssParts.push(`@media(max-width:${breakpoint}px){.sv2-${key}{${settingsRules.join(";")}}}`);
178
+ }
179
+
180
+ // Column position/span overrides
181
+ const colCss = buildColumnOverrideCss(key, override.columns, breakpoint);
182
+ if (colCss) cssParts.push(colCss);
183
+ }
184
+
185
+ return cssParts.length > 0 ? cssParts.join("") : null;
186
+ }
187
+
188
+ // ── Main component ──
189
+
190
+ interface SectionV2RendererProps {
191
+ section: PageSectionV2;
192
+ /** Page-level enter animation config (from page_settings.enter_animation) */
193
+ pageEnterAnimation?: EnterAnimationConfig;
194
+ }
195
+
196
+ export default function SectionV2Renderer({ section, pageEnterAnimation }: SectionV2RendererProps) {
197
+ const s = section.settings;
198
+
199
+ const gridColumns = s.grid_columns || 12;
200
+ const colGap = s.col_gap ?? 20;
201
+ const rowGap = s.row_gap ?? 20;
202
+
203
+ // Resolve section-level enter animation (section → page → null)
204
+ const sectionEnterConfig = s.enter_animation;
205
+
206
+ // Stagger settings — stagger only applies when the section has an enter animation
207
+ const resolvedSectionEnter = resolveEnterAnimation(undefined, undefined, sectionEnterConfig, pageEnterAnimation);
208
+ const hasAnimation = resolvedSectionEnter !== null && resolvedSectionEnter.preset !== "none";
209
+
210
+ const stagger = s.stagger;
211
+ const staggerEnabled = stagger?.enabled && hasAnimation;
212
+ const staggerDelay = stagger?.delayPerChild ?? 100;
213
+ const staggerDirection = stagger?.direction ?? "left-to-right";
214
+
215
+ // Section-level layout styles (background, spacing, border, offset)
216
+ const layoutStyles = getRowLayoutStyles(s as unknown as Record<string, unknown>, process.env.NEXT_PUBLIC_ASSET_BASE_URL);
217
+
218
+ // Responsive CSS (section settings + column overrides)
219
+ const responsiveCss = buildSectionV2ResponsiveCss(section);
220
+
221
+ // Sort columns for stagger ordering
222
+ const sortedColumns = [...section.columns].sort((a, b) => {
223
+ if (a.grid_row !== b.grid_row) return a.grid_row - b.grid_row;
224
+ return a.grid_column - b.grid_column;
225
+ });
226
+
227
+ // Stagger index computation
228
+ const getStaggerIndex = (colIndex: number): number => {
229
+ if (!staggerEnabled) return 0;
230
+ return staggerDirection === "right-to-left"
231
+ ? sortedColumns.length - 1 - colIndex
232
+ : colIndex;
233
+ };
234
+
235
+ // Build section content
236
+ const sectionContent = (
237
+ <section
238
+ className={`sv2-${section._key}`}
239
+ style={layoutStyles}
240
+ >
241
+ {responsiveCss && <style dangerouslySetInnerHTML={{ __html: responsiveCss }} />}
242
+ <div
243
+ style={{
244
+ maxWidth: "var(--grid-width, 1445px)",
245
+ marginLeft: "auto",
246
+ marginRight: "auto",
247
+ width: "100%",
248
+ }}
249
+ >
250
+ <div
251
+ style={{
252
+ display: "grid",
253
+ gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
254
+ columnGap: `${colGap}px`,
255
+ rowGap: `${rowGap}px`,
256
+ }}
257
+ >
258
+ {sortedColumns.map((col, colIndex) => {
259
+ const staggerIdx = staggerEnabled ? getStaggerIndex(colIndex) : undefined;
260
+
261
+ // Column-level vertical alignment from blocks' align_v settings
262
+ const colJustify = getColumnVerticalAlign(col.blocks || []);
263
+
264
+ const columnContent = (
265
+ <div
266
+ key={col._key}
267
+ className={`sv2-col-${section._key}-${col._key}`}
268
+ style={{
269
+ gridColumn: `${col.grid_column} / span ${col.span}`,
270
+ gridRow: col.grid_row,
271
+ display: "flex",
272
+ flexDirection: "column",
273
+ ...(colJustify ? { justifyContent: colJustify } : {}),
274
+ height: "100%",
275
+ minWidth: 0,
276
+ overflow: "hidden",
277
+ }}
278
+ >
279
+ {(col.blocks || []).map((block) => {
280
+ const blockLayout = (block as unknown as Record<string, unknown>).layout as BlockLayout | undefined;
281
+ const alignStyles = hasBlockAlignment(blockLayout) ? getBlockAlignmentStyles(blockLayout) : undefined;
282
+ const hasHAlign = blockLayout?.align_h && blockLayout.align_h !== "left";
283
+ return (
284
+ <div key={block._key} className={`blk-wrap-${block._key}`} style={{ ...(!hasHAlign ? { width: "100%" } : {}), minWidth: 0, ...alignStyles }}>
285
+ <BlockRenderer
286
+ block={block}
287
+ columnEnterAnimation={col.enter_animation}
288
+ sectionEnterAnimation={sectionEnterConfig}
289
+ pageEnterAnimation={pageEnterAnimation}
290
+ />
291
+ </div>
292
+ );
293
+ })}
294
+ </div>
295
+ );
296
+
297
+ // When stagger is enabled, wrap each column individually with enter animation
298
+ if (staggerEnabled && staggerIdx !== undefined && resolvedSectionEnter) {
299
+ return (
300
+ <EnterAnimationWrapper
301
+ key={col._key}
302
+ config={resolvedSectionEnter}
303
+ staggerIndex={staggerIdx}
304
+ staggerDelay={staggerDelay}
305
+ >
306
+ {columnContent}
307
+ </EnterAnimationWrapper>
308
+ );
309
+ }
310
+
311
+ return columnContent;
312
+ })}
313
+ </div>
314
+ </div>
315
+ </section>
316
+ );
317
+
318
+ // Section-level enter animation (without stagger — wraps entire section)
319
+ let result: React.ReactNode = sectionContent;
320
+
321
+ if (hasAnimation && !staggerEnabled && resolvedSectionEnter) {
322
+ result = (
323
+ <EnterAnimationWrapper config={resolvedSectionEnter}>
324
+ {result}
325
+ </EnterAnimationWrapper>
326
+ );
327
+ }
328
+
329
+ return result;
330
+ }