@morphika/webframe 0.1.0

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 +46 -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 +210 -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,12 @@
1
+ export { default as DndWrapper } from "./DndWrapper";
2
+ export { default as SortableRow } from "./SortableRow";
3
+ export { default as SortableBlock } from "./SortableBlock";
4
+ export { default as SectionTypePicker } from "./SectionTypePicker";
5
+ export { default as BlockTypePicker } from "./BlockTypePicker";
6
+ export { default as SettingsPanel } from "./SettingsPanel";
7
+ export { default as BuilderCanvas } from "./BuilderCanvas";
8
+ export { default as SectionV2Canvas } from "./SectionV2Canvas";
9
+ export { default as ParallaxGroupCanvas } from "./ParallaxGroupCanvas";
10
+ export { default as SectionV2Column } from "./SectionV2Column";
11
+ export { default as CanvasToolbar } from "./CanvasToolbar";
12
+ export { makeRowId, makeBlockId, makeColumnDroppableId } from "./DndWrapper";
@@ -0,0 +1,38 @@
1
+ "use client";
2
+
3
+ import type { ButtonBlock } from "../../../lib/sanity/types";
4
+
5
+ export default function LiveButtonPreview({ block }: { block: ButtonBlock }) {
6
+ const sizeClasses: Record<string, string> = {
7
+ small: "px-4 py-2 text-xs",
8
+ medium: "px-6 py-3 text-sm",
9
+ large: "px-8 py-4 text-base",
10
+ };
11
+
12
+ const styleClasses: Record<string, string> = {
13
+ primary: "bg-white text-black hover:bg-neutral-200",
14
+ secondary: "bg-neutral-800 text-white hover:bg-neutral-700",
15
+ outline: "border border-white text-white hover:bg-white/10",
16
+ text: "text-white underline underline-offset-4 hover:text-neutral-300",
17
+ };
18
+
19
+ const align = block.alignment || "left";
20
+
21
+ return (
22
+ <div
23
+ style={{
24
+ textAlign: align,
25
+ width: block.full_width ? "100%" : undefined,
26
+ }}
27
+ >
28
+ <span
29
+ className={`inline-block rounded-lg cursor-pointer transition-colors ${
30
+ sizeClasses[block.size || "medium"]
31
+ } ${styleClasses[block.style || "primary"]}`}
32
+ style={{ width: block.full_width ? "100%" : undefined, textAlign: "center" }}
33
+ >
34
+ {block.text || "Button"}
35
+ </span>
36
+ </div>
37
+ );
38
+ }
@@ -0,0 +1,146 @@
1
+ "use client";
2
+
3
+ import { adminAssetUrl } from "../../../lib/assets";
4
+ import { ThumbBadge } from "./shared";
5
+ import type { CoverBlock } from "../../../lib/sanity/types";
6
+
7
+ export default function LiveCoverPreview({ block }: { block: CoverBlock }) {
8
+ const mediaSrc = block.media_path ? adminAssetUrl(block.media_path) : undefined;
9
+ const posterSrc = block.video_poster ? adminAssetUrl(block.video_poster) : undefined;
10
+ const isVideo = block.media_type === "video";
11
+
12
+ const overlayOpacity = (block.overlay_opacity ?? 50) / 100;
13
+ let overlayBg = "";
14
+ switch (block.overlay) {
15
+ case "dark":
16
+ overlayBg = `rgba(0,0,0,${overlayOpacity})`;
17
+ break;
18
+ case "light":
19
+ overlayBg = `rgba(255,255,255,${overlayOpacity})`;
20
+ break;
21
+ case "gradient-bottom":
22
+ overlayBg = `linear-gradient(to top, rgba(0,0,0,${overlayOpacity}) 0%, transparent 60%)`;
23
+ break;
24
+ case "gradient-top":
25
+ overlayBg = `linear-gradient(to bottom, rgba(0,0,0,${overlayOpacity}) 0%, transparent 60%)`;
26
+ break;
27
+ }
28
+
29
+ const alignMap: Record<string, string> = {
30
+ top: "flex-start",
31
+ center: "center",
32
+ bottom: "flex-end",
33
+ left: "flex-start",
34
+ right: "flex-end",
35
+ };
36
+
37
+ const textColor = block.text_color || "#ffffff";
38
+
39
+ // Resolve height — must match CoverBlockRenderer on public site
40
+ const height =
41
+ block.height === "custom" && block.custom_height
42
+ ? block.custom_height
43
+ : block.height || "100vh";
44
+
45
+ return (
46
+ <div
47
+ className="relative rounded overflow-hidden"
48
+ style={{
49
+ height,
50
+ minHeight: height,
51
+ display: "flex",
52
+ alignItems: alignMap[block.content_align_v || "center"],
53
+ justifyContent: alignMap[block.content_align_h || "center"],
54
+ color: textColor,
55
+ }}
56
+ >
57
+ {/* Background */}
58
+ {mediaSrc && !isVideo && (
59
+ <>
60
+ {/* eslint-disable-next-line @next/next/no-img-element */}
61
+ <img
62
+ src={mediaSrc}
63
+ alt=""
64
+ className="absolute inset-0 w-full h-full"
65
+ style={{
66
+ objectFit: (block.background_size || "cover") as "cover" | "contain" | "none",
67
+ objectPosition: block.background_position || "center center",
68
+ }}
69
+ />
70
+ {block.media_path && <ThumbBadge assetPath={block.media_path} />}
71
+ </>
72
+ )}
73
+ {mediaSrc && isVideo && (
74
+ <video
75
+ src={mediaSrc}
76
+ poster={posterSrc}
77
+ autoPlay
78
+ loop
79
+ muted
80
+ playsInline
81
+ className="absolute inset-0 w-full h-full"
82
+ style={{
83
+ objectFit: (block.background_size || "cover") as "cover" | "contain" | "none",
84
+ objectPosition: block.background_position || "center center",
85
+ }}
86
+ />
87
+ )}
88
+ {!mediaSrc && posterSrc && (
89
+ // eslint-disable-next-line @next/next/no-img-element
90
+ <img
91
+ src={posterSrc}
92
+ alt=""
93
+ className="absolute inset-0 w-full h-full object-cover"
94
+ />
95
+ )}
96
+ {!mediaSrc && !posterSrc && <div className="absolute inset-0 bg-neutral-900" />}
97
+
98
+ {/* Overlay */}
99
+ {overlayBg && (
100
+ <div
101
+ className="absolute inset-0"
102
+ style={{ background: overlayBg }}
103
+ />
104
+ )}
105
+
106
+ {/* Content */}
107
+ <div
108
+ className="relative z-10 flex flex-col gap-3 py-12"
109
+ style={{
110
+ maxWidth: block.content_max_width || "800px",
111
+ textAlign: (block.content_align_h || "center") as "left" | "center" | "right",
112
+ width: "100%",
113
+ paddingLeft: "var(--grid-padding, 24px)",
114
+ paddingRight: "var(--grid-padding, 24px)",
115
+ }}
116
+ >
117
+ {block.headline && (
118
+ <h1 className="text-2xl font-bold uppercase tracking-widest">
119
+ {block.headline}
120
+ </h1>
121
+ )}
122
+ {block.subheadline && (
123
+ <p className="text-xs opacity-80 uppercase tracking-wider">
124
+ {block.subheadline}
125
+ </p>
126
+ )}
127
+ {block.cta_button?.text && (
128
+ <div>
129
+ <span className="inline-block rounded-lg bg-white text-black text-xs px-6 py-3">
130
+ {block.cta_button.text}
131
+ </span>
132
+ </div>
133
+ )}
134
+ </div>
135
+
136
+ {/* Scroll indicator */}
137
+ {block.show_scroll_indicator && (
138
+ <div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 opacity-60">
139
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
140
+ <path d="M12 5v14M5 12l7 7 7-7" />
141
+ </svg>
142
+ </div>
143
+ )}
144
+ </div>
145
+ );
146
+ }
@@ -0,0 +1,123 @@
1
+ "use client";
2
+
3
+ import { adminAssetUrl, adminThumbUrl } from "../../../lib/assets";
4
+ import { ThumbBadge } from "./shared";
5
+ import type { ImageGridBlock } from "../../../lib/sanity/types";
6
+
7
+ // Row patterns for random grid (must match ImageGridBlockRenderer)
8
+ const ROW_PATTERNS_PREVIEW: Record<string, number[][]> = {
9
+ "small2-big4": [
10
+ [4, 4, 4], [2, 2, 4, 4], [4, 2, 2, 4], [4, 4, 2, 2],
11
+ [2, 4, 4, 2], [2, 2, 2, 2, 4], [4, 2, 2, 2, 2], [2, 2, 2, 2, 2, 2],
12
+ ],
13
+ "small3-big6": [
14
+ [6, 3, 3], [3, 6, 3], [3, 3, 6], [6, 6], [3, 3, 3, 3],
15
+ ],
16
+ "small4-big8": [
17
+ [8, 4], [4, 8], [4, 4, 4],
18
+ ],
19
+ };
20
+
21
+ function generateRandomGridSpans(
22
+ count: number,
23
+ mode: "small2-big4" | "small3-big6" | "small4-big8",
24
+ seed: number = 1
25
+ ): number[] {
26
+ const patterns = ROW_PATTERNS_PREVIEW[mode];
27
+ if (!patterns) return new Array(count).fill(Math.floor(12 / Math.max(count, 1)));
28
+
29
+ let s = seed;
30
+ const rand = () => { s = (s * 16807 + 12345) % 2147483647; return (s & 0x7fffffff) / 0x7fffffff; };
31
+ const spans: number[] = [];
32
+ let placed = 0;
33
+
34
+ while (placed < count) {
35
+ const imagesLeft = count - placed;
36
+ const valid = patterns.filter((p) => p.length <= imagesLeft);
37
+ if (valid.length === 0) {
38
+ const perImage = Math.floor(12 / imagesLeft);
39
+ let leftover = 12 - perImage * imagesLeft;
40
+ for (let i = 0; i < imagesLeft; i++) {
41
+ spans.push(perImage + (leftover > 0 ? 1 : 0));
42
+ if (leftover > 0) leftover--;
43
+ }
44
+ placed += imagesLeft;
45
+ break;
46
+ }
47
+ const pattern = valid[Math.floor(rand() * valid.length)];
48
+ for (const span of pattern) spans.push(span);
49
+ placed += pattern.length;
50
+ }
51
+ return spans;
52
+ }
53
+
54
+ export default function LiveImageGridPreview({ block }: { block: ImageGridBlock }) {
55
+ const images = block.images || [];
56
+ if (images.length === 0) {
57
+ return (
58
+ <div className="border border-dashed border-neutral-700 rounded bg-neutral-900/50 flex items-center justify-center py-8">
59
+ <span className="text-neutral-600 text-xs">Empty image grid</span>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ const hGutter = block.h_gutter ?? 10;
65
+ const vGutter = block.v_gutter ?? 10;
66
+ const imagesPerRow = block.images_per_row ?? 2;
67
+ const randomGrid = block.random_grid ?? "disabled";
68
+ const randomSeed = block.random_seed ?? 1;
69
+ const fit = block.object_fit ?? "cover";
70
+ const borderRadius = block.border_radius ? `${String(block.border_radius).replace(/px$/i, "")}px` : undefined;
71
+
72
+ const useRandom = randomGrid !== "disabled";
73
+ const spans = useRandom
74
+ ? generateRandomGridSpans(images.length, randomGrid as "small2-big4" | "small3-big6" | "small4-big8", randomSeed)
75
+ : null;
76
+ const uniformColSpan = 12 / imagesPerRow;
77
+
78
+ return (
79
+ <div
80
+ style={{
81
+ display: "grid",
82
+ gridTemplateColumns: "repeat(12, 1fr)",
83
+ columnGap: `${hGutter}px`,
84
+ rowGap: `${vGutter}px`,
85
+ }}
86
+ >
87
+ {images.map((img, i) => {
88
+ const colSpan = spans ? spans[i] : uniformColSpan;
89
+ return (
90
+ // minWidth:0 + overflow:hidden prevent grid items from
91
+ // expanding beyond their assigned columns in narrow frames
92
+ // (Phone/Tablet). Without this, the intrinsic image width
93
+ // forces the grid to overflow and collapse to 1 column.
94
+ <div key={i} style={{ gridColumn: `span ${colSpan}`, minWidth: 0, overflow: "hidden", position: "relative", borderRadius }}>
95
+ {/* eslint-disable-next-line @next/next/no-img-element */}
96
+ <img
97
+ src={adminThumbUrl(img.asset_path)}
98
+ alt={img.alt || ""}
99
+ style={{
100
+ width: "100%",
101
+ height: "100%",
102
+ objectFit: fit,
103
+ display: "block",
104
+ }}
105
+ loading="lazy"
106
+ onError={(e) => {
107
+ const imgEl = e.currentTarget;
108
+ const fullSrc = adminAssetUrl(img.asset_path);
109
+ if (imgEl.src !== fullSrc) {
110
+ if (process.env.NODE_ENV === "development") {
111
+ console.warn(`[thumbs] fallback: ${imgEl.src} → ${fullSrc}`);
112
+ }
113
+ imgEl.src = fullSrc;
114
+ }
115
+ }}
116
+ />
117
+ <ThumbBadge assetPath={img.asset_path} />
118
+ </div>
119
+ );
120
+ })}
121
+ </div>
122
+ );
123
+ }
@@ -0,0 +1,107 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { adminAssetUrl, adminThumbUrl } from "../../../lib/assets";
5
+ import { ThumbBadge } from "./shared";
6
+ import type { ImageBlock } from "../../../lib/sanity/types";
7
+
8
+ export default function LiveImagePreview({ block }: { block: ImageBlock }) {
9
+ const [imgError, setImgError] = useState(false);
10
+ const [imgLoaded, setImgLoaded] = useState(false);
11
+ const [useFallback, setUseFallback] = useState(false);
12
+
13
+ if (!block.asset_path) {
14
+ return (
15
+ <div className="border border-dashed border-neutral-300 rounded bg-neutral-50 flex items-center justify-center py-12">
16
+ <span className="text-neutral-400 text-xs">No image set</span>
17
+ </div>
18
+ );
19
+ }
20
+
21
+ const thumbSrc = adminThumbUrl(block.asset_path);
22
+ const fullSrc = adminAssetUrl(block.asset_path);
23
+ const src = useFallback ? fullSrc : thumbSrc;
24
+ const widthStyle =
25
+ block.width === "contained"
26
+ ? "75%"
27
+ : block.width === "small"
28
+ ? "50%"
29
+ : "100%";
30
+
31
+ const aspectMap: Record<string, string> = {
32
+ "16:9": "16/9",
33
+ "4:3": "4/3",
34
+ "1:1": "1/1",
35
+ "21:9": "21/9",
36
+ };
37
+
38
+ return (
39
+ <div style={{ width: widthStyle, margin: block.width !== "full" ? "0 auto" : undefined }}>
40
+ {imgError ? (
41
+ <div
42
+ className="border border-dashed border-red-300 rounded bg-red-50 flex flex-col items-center justify-center py-8 gap-2"
43
+ style={{
44
+ aspectRatio: block.aspect_ratio && block.aspect_ratio !== "auto"
45
+ ? aspectMap[block.aspect_ratio]
46
+ : undefined,
47
+ }}
48
+ >
49
+ <span className="text-red-400 text-lg">{"\u26A0"}</span>
50
+ <span className="text-red-400 text-xs">Image failed to load</span>
51
+ <span className="text-red-300 text-[10px] max-w-[200px] truncate" title={block.asset_path}>
52
+ {block.asset_path}
53
+ </span>
54
+ </div>
55
+ ) : (
56
+ <div className="relative" style={{ borderRadius: block.border_radius ? `${String(block.border_radius).replace(/px$/i, "")}px` : undefined, overflow: "hidden" }}>
57
+ {!imgLoaded && (
58
+ <div
59
+ className="border border-neutral-200 rounded bg-neutral-100 flex items-center justify-center animate-pulse"
60
+ style={{
61
+ aspectRatio: block.aspect_ratio && block.aspect_ratio !== "auto"
62
+ ? aspectMap[block.aspect_ratio]
63
+ : "16/9",
64
+ }}
65
+ >
66
+ <span className="text-neutral-300 text-xs">Loading...</span>
67
+ </div>
68
+ )}
69
+ {/* eslint-disable-next-line @next/next/no-img-element */}
70
+ <img
71
+ src={src}
72
+ alt={block.alt || ""}
73
+ onLoad={() => setImgLoaded(true)}
74
+ onError={() => {
75
+ if (!useFallback) {
76
+ if (process.env.NODE_ENV === "development") {
77
+ console.warn(`[thumbs] fallback: ${thumbSrc} → ${fullSrc}`);
78
+ }
79
+ setUseFallback(true);
80
+ } else {
81
+ setImgError(true);
82
+ }
83
+ }}
84
+ style={{
85
+ width: "100%",
86
+ aspectRatio: block.aspect_ratio && block.aspect_ratio !== "auto"
87
+ ? aspectMap[block.aspect_ratio]
88
+ : undefined,
89
+ objectFit: "cover",
90
+ position: imgLoaded ? "static" : "absolute",
91
+ top: 0,
92
+ left: 0,
93
+ opacity: imgLoaded ? 1 : 0,
94
+ }}
95
+ className={block.shadow ? "shadow-lg" : ""}
96
+ />
97
+ {imgLoaded && <ThumbBadge assetPath={block.asset_path} />}
98
+ </div>
99
+ )}
100
+ {block.caption && (
101
+ <p className="text-xs text-neutral-500 mt-2">
102
+ {block.caption}
103
+ </p>
104
+ )}
105
+ </div>
106
+ );
107
+ }