@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,304 @@
1
+ "use client";
2
+
3
+ import { useState, useRef, useEffect, useCallback } from "react";
4
+ import type { VideoBlock } from "../../lib/sanity/types";
5
+ import { useAssetUrl } from "../../lib/contexts/AssetContext";
6
+ import { handleVideoRetry, handleImageRetry } from "../../lib/asset-retry";
7
+
8
+ /**
9
+ * VideoBlockRenderer — Optimized for bandwidth and playback speed.
10
+ *
11
+ * Optimizations:
12
+ * 1. Facade pattern for Vimeo/YouTube: shows poster + play button, loads iframe on click
13
+ * 2. Lazy mount: uses IntersectionObserver to defer loading until near viewport
14
+ * 3. Conditional preload: autoplay → "auto", no autoplay → "none"
15
+ * 4. loading="lazy" on iframes as progressive enhancement
16
+ */
17
+
18
+ // ============================================
19
+ // Constants
20
+ // ============================================
21
+
22
+ const widthStyleMap: Record<string, { width: string; margin?: string; minWidth: number }> = {
23
+ full: { width: "100%", minWidth: 0 },
24
+ contained: { width: "75%", margin: "0 auto", minWidth: 0 },
25
+ };
26
+
27
+ const aspectMap: Record<string, string> = {
28
+ "16:9": "56.25%",
29
+ "21:9": "42.86%",
30
+ "4:3": "75%",
31
+ auto: "56.25%",
32
+ };
33
+
34
+ // ============================================
35
+ // URL Extractors
36
+ // ============================================
37
+
38
+ function getVimeoId(url: string): string | null {
39
+ const match = url.match(/vimeo\.com\/(\d+)/);
40
+ return match ? match[1] : null;
41
+ }
42
+
43
+ function getYouTubeId(url: string): string | null {
44
+ const match = url.match(
45
+ /(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([\w-]+)/
46
+ );
47
+ return match ? match[1] : null;
48
+ }
49
+
50
+ // ============================================
51
+ // Hooks
52
+ // ============================================
53
+
54
+ /** Returns true once the element is within `rootMargin` of the viewport */
55
+ function useInView(rootMargin = "200px"): [React.RefObject<HTMLDivElement | null>, boolean] {
56
+ const ref = useRef<HTMLDivElement | null>(null);
57
+ const [inView, setInView] = useState(false);
58
+
59
+ useEffect(() => {
60
+ const el = ref.current;
61
+ if (!el || inView) return; // Once in view, stay in view
62
+
63
+ const observer = new IntersectionObserver(
64
+ ([entry]) => {
65
+ if (entry.isIntersecting) {
66
+ setInView(true);
67
+ observer.disconnect();
68
+ }
69
+ },
70
+ { rootMargin }
71
+ );
72
+
73
+ observer.observe(el);
74
+ return () => observer.disconnect();
75
+ }, [rootMargin, inView]);
76
+
77
+ return [ref, inView];
78
+ }
79
+
80
+ // ============================================
81
+ // Sub-components
82
+ // ============================================
83
+
84
+ /** Shared container with aspect ratio padding */
85
+ function AspectContainer({
86
+ paddingBottom,
87
+ children,
88
+ }: {
89
+ paddingBottom: string;
90
+ children: React.ReactNode;
91
+ }) {
92
+ return (
93
+ <div
94
+ style={{
95
+ position: "relative",
96
+ paddingBottom,
97
+ overflow: "hidden",
98
+ borderRadius: "inherit",
99
+ background: "#000",
100
+ lineHeight: 0,
101
+ fontSize: 0,
102
+ }}
103
+ >
104
+ {children}
105
+ </div>
106
+ );
107
+ }
108
+
109
+ /** Play button overlay for facade pattern */
110
+ function PlayButton({ onClick }: { onClick: () => void }) {
111
+ return (
112
+ <button
113
+ onClick={onClick}
114
+ aria-label="Play video"
115
+ style={{
116
+ position: "absolute",
117
+ inset: 0,
118
+ display: "flex",
119
+ alignItems: "center",
120
+ justifyContent: "center",
121
+ background: "rgba(0,0,0,0.3)",
122
+ border: "none",
123
+ cursor: "pointer",
124
+ transition: "background 0.2s",
125
+ zIndex: 1,
126
+ }}
127
+ onMouseEnter={(e) => {
128
+ (e.currentTarget as HTMLButtonElement).style.background = "rgba(0,0,0,0.5)";
129
+ }}
130
+ onMouseLeave={(e) => {
131
+ (e.currentTarget as HTMLButtonElement).style.background = "rgba(0,0,0,0.3)";
132
+ }}
133
+ >
134
+ <svg
135
+ width="68"
136
+ height="68"
137
+ viewBox="0 0 68 68"
138
+ fill="none"
139
+ xmlns="http://www.w3.org/2000/svg"
140
+ aria-hidden="true"
141
+ >
142
+ <circle cx="34" cy="34" r="34" fill="rgba(0,0,0,0.6)" />
143
+ <path d="M27 21L49 34L27 47V21Z" fill="white" />
144
+ </svg>
145
+ </button>
146
+ );
147
+ }
148
+
149
+ /** Poster image (Vimeo/YouTube thumbnail or custom poster) */
150
+ function PosterImage({ src, alt }: { src: string; alt: string }) {
151
+ return (
152
+ <img
153
+ src={src}
154
+ alt={alt}
155
+ loading="lazy"
156
+ onError={handleImageRetry}
157
+ style={{
158
+ position: "absolute",
159
+ inset: 0,
160
+ width: "100%",
161
+ height: "100%",
162
+ objectFit: "cover",
163
+ }}
164
+ />
165
+ );
166
+ }
167
+
168
+ // ============================================
169
+ // Embed Renderers (with facade)
170
+ // ============================================
171
+
172
+ function VimeoEmbed({ block, paddingBottom }: { block: VideoBlock; paddingBottom: string }) {
173
+ const vimeoId = getVimeoId(block.url_or_path);
174
+ const [activated, setActivated] = useState(block.autoplay ?? false);
175
+ const [ref, inView] = useInView();
176
+
177
+ const activate = useCallback(() => setActivated(true), []);
178
+
179
+ if (!vimeoId) return null;
180
+
181
+ // Vimeo thumbnail URL (medium quality — loads fast, good enough for facade)
182
+ const posterUrl = `https://vumbnail.com/${vimeoId}.jpg`;
183
+
184
+ return (
185
+ <div ref={ref}>
186
+ <AspectContainer paddingBottom={paddingBottom}>
187
+ {inView && activated ? (
188
+ <iframe
189
+ src={`https://player.vimeo.com/video/${vimeoId}?autoplay=1&loop=${block.loop ? 1 : 0}&muted=${block.muted ? 1 : 0}&controls=${block.controls !== false ? 1 : 0}&dnt=1`}
190
+ className="absolute inset-0 w-full h-full"
191
+ allow="autoplay; fullscreen; picture-in-picture"
192
+ allowFullScreen
193
+ loading="lazy"
194
+ title="Video"
195
+ style={{ border: "none" }}
196
+ />
197
+ ) : inView ? (
198
+ <>
199
+ <PosterImage src={posterUrl} alt="Video thumbnail" />
200
+ <PlayButton onClick={activate} />
201
+ </>
202
+ ) : null}
203
+ </AspectContainer>
204
+ </div>
205
+ );
206
+ }
207
+
208
+ function YouTubeEmbed({ block, paddingBottom }: { block: VideoBlock; paddingBottom: string }) {
209
+ const ytId = getYouTubeId(block.url_or_path);
210
+ const [activated, setActivated] = useState(block.autoplay ?? false);
211
+ const [ref, inView] = useInView();
212
+
213
+ const activate = useCallback(() => setActivated(true), []);
214
+
215
+ if (!ytId) return null;
216
+
217
+ // YouTube thumbnail (maxresdefault with fallback to hqdefault)
218
+ const posterUrl = `https://i.ytimg.com/vi/${ytId}/hqdefault.jpg`;
219
+
220
+ return (
221
+ <div ref={ref}>
222
+ <AspectContainer paddingBottom={paddingBottom}>
223
+ {inView && activated ? (
224
+ <iframe
225
+ src={`https://www.youtube.com/embed/${ytId}?autoplay=1&loop=${block.loop ? 1 : 0}&mute=${block.muted ? 1 : 0}&controls=${block.controls !== false ? 1 : 0}`}
226
+ className="absolute inset-0 w-full h-full"
227
+ allow="autoplay; fullscreen"
228
+ allowFullScreen
229
+ loading="lazy"
230
+ title="Video"
231
+ style={{ border: "none" }}
232
+ />
233
+ ) : inView ? (
234
+ <>
235
+ <PosterImage src={posterUrl} alt="Video thumbnail" />
236
+ <PlayButton onClick={activate} />
237
+ </>
238
+ ) : null}
239
+ </AspectContainer>
240
+ </div>
241
+ );
242
+ }
243
+
244
+ function NativeVideo({ block, paddingBottom, resolveAsset }: {
245
+ block: VideoBlock;
246
+ paddingBottom: string;
247
+ resolveAsset: (path: string) => string;
248
+ }) {
249
+ const [ref, inView] = useInView();
250
+
251
+ const src = block.video_type === "mp4"
252
+ ? resolveAsset(block.url_or_path)
253
+ : block.url_or_path;
254
+ const posterSrc = block.poster ? resolveAsset(block.poster) : undefined;
255
+
256
+ // Conditional preload: autoplay needs data immediately, others can wait
257
+ const preload = block.autoplay ? "auto" : "none";
258
+
259
+ return (
260
+ <div ref={ref}>
261
+ <AspectContainer paddingBottom={paddingBottom}>
262
+ {inView ? (
263
+ <video
264
+ src={src}
265
+ poster={posterSrc}
266
+ autoPlay={block.autoplay ?? false}
267
+ loop={block.loop ?? false}
268
+ muted={block.muted ?? true}
269
+ controls={block.controls !== false}
270
+ playsInline
271
+ preload={preload}
272
+ onError={handleVideoRetry}
273
+ className="absolute inset-0 w-full h-full object-cover"
274
+ />
275
+ ) : posterSrc ? (
276
+ <PosterImage src={posterSrc} alt="Video poster" />
277
+ ) : null}
278
+ </AspectContainer>
279
+ </div>
280
+ );
281
+ }
282
+
283
+ // ============================================
284
+ // Main Component
285
+ // ============================================
286
+
287
+ export default function VideoBlockRenderer({ block }: { block: VideoBlock }) {
288
+ const resolveAsset = useAssetUrl();
289
+ const widthStyle = widthStyleMap[block.width ?? "full"] || widthStyleMap.full;
290
+ const paddingBottom = aspectMap[block.aspect_ratio ?? "16:9"] || "56.25%";
291
+ const borderRadius = block.border_radius ? `${String(block.border_radius).replace(/px$/i, "")}px` : undefined;
292
+
293
+ return (
294
+ <div style={{ ...widthStyle, borderRadius, overflow: borderRadius ? "hidden" : undefined }}>
295
+ {block.video_type === "vimeo" ? (
296
+ <VimeoEmbed block={block} paddingBottom={paddingBottom} />
297
+ ) : block.video_type === "youtube" ? (
298
+ <YouTubeEmbed block={block} paddingBottom={paddingBottom} />
299
+ ) : (
300
+ <NativeVideo block={block} paddingBottom={paddingBottom} resolveAsset={resolveAsset} />
301
+ )}
302
+ </div>
303
+ );
304
+ }
@@ -0,0 +1,2 @@
1
+ export { default as BlockRenderer } from "./BlockRenderer";
2
+ export { default as PageRenderer } from "./PageRenderer";
@@ -0,0 +1,2 @@
1
+ // Re-export for backward compatibility — all code now lives in asset-browser/
2
+ export { default, AssetBrowserInline } from "./asset-browser";
@@ -0,0 +1,101 @@
1
+ "use client";
2
+
3
+ /**
4
+ * BlockLivePreview — renders blocks as they would appear on the public site.
5
+ *
6
+ * Used in both Design mode (content + subtle edit chrome on hover) and
7
+ * Preview mode (pure content, no chrome at all).
8
+ *
9
+ * This is a thin dispatcher that routes to focused sub-components
10
+ * in the `live-preview/` directory.
11
+ */
12
+
13
+ import { memo } from "react";
14
+ import { resolveBlock } from "../../lib/builder/responsive";
15
+ import { getBlockLayoutStyles, hasBlockLayout } from "../../lib/builder/layout-styles";
16
+ import type { DeviceViewport } from "../../lib/builder/types";
17
+ import type {
18
+ ContentBlock,
19
+ TextBlock,
20
+ ImageBlock,
21
+ ImageGridBlock,
22
+ VideoBlock,
23
+ SpacerBlock,
24
+ ButtonBlock,
25
+ CoverBlock,
26
+ ProjectGridBlock,
27
+ } from "../../lib/sanity/types";
28
+
29
+ import { LiveTextEditor } from "./live-preview";
30
+ import { LiveImagePreview } from "./live-preview";
31
+ import { LiveImageGridPreview } from "./live-preview";
32
+ import { LiveVideoPreview } from "./live-preview";
33
+ import { LiveSpacerPreview } from "./live-preview";
34
+ import { LiveButtonPreview } from "./live-preview";
35
+ import { LiveCoverPreview } from "./live-preview";
36
+ import { LiveProjectGridPreview } from "./live-preview";
37
+ import { LivePlaceholder } from "./live-preview";
38
+
39
+ // ============================================
40
+ // Main dispatcher
41
+ // ============================================
42
+
43
+ interface BlockLivePreviewProps {
44
+ block: ContentBlock;
45
+ /** Which viewport to render for. Defaults to "desktop". */
46
+ viewport?: DeviceViewport;
47
+ /** Whether inline editing is enabled (only for active frame, not read-only mirrors). */
48
+ editable?: boolean;
49
+ }
50
+
51
+ function BlockLivePreviewInner({ block, viewport = "desktop", editable = false }: BlockLivePreviewProps) {
52
+ // Merge responsive overrides for the target viewport
53
+ const resolved = resolveBlock(block, viewport);
54
+
55
+ let content: React.ReactNode;
56
+
57
+ switch (resolved._type) {
58
+ case "textBlock":
59
+ content = <LiveTextEditor block={resolved as TextBlock} editable={editable} />;
60
+ break;
61
+ case "imageBlock":
62
+ content = <LiveImagePreview block={resolved as ImageBlock} />;
63
+ break;
64
+ case "imageGridBlock":
65
+ content = <LiveImageGridPreview block={resolved as ImageGridBlock} />;
66
+ break;
67
+ case "videoBlock":
68
+ content = <LiveVideoPreview block={resolved as VideoBlock} />;
69
+ break;
70
+ case "spacerBlock":
71
+ content = <LiveSpacerPreview block={resolved as SpacerBlock} />;
72
+ break;
73
+ case "buttonBlock":
74
+ content = <LiveButtonPreview block={resolved as ButtonBlock} />;
75
+ break;
76
+ case "coverBlock":
77
+ content = <LiveCoverPreview block={resolved as CoverBlock} />;
78
+ break;
79
+ case "projectGridBlock":
80
+ content = <LiveProjectGridPreview block={resolved as ProjectGridBlock} viewport={viewport} />;
81
+ break;
82
+ default:
83
+ content = <LivePlaceholder type={(resolved as ContentBlock)._type} />;
84
+ }
85
+
86
+ // Wrap in layout div if block has layout properties set (spacing, background, border, etc.)
87
+ // NOTE: Alignment (align_h, align_v) is NOT applied here — it's handled by the parent
88
+ // wrapper (SortableBlock in builder, block wrapper div in public renderers) which is
89
+ // the direct flex child of the column. Applying alignment here would double-apply it.
90
+ const layout = (resolved as unknown as Record<string, unknown>).layout as ContentBlock["layout"] | undefined;
91
+ if (hasBlockLayout(layout)) {
92
+ const layoutStyles = getBlockLayoutStyles(layout);
93
+ content = <div style={layoutStyles}>{content}</div>;
94
+ }
95
+
96
+ return <>{content}</>;
97
+ }
98
+
99
+ /** Memoized wrapper — skips re-render when block, viewport, and editable are unchanged */
100
+ const BlockLivePreview = memo(BlockLivePreviewInner);
101
+ export default BlockLivePreview;
@@ -0,0 +1,178 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { BLOCK_TYPE_REGISTRY } from "../../lib/builder/types";
5
+ import type { BlockType, BlockTypeInfo } from "../../lib/builder/types";
6
+ import { BLOCK_GRADIENTS, BLOCK_ICON_COMPONENTS } from "./blockStyles";
7
+
8
+ interface BlockTypePickerProps {
9
+ onSelect: (type: BlockType) => void;
10
+ onClose: () => void;
11
+ insertIndex?: number;
12
+ }
13
+
14
+ // English labels for block picker (consistent with the rest of the admin UI)
15
+ const BLOCK_LABELS: Record<string, { label: string; description: string }> = {
16
+ textBlock: { label: "Text", description: "Rich text content with formatting" },
17
+ imageBlock: { label: "Image", description: "Single image with caption" },
18
+ imageGridBlock: { label: "Image Grid", description: "Multiple images in a grid layout" },
19
+ videoBlock: { label: "Video", description: "Vimeo, YouTube or MP4 file" },
20
+ spacerBlock: { label: "Spacer", description: "Customizable vertical spacing" },
21
+ buttonBlock: { label: "Button", description: "Call-to-action button (CTA)" },
22
+ coverBlock: { label: "Cover", description: "Full-screen hero section with image/video" },
23
+ projectGridBlock: { label: "Project Grid", description: "Staggered project showcase grid" },
24
+ };
25
+
26
+ function BlockCard({
27
+ block,
28
+ isHovered,
29
+ onSelect,
30
+ onHover,
31
+ onLeave,
32
+ }: {
33
+ block: BlockTypeInfo;
34
+ isHovered: boolean;
35
+ onSelect: () => void;
36
+ onHover: () => void;
37
+ onLeave: () => void;
38
+ }) {
39
+ const cardGradient = BLOCK_GRADIENTS[block.type];
40
+ const labels = BLOCK_LABELS[block.type];
41
+ const IconComponent = BLOCK_ICON_COMPONENTS[block.type];
42
+
43
+ return (
44
+ <button
45
+ onClick={onSelect}
46
+ onMouseEnter={onHover}
47
+ onMouseLeave={onLeave}
48
+ className="relative flex items-center gap-3 rounded-2xl px-3.5 py-3 transition-all text-left group overflow-hidden border-0"
49
+ style={{
50
+ background: cardGradient || "#f5f5f5",
51
+ transform: isHovered ? "translateY(-1px) scale(1.015)" : "translateY(0) scale(1)",
52
+ boxShadow: isHovered
53
+ ? "0 8px 24px rgba(0,0,0,0.18), inset 0 1px 0 rgba(255,255,255,0.3)"
54
+ : "0 2px 8px rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.2)",
55
+ transition: "all 0.3s cubic-bezier(0.23, 1, 0.32, 1)",
56
+ }}
57
+ >
58
+ {/* Glass overlay */}
59
+ <div
60
+ className="absolute inset-0 rounded-2xl pointer-events-none"
61
+ style={{
62
+ background: "linear-gradient(135deg, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0.05) 100%)",
63
+ }}
64
+ />
65
+
66
+ {/* Icon container — frosted glass */}
67
+ <div
68
+ className="relative shrink-0 flex items-center justify-center"
69
+ style={{
70
+ width: 44,
71
+ height: 44,
72
+ borderRadius: 12,
73
+ background: "rgba(255,255,255,0.4)",
74
+ backdropFilter: "blur(8px)",
75
+ boxShadow: "0 2px 8px rgba(0,0,0,0.06), inset 0 1px 0 rgba(255,255,255,0.5)",
76
+ transition: "transform 0.3s",
77
+ transform: isHovered ? "scale(1.08)" : "scale(1)",
78
+ }}
79
+ >
80
+ {IconComponent ? <IconComponent /> : null}
81
+ </div>
82
+
83
+ {/* Text */}
84
+ <div className="relative z-10 min-w-0">
85
+ <p
86
+ className="text-sm font-semibold truncate"
87
+ style={{
88
+ color: "rgba(0,0,0,0.72)",
89
+ textShadow: "0 1px 0 rgba(255,255,255,0.3)",
90
+ }}
91
+ >
92
+ {labels?.label || block.label}
93
+ </p>
94
+ <p
95
+ className="text-xs truncate leading-snug mt-0.5"
96
+ style={{ color: "rgba(0,0,0,0.42)" }}
97
+ >
98
+ {labels?.description || block.description}
99
+ </p>
100
+ </div>
101
+ </button>
102
+ );
103
+ }
104
+
105
+ export default function BlockTypePicker({
106
+ onSelect,
107
+ onClose,
108
+ insertIndex,
109
+ }: BlockTypePickerProps) {
110
+ const [hovered, setHovered] = useState<string | null>(null);
111
+
112
+ // Only show content blocks — section-level blocks are in SectionTypePicker
113
+ const contentBlocks = BLOCK_TYPE_REGISTRY.filter((b) => b.category === "content");
114
+
115
+ return (
116
+ <div
117
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/30 backdrop-blur-sm"
118
+ onClick={onClose}
119
+ >
120
+ <div
121
+ className="w-full max-w-2xl rounded-2xl bg-white max-h-[80vh] flex flex-col shadow-2xl border border-neutral-200/50 overflow-hidden"
122
+ style={{ fontFamily: "Inter, system-ui, sans-serif" }}
123
+ onClick={(e) => e.stopPropagation()}
124
+ >
125
+ {/* Header */}
126
+ <div className="px-6 pt-6 pb-4 border-b border-neutral-100">
127
+ <div className="flex items-center justify-between mb-1">
128
+ <h3 className="text-lg font-semibold text-neutral-900">
129
+ Add Block
130
+ </h3>
131
+ <div className="flex items-center gap-3">
132
+ {insertIndex !== undefined && (
133
+ <span className="text-xs text-neutral-400 bg-neutral-100 px-3 py-1 rounded-full font-medium">
134
+ Position: {insertIndex + 1}
135
+ </span>
136
+ )}
137
+ <button
138
+ onClick={onClose}
139
+ className="w-8 h-8 rounded-lg flex items-center justify-center text-neutral-400 hover:text-neutral-600 hover:bg-neutral-100 transition-colors"
140
+ aria-label="Close block picker"
141
+ >
142
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
143
+ <line x1="18" y1="6" x2="6" y2="18" />
144
+ <line x1="6" y1="6" x2="18" y2="18" />
145
+ </svg>
146
+ </button>
147
+ </div>
148
+ </div>
149
+ <p className="text-sm text-neutral-400">
150
+ Choose a block and add it to your page
151
+ </p>
152
+ </div>
153
+
154
+ {/* Block grid */}
155
+ <div className="overflow-y-auto px-6 py-4 flex-1">
156
+ {/* Content blocks */}
157
+ <div>
158
+ <p className="text-[10px] font-semibold uppercase tracking-widest text-neutral-300 mb-2.5">
159
+ Content Blocks
160
+ </p>
161
+ <div className="grid grid-cols-2 gap-2.5">
162
+ {contentBlocks.map((block) => (
163
+ <BlockCard
164
+ key={block.type}
165
+ block={block}
166
+ isHovered={hovered === block.type}
167
+ onSelect={() => { onSelect(block.type); onClose(); }}
168
+ onHover={() => setHovered(block.type)}
169
+ onLeave={() => setHovered(null)}
170
+ />
171
+ ))}
172
+ </div>
173
+ </div>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ );
178
+ }