@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,404 @@
1
+ "use client";
2
+
3
+ import type { ContentBlock, BlockLayout } from "../../lib/sanity/types";
4
+ import type { EnterAnimationConfig, TypewriterConfig } from "../../lib/animation/enter-types";
5
+ import type { HoverEffectConfig } from "../../lib/animation/hover-effect-types";
6
+ import { isShaderPreset } from "../../lib/animation/hover-effect-presets";
7
+ import { resolveEnterAnimation } from "../../lib/animation/enter-resolve";
8
+ import { useAssetUrl } from "../../lib/contexts/AssetContext";
9
+ import { useViewport } from "../../lib/hooks/useViewport";
10
+ import { resolveBlock } from "../../lib/builder/responsive";
11
+ import { getBlockLayoutStyles, hasBlockLayout } from "../../lib/builder/layout-styles";
12
+ import { hexToRgba } from "../../lib/color-utils";
13
+ import { BREAKPOINTS } from "../../lib/builder/constants";
14
+ import EnterAnimationWrapper from "./EnterAnimationWrapper";
15
+ import HoverAnimationWrapper from "./HoverAnimationWrapper";
16
+ import TypewriterWrapper from "./TypewriterWrapper";
17
+
18
+ import TextBlockRenderer, { getTextBlockStyles } from "./TextBlockRenderer";
19
+ import ImageBlockRenderer from "./ImageBlockRenderer";
20
+ import ImageGridBlockRenderer from "./ImageGridBlockRenderer";
21
+ import VideoBlockRenderer from "./VideoBlockRenderer";
22
+ import SpacerBlockRenderer from "./SpacerBlockRenderer";
23
+ import ButtonBlockRenderer from "./ButtonBlockRenderer";
24
+ import CoverBlockRenderer from "./CoverBlockRenderer";
25
+ import ProjectGridBlockRenderer from "./ProjectGridBlockRenderer";
26
+
27
+ /**
28
+ * Central block dispatcher for the public site.
29
+ *
30
+ * Resolves responsive overrides (tablet/phone) based on the current
31
+ * browser viewport width before passing the block to its renderer.
32
+ * This ensures all block types benefit from responsive overrides
33
+ * without each renderer needing to handle resolution individually.
34
+ *
35
+ * If the block has layout properties (spacing, background, offset, border),
36
+ * wraps the rendered block in a <div> with those styles applied.
37
+ * Layout responsive overrides (tablet/phone) generate @media CSS via
38
+ * buildBlockLayoutResponsiveCss().
39
+ *
40
+ * Enter animation: receives cascade from parent (page → section → column).
41
+ * Block-level enter_animation overrides the cascade entirely.
42
+ * Hover animation: block-level only, no cascade. Reads directly from
43
+ * block.hover_effect.
44
+ *
45
+ * Breakpoints match the builder device frame widths:
46
+ * Desktop: > 810px
47
+ * Tablet: 390–810px
48
+ * Phone: < 390px
49
+ */
50
+
51
+ // ── Block layout responsive CSS generation ──
52
+
53
+ /**
54
+ * Build CSS rules for block LAYOUT overrides (spacing, border, background) for a viewport.
55
+ * These rules target the inner `.blk-layout-{key}` div inside BlockRenderer.
56
+ */
57
+ function buildBlockLayoutOverrideRules(
58
+ overrides?: Partial<BlockLayout>
59
+ ): string[] {
60
+ if (!overrides) return [];
61
+ const rules: string[] = [];
62
+
63
+ // px-value fields (excluding offset — vertical offset conflicts with alignment margin-auto)
64
+ const pxMap: Record<string, string> = {
65
+ spacing_top: "padding-top",
66
+ spacing_right: "padding-right",
67
+ spacing_bottom: "padding-bottom",
68
+ spacing_left: "padding-left",
69
+ offset_top: "margin-top",
70
+ offset_right: "margin-right",
71
+ offset_bottom: "margin-bottom",
72
+ offset_left: "margin-left",
73
+ border_radius: "border-radius",
74
+ };
75
+
76
+ for (const [field, cssProp] of Object.entries(pxMap)) {
77
+ const val = (overrides as Record<string, unknown>)[field];
78
+ if (val !== undefined && val !== null && val !== "") {
79
+ rules.push(`${cssProp}:${val}px!important`);
80
+ }
81
+ }
82
+
83
+ // Border width — side-aware
84
+ if (overrides.border_width !== undefined && overrides.border_width !== null && overrides.border_width !== "") {
85
+ const bw = overrides.border_width;
86
+ const sides = overrides.border_sides || "all";
87
+ switch (sides) {
88
+ case "top":
89
+ rules.push(`border-top-width:${bw}px!important`);
90
+ break;
91
+ case "right":
92
+ rules.push(`border-right-width:${bw}px!important`);
93
+ break;
94
+ case "bottom":
95
+ rules.push(`border-bottom-width:${bw}px!important`);
96
+ break;
97
+ case "left":
98
+ rules.push(`border-left-width:${bw}px!important`);
99
+ break;
100
+ case "top-bottom":
101
+ rules.push(`border-top-width:${bw}px!important`);
102
+ rules.push(`border-bottom-width:${bw}px!important`);
103
+ break;
104
+ case "left-right":
105
+ rules.push(`border-left-width:${bw}px!important`);
106
+ rules.push(`border-right-width:${bw}px!important`);
107
+ break;
108
+ default:
109
+ rules.push(`border-width:${bw}px!important`);
110
+ break;
111
+ }
112
+ }
113
+
114
+ // Border color (no px)
115
+ if (overrides.border_color) {
116
+ rules.push(`border-color:${overrides.border_color}!important`);
117
+ }
118
+
119
+ // Border style (no px)
120
+ if (overrides.border_style) {
121
+ rules.push(`border-style:${overrides.border_style}!important`);
122
+ }
123
+
124
+ // Background color (with opacity support)
125
+ if (overrides.background_color) {
126
+ const opacity = overrides.background_opacity;
127
+ if (opacity !== undefined && opacity < 100) {
128
+ const rgba = hexToRgba(overrides.background_color, opacity / 100);
129
+ rules.push(`background-color:${rgba}!important`);
130
+ } else {
131
+ rules.push(`background-color:${overrides.background_color}!important`);
132
+ }
133
+ } else if (overrides.background_opacity !== undefined) {
134
+ // Opacity-only override (color inherited from desktop) — handled at render via resolveBlock
135
+ }
136
+
137
+ return rules;
138
+ }
139
+
140
+ /**
141
+ * Build CSS rules for block ALIGNMENT overrides (h-align, v-align) for a viewport.
142
+ * These rules target the outer `.blk-wrap-{key}` wrapper div — the direct flex child
143
+ * of the column — where align-self actually takes effect.
144
+ *
145
+ * NOTE: Vertical alignment (align_v) is NOT handled here — it's applied as
146
+ * justify-content on the column flex container at render time. Responsive vertical
147
+ * alignment changes require column-level CSS which the renderers handle.
148
+ */
149
+ function buildBlockAlignOverrideRules(
150
+ overrides?: Partial<BlockLayout>
151
+ ): string[] {
152
+ if (!overrides) return [];
153
+ const rules: string[] = [];
154
+
155
+ // Horizontal alignment → align-self within flex-col parent
156
+ if (overrides.align_h) {
157
+ const alignSelfMap: Record<string, string> = {
158
+ left: "flex-start",
159
+ center: "center",
160
+ right: "flex-end",
161
+ };
162
+ rules.push(`align-self:${alignSelfMap[overrides.align_h] || "flex-start"}!important`);
163
+ if (overrides.align_h === "left") {
164
+ rules.push("width:100%!important");
165
+ } else {
166
+ rules.push("width:auto!important");
167
+ }
168
+ }
169
+
170
+ return rules;
171
+ }
172
+
173
+ /**
174
+ * Generate responsive CSS <style> content for a block's layout AND alignment overrides.
175
+ */
176
+ function buildBlockLayoutResponsiveCss(block: ContentBlock): string | null {
177
+ const responsive = (block as unknown as Record<string, unknown>).responsive as
178
+ | Record<string, Record<string, unknown>>
179
+ | undefined;
180
+ if (!responsive) return null;
181
+
182
+ const key = block._key;
183
+ const tabletLayout = responsive.tablet?.layout as Partial<BlockLayout> | undefined;
184
+ const phoneLayout = responsive.phone?.layout as Partial<BlockLayout> | undefined;
185
+
186
+ const tabletLayoutRules = buildBlockLayoutOverrideRules(tabletLayout);
187
+ const phoneLayoutRules = buildBlockLayoutOverrideRules(phoneLayout);
188
+
189
+ const tabletAlignRules = buildBlockAlignOverrideRules(tabletLayout);
190
+ const phoneAlignRules = buildBlockAlignOverrideRules(phoneLayout);
191
+
192
+ const hasAny = tabletLayoutRules.length + phoneLayoutRules.length +
193
+ tabletAlignRules.length + phoneAlignRules.length > 0;
194
+ if (!hasAny) return null;
195
+
196
+ let css = "";
197
+
198
+ if (tabletLayoutRules.length > 0 || tabletAlignRules.length > 0) {
199
+ const parts: string[] = [];
200
+ if (tabletLayoutRules.length > 0) {
201
+ parts.push(`.blk-layout-${key}{${tabletLayoutRules.join(";")}}`);
202
+ }
203
+ if (tabletAlignRules.length > 0) {
204
+ parts.push(`.blk-wrap-${key}{${tabletAlignRules.join(";")}}`);
205
+ }
206
+ css += `@media(max-width:${BREAKPOINTS.tablet}px){${parts.join("")}}`;
207
+ }
208
+
209
+ if (phoneLayoutRules.length > 0 || phoneAlignRules.length > 0) {
210
+ const parts: string[] = [];
211
+ if (phoneLayoutRules.length > 0) {
212
+ parts.push(`.blk-layout-${key}{${phoneLayoutRules.join(";")}}`);
213
+ }
214
+ if (phoneAlignRules.length > 0) {
215
+ parts.push(`.blk-wrap-${key}{${phoneAlignRules.join(";")}}`);
216
+ }
217
+ css += `@media(max-width:${BREAKPOINTS.phone}px){${parts.join("")}}`;
218
+ }
219
+
220
+ return css;
221
+ }
222
+
223
+ // ── Main component ──
224
+
225
+ interface BlockRendererProps {
226
+ block: ContentBlock;
227
+ /** Column-level enter animation (from SectionV2Renderer) */
228
+ columnEnterAnimation?: EnterAnimationConfig;
229
+ /** Section-level enter animation (from section settings) */
230
+ sectionEnterAnimation?: EnterAnimationConfig;
231
+ /** Page-level enter animation (from page_settings) */
232
+ pageEnterAnimation?: EnterAnimationConfig;
233
+ }
234
+
235
+ export default function BlockRenderer({
236
+ block,
237
+ columnEnterAnimation,
238
+ sectionEnterAnimation,
239
+ pageEnterAnimation,
240
+ }: BlockRendererProps) {
241
+ const viewport = useViewport();
242
+ const resolveAsset = useAssetUrl();
243
+ const resolved = resolveBlock(block, viewport);
244
+
245
+ let content: React.ReactNode;
246
+
247
+ switch (resolved._type) {
248
+ case "textBlock":
249
+ content = <TextBlockRenderer block={resolved} />;
250
+ break;
251
+ case "imageBlock":
252
+ content = <ImageBlockRenderer block={resolved} />;
253
+ break;
254
+ case "imageGridBlock":
255
+ content = <ImageGridBlockRenderer block={resolved} />;
256
+ break;
257
+ case "videoBlock":
258
+ content = <VideoBlockRenderer block={resolved} />;
259
+ break;
260
+ case "spacerBlock":
261
+ content = <SpacerBlockRenderer block={resolved} />;
262
+ break;
263
+ case "buttonBlock":
264
+ content = <ButtonBlockRenderer block={resolved} />;
265
+ break;
266
+ case "coverBlock":
267
+ content = <CoverBlockRenderer block={resolved} />;
268
+ break;
269
+ case "projectGridBlock":
270
+ content = <ProjectGridBlockRenderer block={resolved as import("../../lib/sanity/types").ProjectGridBlock} />;
271
+ break;
272
+ default:
273
+ if (process.env.NODE_ENV === "development") {
274
+ content = (
275
+ <div className="border border-dashed border-brand-secondary/50 p-4 font-mono text-xs text-brand-secondary">
276
+ Unknown block type: {(resolved as ContentBlock)._type}
277
+ </div>
278
+ );
279
+ } else {
280
+ return null;
281
+ }
282
+ }
283
+
284
+ // ── Resolve enter animation once (used both for typewriter early-wrap and normal path) ──
285
+ const blockEnterAnim = (resolved as unknown as Record<string, unknown>).enter_animation as
286
+ | EnterAnimationConfig
287
+ | undefined;
288
+ const resolvedEnter = resolveEnterAnimation(
289
+ blockEnterAnim,
290
+ columnEnterAnimation,
291
+ sectionEnterAnimation,
292
+ pageEnterAnimation,
293
+ );
294
+ const isTypewriter = resolvedEnter?.preset === "typewriter" && resolved._type === "textBlock";
295
+
296
+ // ── Typewriter: wrap BEFORE layout so padding applies to the animation ──
297
+ // Must be before the layout wrapper so that block padding/background wraps
298
+ // the TypewriterWrapper. Single-phase: rich text with per-character animation,
299
+ // swaps to standard PortableText children after completion.
300
+ if (isTypewriter && resolvedEnter) {
301
+ const twConfig = (resolved as unknown as Record<string, unknown>).typewriter_config as
302
+ | TypewriterConfig
303
+ | undefined;
304
+ const textBlock = resolved as import("../../lib/sanity/types").TextBlock;
305
+ const { className: textClassName, style: textStyle } = getTextBlockStyles(textBlock);
306
+ content = (
307
+ <TypewriterWrapper
308
+ portableText={textBlock.text}
309
+ config={twConfig}
310
+ delay={resolvedEnter.delay}
311
+ textClassName={textClassName}
312
+ textStyle={textStyle}
313
+ >
314
+ {content}
315
+ </TypewriterWrapper>
316
+ );
317
+ }
318
+
319
+ // Wrap in layout div if block has layout properties set (spacing, background, border).
320
+ const layout = (resolved as unknown as Record<string, unknown>).layout as ContentBlock["layout"] | undefined;
321
+ if (hasBlockLayout(layout)) {
322
+ const layoutStyles = getBlockLayoutStyles(layout, process.env.NEXT_PUBLIC_ASSET_BASE_URL);
323
+ const responsiveCss = buildBlockLayoutResponsiveCss(block);
324
+
325
+ content = (
326
+ <>
327
+ {responsiveCss && (
328
+ <style dangerouslySetInnerHTML={{ __html: responsiveCss }} />
329
+ )}
330
+ <div
331
+ className={responsiveCss ? `blk-layout-${block._key}` : undefined}
332
+ style={layoutStyles}
333
+ >
334
+ {content}
335
+ </div>
336
+ </>
337
+ );
338
+ } else {
339
+ const responsiveCss = buildBlockLayoutResponsiveCss(block);
340
+ if (responsiveCss) {
341
+ content = (
342
+ <>
343
+ <style dangerouslySetInnerHTML={{ __html: responsiveCss }} />
344
+ <div className={`blk-layout-${block._key}`}>{content}</div>
345
+ </>
346
+ );
347
+ }
348
+ }
349
+
350
+ // ── Enter animation: apply non-typewriter presets after layout wrapper ──
351
+ // Typewriter was already applied BEFORE the layout wrapper (above) so that
352
+ // block padding wraps both phases. All other presets wrap after layout.
353
+ if (resolvedEnter && resolvedEnter.preset !== "none" && !isTypewriter) {
354
+ content = (
355
+ <EnterAnimationWrapper config={resolvedEnter}>
356
+ {content}
357
+ </EnterAnimationWrapper>
358
+ );
359
+ }
360
+
361
+ // ── Hover animation: block-level only, no cascade ──
362
+ const blockHoverEffect = (resolved as unknown as Record<string, unknown>).hover_effect as
363
+ | HoverEffectConfig
364
+ | undefined;
365
+ if (blockHoverEffect?.preset && blockHoverEffect.preset !== "none") {
366
+ // Extract image src for shader presets (ripple, rgb-shift, pixelate)
367
+ let shaderSrc: string | undefined;
368
+ let shaderBorderRadius: string | undefined;
369
+ const isShader = isShaderPreset(blockHoverEffect.preset);
370
+ if (isShader) {
371
+ if (resolved._type === "imageBlock") {
372
+ shaderSrc = resolveAsset((resolved as import("../../lib/sanity/types").ImageBlock).asset_path);
373
+ const br = (resolved as import("../../lib/sanity/types").ImageBlock).border_radius;
374
+ if (br) shaderBorderRadius = `${String(br).replace(/px$/i, "")}px`;
375
+ } else if (resolved._type === "coverBlock") {
376
+ const mediaPath = (resolved as import("../../lib/sanity/types").CoverBlock).media_path;
377
+ if (mediaPath) shaderSrc = resolveAsset(mediaPath);
378
+ }
379
+ // Shader preset without image src: skip wrapper entirely
380
+ if (!shaderSrc) {
381
+ // No-op: can't apply shader without an image source
382
+ } else {
383
+ content = (
384
+ <HoverAnimationWrapper
385
+ config={blockHoverEffect}
386
+ shaderSrc={shaderSrc}
387
+ shaderContainerStyle={shaderBorderRadius ? { borderRadius: shaderBorderRadius } : undefined}
388
+ >
389
+ {content}
390
+ </HoverAnimationWrapper>
391
+ );
392
+ }
393
+ } else {
394
+ // CSS hover preset — always safe to wrap
395
+ content = (
396
+ <HoverAnimationWrapper config={blockHoverEffect}>
397
+ {content}
398
+ </HoverAnimationWrapper>
399
+ );
400
+ }
401
+ }
402
+
403
+ return <>{content}</>;
404
+ }
@@ -0,0 +1,52 @@
1
+ import type { ButtonBlock } from "../../lib/sanity/types";
2
+
3
+ const styleMap: Record<string, string> = {
4
+ primary:
5
+ "bg-brand-accent-alt text-brand-dark hover:bg-brand-accent",
6
+ secondary: "bg-brand-primary text-white hover:opacity-90",
7
+ outline:
8
+ "border border-brand-text text-brand-text hover:bg-brand-text hover:text-brand-dark",
9
+ text: "text-brand-accent-alt underline underline-offset-4 hover:text-brand-accent",
10
+ };
11
+
12
+ const sizeMap: Record<string, string> = {
13
+ small: "px-4 py-2 text-xs",
14
+ medium: "px-6 py-3 text-sm",
15
+ large: "px-8 py-4 text-base",
16
+ };
17
+
18
+ const alignmentMap: Record<string, string> = {
19
+ left: "justify-start",
20
+ center: "justify-center",
21
+ right: "justify-end",
22
+ };
23
+
24
+ export default function ButtonBlockRenderer({
25
+ block,
26
+ }: {
27
+ block: ButtonBlock;
28
+ }) {
29
+ const variant = styleMap[block.style ?? "primary"];
30
+ const size = sizeMap[block.size ?? "medium"];
31
+ const alignment = alignmentMap[block.alignment ?? "left"];
32
+
33
+ return (
34
+ <div className={`flex ${alignment}`}>
35
+ <a
36
+ href={block.url}
37
+ target={block.target ? "_blank" : undefined}
38
+ rel={block.target ? "noopener noreferrer" : undefined}
39
+ className={[
40
+ "inline-block font-mono uppercase tracking-wider transition-all",
41
+ variant,
42
+ size,
43
+ block.full_width ? "w-full text-center" : "",
44
+ ]
45
+ .filter(Boolean)
46
+ .join(" ")}
47
+ >
48
+ {block.text}
49
+ </a>
50
+ </div>
51
+ );
52
+ }
@@ -0,0 +1,239 @@
1
+ "use client";
2
+
3
+ import type { CoverBlock } from "../../lib/sanity/types";
4
+ import { useAssetUrl } from "../../lib/contexts/AssetContext";
5
+ import { handleImageRetry, handleVideoRetry } from "../../lib/asset-retry";
6
+
7
+ function getOverlayStyle(
8
+ overlay: CoverBlock["overlay"],
9
+ opacity: number
10
+ ): React.CSSProperties | null {
11
+ const alpha = opacity / 100;
12
+ switch (overlay) {
13
+ case "dark":
14
+ return { backgroundColor: `rgba(0,0,0,${alpha})` };
15
+ case "light":
16
+ return { backgroundColor: `rgba(255,255,255,${alpha})` };
17
+ case "gradient-bottom":
18
+ return {
19
+ background: `linear-gradient(to top, rgba(0,0,0,${alpha}) 0%, transparent 60%)`,
20
+ };
21
+ case "gradient-top":
22
+ return {
23
+ background: `linear-gradient(to bottom, rgba(0,0,0,${alpha}) 0%, transparent 60%)`,
24
+ };
25
+ default:
26
+ return null;
27
+ }
28
+ }
29
+
30
+ function getAlignItems(v: CoverBlock["content_align_v"]): string {
31
+ switch (v) {
32
+ case "top":
33
+ return "flex-start";
34
+ case "bottom":
35
+ return "flex-end";
36
+ default:
37
+ return "center";
38
+ }
39
+ }
40
+
41
+ function getJustify(h: CoverBlock["content_align_h"]): string {
42
+ switch (h) {
43
+ case "left":
44
+ return "flex-start";
45
+ case "right":
46
+ return "flex-end";
47
+ default:
48
+ return "center";
49
+ }
50
+ }
51
+
52
+ function getTextAlign(
53
+ h: CoverBlock["content_align_h"]
54
+ ): "left" | "center" | "right" {
55
+ return h || "center";
56
+ }
57
+
58
+ export default function CoverBlockRenderer({
59
+ block,
60
+ }: {
61
+ block: CoverBlock;
62
+ }) {
63
+ const height =
64
+ block.height === "custom" && block.custom_height
65
+ ? block.custom_height
66
+ : block.height || "100vh";
67
+
68
+ const mobileHeight =
69
+ block.mobile_height && block.mobile_height !== "same"
70
+ ? block.mobile_height
71
+ : null;
72
+
73
+ const overlayStyle = getOverlayStyle(
74
+ block.overlay ?? "none",
75
+ block.overlay_opacity ?? 50
76
+ );
77
+
78
+ const resolveAsset = useAssetUrl();
79
+ const mediaSrc = block.media_path ? resolveAsset(block.media_path) : undefined;
80
+ const posterSrc = block.video_poster
81
+ ? resolveAsset(block.video_poster)
82
+ : undefined;
83
+ const isVideo = block.media_type === "video";
84
+
85
+ const objectFit = block.background_size || "cover";
86
+ const objectPosition = block.background_position || "center center";
87
+
88
+ const textColor = block.text_color || "#ffffff";
89
+
90
+ const ctaStyleClasses: Record<string, string> = {
91
+ primary:
92
+ "bg-brand-accent-alt text-brand-dark hover:bg-brand-accent",
93
+ secondary:
94
+ "bg-brand-dark text-white hover:bg-neutral-800",
95
+ outline:
96
+ "border-2 border-current bg-transparent hover:bg-white/10",
97
+ text: "underline underline-offset-4 hover:opacity-70",
98
+ };
99
+
100
+ return (
101
+ <>
102
+ {/* Mobile height override via inline style tag */}
103
+ {mobileHeight && (
104
+ <style
105
+ dangerouslySetInnerHTML={{
106
+ __html: `@media(max-width:767px){.cover-block-${block._key}{height:${mobileHeight}!important;min-height:${mobileHeight}!important;}}`,
107
+ }}
108
+ />
109
+ )}
110
+
111
+ <section
112
+ className={`cover-block-${block._key} relative flex overflow-hidden`}
113
+ style={{
114
+ height,
115
+ minHeight: height,
116
+ alignItems: getAlignItems(block.content_align_v),
117
+ justifyContent: getJustify(block.content_align_h),
118
+ color: textColor,
119
+ }}
120
+ >
121
+ {/* Media layer — uses <img> instead of background-image for error recovery */}
122
+ {mediaSrc && !isVideo && (
123
+ /* eslint-disable-next-line @next/next/no-img-element */
124
+ <img
125
+ src={mediaSrc}
126
+ alt={block.headline || ""}
127
+ onError={handleImageRetry}
128
+ className="absolute inset-0 h-full w-full"
129
+ style={{
130
+ objectFit: objectFit as "cover" | "contain" | "none",
131
+ objectPosition,
132
+ }}
133
+ />
134
+ )}
135
+
136
+ {mediaSrc && isVideo && (
137
+ <video
138
+ src={mediaSrc}
139
+ poster={posterSrc}
140
+ autoPlay
141
+ loop
142
+ muted
143
+ playsInline
144
+ onError={handleVideoRetry}
145
+ className="absolute inset-0 h-full w-full"
146
+ style={{
147
+ objectFit: objectFit as "cover" | "contain" | "none",
148
+ objectPosition,
149
+ }}
150
+ />
151
+ )}
152
+
153
+ {/* Fallback: poster only if no media_path but video_poster exists */}
154
+ {!mediaSrc && posterSrc && (
155
+ /* eslint-disable-next-line @next/next/no-img-element */
156
+ <img
157
+ src={posterSrc}
158
+ alt=""
159
+ onError={handleImageRetry}
160
+ className="absolute inset-0 h-full w-full"
161
+ style={{ objectFit: "cover", objectPosition: "center" }}
162
+ />
163
+ )}
164
+
165
+ {/* No media fallback */}
166
+ {!mediaSrc && !posterSrc && (
167
+ <div className="absolute inset-0 bg-brand-dark" />
168
+ )}
169
+
170
+ {/* Overlay */}
171
+ {overlayStyle && <div className="absolute inset-0" style={overlayStyle} />}
172
+
173
+ {/* Content */}
174
+ <div
175
+ className="relative z-10 flex flex-col gap-4 py-12"
176
+ style={{
177
+ maxWidth: block.content_max_width || "800px",
178
+ textAlign: getTextAlign(block.content_align_h),
179
+ width: "100%",
180
+ paddingLeft: "var(--grid-padding, 24px)",
181
+ paddingRight: "var(--grid-padding, 24px)",
182
+ }}
183
+ >
184
+ {block.headline && (
185
+ <h1 className="font-mono text-4xl uppercase tracking-widest md:text-6xl lg:text-7xl">
186
+ {block.headline}
187
+ </h1>
188
+ )}
189
+
190
+ {block.subheadline && (
191
+ <p className="font-mono text-sm uppercase tracking-wider opacity-80 md:text-base">
192
+ {block.subheadline}
193
+ </p>
194
+ )}
195
+
196
+ {block.cta_button?.text && block.cta_button.url && (
197
+ <div>
198
+ <a
199
+ href={block.cta_button.url}
200
+ target={
201
+ block.cta_button.target_blank ? "_blank" : undefined
202
+ }
203
+ rel={
204
+ block.cta_button.target_blank
205
+ ? "noopener noreferrer"
206
+ : undefined
207
+ }
208
+ className={`inline-block px-6 py-3 font-mono text-sm uppercase tracking-wider transition ${
209
+ ctaStyleClasses[block.cta_button.style || "primary"]
210
+ }`}
211
+ >
212
+ {block.cta_button.text}
213
+ </a>
214
+ </div>
215
+ )}
216
+ </div>
217
+
218
+ {/* Scroll indicator */}
219
+ {block.show_scroll_indicator && (
220
+ <div className="absolute bottom-6 left-1/2 z-10 -translate-x-1/2 animate-bounce">
221
+ <svg
222
+ width="24"
223
+ height="24"
224
+ viewBox="0 0 24 24"
225
+ fill="none"
226
+ stroke="currentColor"
227
+ strokeWidth="2"
228
+ strokeLinecap="round"
229
+ strokeLinejoin="round"
230
+ aria-hidden="true"
231
+ >
232
+ <path d="M12 5v14M5 12l7 7 7-7" />
233
+ </svg>
234
+ </div>
235
+ )}
236
+ </section>
237
+ </>
238
+ );
239
+ }