@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,230 @@
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import { useSortable } from "@dnd-kit/sortable";
5
+ import { CSS } from "@dnd-kit/utilities";
6
+ import { makeBlockId } from "./DndWrapper";
7
+ import { ALL_BLOCK_INFO, isSectionBlockType } from "../../lib/builder/types";
8
+ import type { DeviceViewport } from "../../lib/builder/types";
9
+ import { useBuilderStore } from "../../lib/builder/store";
10
+ import type { ContentBlock } from "../../lib/sanity/types";
11
+ import BlockLivePreview from "./BlockLivePreview";
12
+ import { getBlockAlignmentStyles, hasBlockAlignment } from "../../lib/builder/layout-styles";
13
+ import type { BlockLayout } from "../../lib/sanity/types";
14
+ import { BUILDER_ORANGE } from "../../lib/builder/constants";
15
+
16
+ interface SortableBlockProps {
17
+ block: ContentBlock;
18
+ rowKey: string;
19
+ colKey: string;
20
+ blockIndex: number;
21
+ totalBlocks: number;
22
+ isSelected: boolean;
23
+ onSelect: () => void;
24
+ onDelete: () => void;
25
+ onContextMenu?: (e: React.MouseEvent) => void;
26
+ onDuplicate?: () => void;
27
+ }
28
+
29
+ export default function SortableBlock({
30
+ block,
31
+ rowKey,
32
+ colKey,
33
+ blockIndex,
34
+ totalBlocks,
35
+ isSelected,
36
+ onSelect,
37
+ onDelete,
38
+ onContextMenu,
39
+ onDuplicate,
40
+ }: SortableBlockProps) {
41
+ const previewMode = useBuilderStore((s) => s.previewMode);
42
+ const activeViewport = useBuilderStore((s) => s.activeViewport) as DeviceViewport;
43
+ const canvasZoom = useBuilderStore((s) => s.canvasZoom);
44
+ const reorderBlocks = useBuilderStore((s) => s.reorderBlocks);
45
+ const [isHovered, setIsHovered] = useState(false);
46
+ const id = makeBlockId(rowKey, colKey, block._key);
47
+
48
+ const canMoveUp = blockIndex > 0;
49
+ const canMoveDown = blockIndex < totalBlocks - 1;
50
+ const {
51
+ attributes,
52
+ listeners,
53
+ setNodeRef,
54
+ transform,
55
+ transition,
56
+ isDragging,
57
+ } = useSortable({ id, disabled: previewMode });
58
+
59
+ const info = ALL_BLOCK_INFO.find((b) => b.type === block._type);
60
+
61
+ // Block alignment styles — applied on the SortableBlock wrapper (direct flex child of column).
62
+ // This is the canonical layer for alignment; BlockLivePreview should NOT duplicate these.
63
+ const blockLayout = (block as unknown as Record<string, unknown>).layout as BlockLayout | undefined;
64
+ const alignStyles = hasBlockAlignment(blockLayout) ? getBlockAlignmentStyles(blockLayout) : {};
65
+ // Only force width:100% when no horizontal alignment — align-self needs width:auto to shrink
66
+ const hasHAlign = blockLayout?.align_h && blockLayout.align_h !== "left";
67
+
68
+ const style: React.CSSProperties = {
69
+ transform: CSS.Transform.toString(transform),
70
+ transition,
71
+ opacity: isDragging ? 0.3 : 1,
72
+ ...(!hasHAlign ? { width: "100%" } : {}),
73
+ minWidth: 0,
74
+ ...alignStyles,
75
+ };
76
+
77
+ const showToolbar = isSelected || isHovered;
78
+
79
+ // ---- Preview Mode: pure content, no chrome ----
80
+ if (previewMode) {
81
+ return (
82
+ <div ref={setNodeRef} style={style}>
83
+ <BlockLivePreview block={block} viewport={activeViewport} />
84
+ </div>
85
+ );
86
+ }
87
+
88
+ // ---- Section blocks (projectGrid, parallax): NO block chrome ----
89
+ // The parent SortableRow handles the purple toolbar; this just renders content.
90
+ // Click still selects the block so SettingsPanel shows block settings.
91
+ const isSectionBlock = isSectionBlockType(block._type);
92
+ if (isSectionBlock) {
93
+ return (
94
+ <div
95
+ ref={setNodeRef}
96
+ style={style}
97
+ className="relative"
98
+ onClick={(e) => { e.stopPropagation(); onSelect(); }}
99
+ >
100
+ <BlockLivePreview block={block} viewport={activeViewport} editable />
101
+ </div>
102
+ );
103
+ }
104
+
105
+ // ---- Design Mode: WYSIWYG — full content with subtle hover overlay + floating controls ----
106
+ return (
107
+ <div
108
+ ref={setNodeRef}
109
+ style={style}
110
+ className={`relative transition-[opacity,box-shadow] ${
111
+ isDragging
112
+ ? "ring-2 ring-[#e28b00] ring-offset-1 ring-offset-transparent rounded"
113
+ : ""
114
+ }`}
115
+ onClick={(e) => {
116
+ e.stopPropagation();
117
+ // If user clicked inside a contentEditable element (inline text editing),
118
+ // still select the block but don't steal focus from the editable.
119
+ onSelect();
120
+ }}
121
+ onContextMenu={onContextMenu}
122
+ onMouseEnter={() => setIsHovered(true)}
123
+ onMouseLeave={() => setIsHovered(false)}
124
+ >
125
+ {/* Hover/selection outline overlay — scales with zoom */}
126
+ <div
127
+ className="pointer-events-none absolute inset-0 z-[3] rounded transition-[box-shadow]"
128
+ style={
129
+ isSelected
130
+ ? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px ${BUILDER_ORANGE}` }
131
+ : isHovered
132
+ ? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px rgba(226, 139, 0, 0.4)` }
133
+ : undefined
134
+ }
135
+ />
136
+
137
+ {/* Floating toolbar — centered INSIDE top of block, appears on hover or when selected.
138
+ Positioning (translate) is separated from counter-scaling (scale) into nested
139
+ elements so that zoom changes don't shift the anchor point.
140
+ Delete button is integrated at the end of the toolbar with a gap separator. */}
141
+ <div
142
+ className={`absolute top-0 left-1/2 z-[6] transition-opacity ${
143
+ showToolbar ? "opacity-100" : "opacity-0 pointer-events-none"
144
+ }`}
145
+ style={{ transform: "translateX(-50%)", marginTop: 4 }}
146
+ onClick={(e) => e.stopPropagation()}
147
+ >
148
+ <div
149
+ className="flex items-center gap-1.5"
150
+ style={{ transform: `scale(${Math.min(2, 1 / canvasZoom)})`, transformOrigin: "top center" }}
151
+ >
152
+ <div className="flex items-center bg-[#e28b00] rounded shadow-lg overflow-hidden">
153
+ {/* Move up arrow */}
154
+ <button
155
+ onClick={() => canMoveUp && reorderBlocks(rowKey, colKey, blockIndex, blockIndex - 1)}
156
+ className={`transition-colors px-1 py-0.5 text-[11px] ${
157
+ canMoveUp
158
+ ? "text-white/80 hover:text-white hover:bg-white/10 cursor-pointer"
159
+ : "text-white/25 cursor-default"
160
+ }`}
161
+ title="Move block up"
162
+ aria-label="Move block up"
163
+ disabled={!canMoveUp}
164
+ >
165
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
166
+ <path d="M5 2L2 6h6L5 2z" fill="currentColor" />
167
+ </svg>
168
+ </button>
169
+ {/* Move down arrow */}
170
+ <button
171
+ onClick={() => canMoveDown && reorderBlocks(rowKey, colKey, blockIndex, blockIndex + 1)}
172
+ className={`transition-colors px-1 py-0.5 text-[11px] border-l border-white/20 ${
173
+ canMoveDown
174
+ ? "text-white/80 hover:text-white hover:bg-white/10 cursor-pointer"
175
+ : "text-white/25 cursor-default"
176
+ }`}
177
+ title="Move block down"
178
+ aria-label="Move block down"
179
+ disabled={!canMoveDown}
180
+ >
181
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
182
+ <path d="M5 8L2 4h6L5 8z" fill="currentColor" />
183
+ </svg>
184
+ </button>
185
+ {/* Block type label */}
186
+ <span className="text-[11px] text-white/70 px-1.5 py-0.5 border-l border-white/20">
187
+ {info?.icon || "▪"} {info?.label || block._type}
188
+ </span>
189
+ {/* Enter animation badge */}
190
+ {block.enter_animation?.preset && block.enter_animation.preset !== "none" && (
191
+ <span className="text-[10px] text-white/50 px-1 py-0.5 border-l border-white/15" title={`Animation: ${block.enter_animation.preset}`}>
192
+
193
+ </span>
194
+ )}
195
+ {/* Duplicate */}
196
+ {onDuplicate && (
197
+ <button
198
+ onClick={onDuplicate}
199
+ className="text-white/70 hover:text-white transition-colors px-1.5 py-0.5 text-[11px] border-l border-white/20 hover:bg-white/10"
200
+ title="Duplicate block (Ctrl+D)"
201
+ aria-label="Duplicate block"
202
+ >
203
+
204
+ </button>
205
+ )}
206
+ </div>
207
+ {/* Delete button — inline at end of toolbar with gap separator */}
208
+ <button
209
+ onClick={onDelete}
210
+ className="w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center hover:bg-red-600 transition-colors shadow-md shrink-0"
211
+ title="Delete block"
212
+ aria-label="Delete block"
213
+ >
214
+ <svg width="10" height="10" viewBox="0 0 10 10">
215
+ <path
216
+ d="M2 2l6 6M8 2l-6 6"
217
+ stroke="currentColor"
218
+ strokeWidth="1.5"
219
+ strokeLinecap="round"
220
+ />
221
+ </svg>
222
+ </button>
223
+ </div>
224
+ </div>
225
+
226
+ {/* Live content — rendered exactly as it would appear on the public site */}
227
+ <BlockLivePreview block={block} viewport={activeViewport} editable />
228
+ </div>
229
+ );
230
+ }
@@ -0,0 +1,362 @@
1
+ "use client";
2
+
3
+ import { useState, useMemo } from "react";
4
+ import { useSortable } from "@dnd-kit/sortable";
5
+ import { CSS } from "@dnd-kit/utilities";
6
+ import { makeRowId } from "./DndWrapper";
7
+ import { useBuilderStore } from "../../lib/builder/store";
8
+ import { DEFAULT_GRID_WIDTH } from "../../lib/builder/constants";
9
+ import { DEVICE_HEIGHTS } from "../../lib/builder/types";
10
+ import type { ReactNode } from "react";
11
+ import type { ContentItem, PageSection, PageSectionV2, CustomSectionInstance, ParallaxGroup } from "../../lib/sanity/types";
12
+ import { isPageSection, isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../lib/sanity/types";
13
+ import { getRowLayoutStyles } from "../../lib/builder/layout-styles";
14
+ import { normalizeMinHeight } from "../../lib/builder/utils";
15
+ import { getSectionV2SettingValue, getRowSettingValue } from "./settings-panel/responsive-helpers";
16
+
17
+ /**
18
+ * Convert vh-based CSS values to pixels using the simulated device viewport height.
19
+ * In the builder canvas, 100vh should map to the device frame height, not the browser viewport.
20
+ * Returns the original value unchanged if it's not a vh unit.
21
+ */
22
+ function vhToBuilderPx(value: string | undefined, deviceHeight: number): string | undefined {
23
+ if (!value) return value;
24
+ const vhMatch = value.match(/^(\d+(?:\.\d+)?)vh$/);
25
+ if (vhMatch) {
26
+ const vhNum = parseFloat(vhMatch[1]);
27
+ return `${Math.round((vhNum / 100) * deviceHeight)}px`;
28
+ }
29
+ return value;
30
+ }
31
+
32
+ /** Section type labels */
33
+ const SECTION_TYPE_LABELS: Record<string, string> = {
34
+ projectGrid: "Project Grid",
35
+ };
36
+
37
+ /**
38
+ * Get the section label for a content item.
39
+ * For PageSections: uses section_type directly (no structural inference).
40
+ * For PageSectionV2: returns "Section".
41
+ */
42
+ function getSectionLabel(item: ContentItem): string | null {
43
+ if (isCustomSectionInstance(item)) {
44
+ return (item as CustomSectionInstance).custom_section_title || "Saved Section";
45
+ }
46
+ if (isParallaxGroup(item)) {
47
+ return "Parallax Showcase";
48
+ }
49
+ if (isPageSectionV2(item)) {
50
+ return "Section";
51
+ }
52
+ if (isPageSection(item)) {
53
+ return SECTION_TYPE_LABELS[item.section_type] || "Section";
54
+ }
55
+ return null;
56
+ }
57
+
58
+ interface SortableRowProps {
59
+ rowKey: string;
60
+ row: ContentItem;
61
+ isSelected: boolean;
62
+ columnCount: number;
63
+ onSelect: () => void;
64
+ onDelete: () => void;
65
+ onAddColumn: () => void;
66
+ onDuplicate: () => void;
67
+ onMoveUp: () => void;
68
+ onMoveDown: () => void;
69
+ isFirst: boolean;
70
+ isLast: boolean;
71
+ children: ReactNode;
72
+ }
73
+
74
+ export default function SortableRow({
75
+ rowKey,
76
+ row,
77
+ isSelected,
78
+ columnCount,
79
+ onSelect,
80
+ onDelete,
81
+ onAddColumn,
82
+ onDuplicate,
83
+ onMoveUp,
84
+ onMoveDown,
85
+ isFirst,
86
+ isLast,
87
+ children,
88
+ }: SortableRowProps) {
89
+ const previewMode = useBuilderStore((s) => s.previewMode);
90
+ const canvasZoom = useBuilderStore((s) => s.canvasZoom);
91
+ const activeViewport = useBuilderStore((s) => s.activeViewport);
92
+ const gridSettings = useBuilderStore((s) => s.gridSettings);
93
+ const customSectionCache = useBuilderStore((s) => s._customSectionCache);
94
+ const [isHovered, setIsHovered] = useState(false);
95
+ const {
96
+ attributes,
97
+ listeners,
98
+ setNodeRef,
99
+ transform,
100
+ transition,
101
+ isDragging,
102
+ } = useSortable({ id: makeRowId(rowKey), disabled: previewMode });
103
+
104
+ const style = {
105
+ transform: CSS.Transform.toString(transform),
106
+ transition,
107
+ opacity: isDragging ? 0.4 : 1,
108
+ zIndex: isDragging ? 50 : undefined,
109
+ };
110
+
111
+ // Determine if this is a PageSection, PageSectionV2, or a Row
112
+ const isV2Section = isPageSectionV2(row);
113
+ const isSection = isPageSection(row);
114
+ const sectionLabel = getSectionLabel(row);
115
+
116
+ // For sections: use section settings — viewport-aware for both V1 and V2 sections
117
+ const resolvedSettings = useMemo(() => {
118
+ const overridableKeys = [
119
+ "spacing_top", "spacing_right", "spacing_bottom", "spacing_left",
120
+ "offset_top", "offset_right", "offset_bottom", "offset_left",
121
+ "background_color", "background_opacity",
122
+ "border_color", "border_width", "border_style", "border_sides", "border_radius",
123
+ ] as const;
124
+
125
+ if (isV2Section) {
126
+ const v2 = row as PageSectionV2;
127
+ const base = v2.settings || {};
128
+ // Merge responsive overrides on top of base settings for the active viewport.
129
+ const merged: Record<string, unknown> = { ...base };
130
+ for (const key of overridableKeys) {
131
+ merged[key] = getSectionV2SettingValue(v2, activeViewport, key, (base as unknown as Record<string, unknown>)[key]);
132
+ }
133
+ return merged;
134
+ }
135
+
136
+ if (isSection) {
137
+ const ps = row as PageSection;
138
+ const base = ps.settings || {};
139
+ // BUG-013 continuation: Merge responsive overrides for V1 PageSections too
140
+ const merged: Record<string, unknown> = { ...base };
141
+ for (const key of overridableKeys) {
142
+ merged[key] = getRowSettingValue(ps, activeViewport, key, (base as Record<string, unknown>)[key]);
143
+ }
144
+ return merged;
145
+ }
146
+
147
+ // CustomSectionInstance: merge base section settings from cache + per-instance overrides
148
+ // Then apply viewport-aware responsive resolution (same pattern as V2 sections)
149
+ if (isCustomSectionInstance(row)) {
150
+ const inst = row as CustomSectionInstance;
151
+ const cachedBase = customSectionCache[inst.custom_section_id] || {};
152
+ // Merge: base from cache → per-instance overrides on top
153
+ const base: Record<string, unknown> = { ...cachedBase, ...inst.settings_overrides };
154
+
155
+ // Apply viewport-aware responsive overrides if available
156
+ if (activeViewport !== "desktop" && inst.responsive_overrides) {
157
+ const viewportKey = activeViewport as "tablet" | "phone";
158
+ const viewportOverride = inst.responsive_overrides[viewportKey];
159
+ if (viewportOverride?.settings) {
160
+ for (const key of overridableKeys) {
161
+ const overrideVal = (viewportOverride.settings as Record<string, unknown>)[key];
162
+ if (overrideVal !== undefined && overrideVal !== null) {
163
+ base[key] = overrideVal;
164
+ }
165
+ }
166
+ }
167
+ }
168
+ return base;
169
+ }
170
+
171
+ return {};
172
+ }, [isV2Section, isSection, row, activeViewport, customSectionCache]);
173
+
174
+ const bgColor = (resolvedSettings as Record<string, unknown>).background_color as string || "transparent";
175
+
176
+ // Sections (V1 + V2) are always full-width
177
+ const maxWidth = "100%";
178
+ const gridPadding = "0px";
179
+ // Sections don't have min_height
180
+ const rawMinHeight = undefined;
181
+ const normalizedMinHeight = normalizeMinHeight(rawMinHeight);
182
+ // In the builder canvas, convert vh units to device frame pixels so 100vh matches
183
+ // the simulated viewport height rather than the actual browser viewport.
184
+ const minHeight = vhToBuilderPx(normalizedMinHeight, DEVICE_HEIGHTS[activeViewport]);
185
+
186
+ // New layout styles (spacing TRBL, background w/ opacity+image, offset, border)
187
+ const layoutStyles = getRowLayoutStyles(resolvedSettings as Record<string, unknown> || {});
188
+
189
+ const showToolbar = isSelected || isHovered;
190
+ const coverRow = isSection ? true : false;
191
+
192
+ // ---- Preview Mode: clean rendering with row styles applied ----
193
+ if (previewMode) {
194
+ // Cover rows: full-width, no padding, no container (matches RowRenderer)
195
+ if (coverRow) {
196
+ return (
197
+ <div
198
+ ref={setNodeRef}
199
+ style={{
200
+ ...style,
201
+ backgroundColor: bgColor !== "transparent" ? bgColor : undefined,
202
+ }}
203
+ >
204
+ {children}
205
+ </div>
206
+ );
207
+ }
208
+ // Build merged styles for preview mode
209
+ const previewRowStyle: React.CSSProperties = {
210
+ ...style,
211
+ ...layoutStyles,
212
+ minHeight,
213
+ };
214
+ // Legacy fallback: if no layout background set, use old bgColor
215
+ if (!layoutStyles.backgroundColor && !layoutStyles.backgroundImage) {
216
+ if (bgColor !== "transparent") previewRowStyle.backgroundColor = bgColor;
217
+ }
218
+
219
+ return (
220
+ <div ref={setNodeRef} style={previewRowStyle}>
221
+ <div style={{ maxWidth, margin: "0 auto", paddingLeft: maxWidth !== "100%" ? gridPadding : undefined, paddingRight: maxWidth !== "100%" ? gridPadding : undefined }}>
222
+ {children}
223
+ </div>
224
+ </div>
225
+ );
226
+ }
227
+
228
+ // ---- Design Mode: WYSIWYG — same visual rendering as Preview, with hover overlays ----
229
+ // Build merged styles for design mode
230
+ const designRowStyle: React.CSSProperties = {
231
+ ...style,
232
+ ...layoutStyles,
233
+ minHeight: coverRow ? undefined : minHeight,
234
+ };
235
+ // Legacy fallback: if no layout background set, use old bgColor
236
+ if (!layoutStyles.backgroundColor && !layoutStyles.backgroundImage) {
237
+ if (bgColor !== "transparent") designRowStyle.backgroundColor = bgColor;
238
+ }
239
+
240
+ return (
241
+ <div
242
+ ref={setNodeRef}
243
+ style={designRowStyle}
244
+ className={`relative transition-[opacity,box-shadow] ${
245
+ isDragging ? "ring-2 ring-[#93278f] ring-offset-2 ring-offset-[#0a0a0a]" : ""
246
+ }`}
247
+ onClick={(e) => { e.stopPropagation(); onSelect(); }}
248
+ onMouseEnter={() => setIsHovered(true)}
249
+ onMouseLeave={() => setIsHovered(false)}
250
+ >
251
+ {/* Selection/hover outline — scales with zoom */}
252
+ <div
253
+ className="pointer-events-none absolute inset-0 z-[1] transition-[box-shadow]"
254
+ style={
255
+ isSelected
256
+ ? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px #93278f` }
257
+ : isHovered
258
+ ? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px rgba(147, 39, 143, 0.4)` }
259
+ : undefined
260
+ }
261
+ />
262
+
263
+ {/* Section toolbar — wide pill aligned top-left outside the row */}
264
+ <div
265
+ className={`absolute top-0 left-0 z-[5] flex flex-col items-stretch transition-opacity ${
266
+ showToolbar ? "opacity-100" : "opacity-0 pointer-events-none"
267
+ }`}
268
+ style={{
269
+ transform: `translateX(-100%) scale(${Math.min(1.5, 1 / canvasZoom)})`,
270
+ transformOrigin: "top right",
271
+ width: "90px",
272
+ }}
273
+ onClick={(e) => { e.stopPropagation(); onSelect(); }}
274
+ >
275
+ {/* Main toolbar — drag + actions */}
276
+ <div
277
+ className="flex flex-col items-stretch bg-[#93278f]/90 rounded-l-lg shadow-lg py-2 px-2.5 gap-1 cursor-grab active:cursor-grabbing"
278
+ {...attributes}
279
+ {...listeners}
280
+ >
281
+ {/* Section label — shows specific type for page sections */}
282
+ <span className="text-[11px] text-white select-none leading-tight pointer-events-none font-medium tracking-wide">
283
+ {sectionLabel || "Section"}
284
+ </span>
285
+
286
+ {/* Action row: Duplicate + Move up + Move down */}
287
+ <div className="flex items-center gap-1 py-0.5">
288
+ <button
289
+ onClick={(e) => { e.stopPropagation(); onDuplicate(); }}
290
+ onPointerDown={(e) => e.stopPropagation()}
291
+ className="flex items-center justify-center text-[12px] text-white/80 hover:text-white transition-colors"
292
+ title="Duplicate section"
293
+ aria-label="Duplicate section"
294
+ >
295
+
296
+ </button>
297
+ <button
298
+ onClick={(e) => { e.stopPropagation(); onMoveUp(); }}
299
+ onPointerDown={(e) => e.stopPropagation()}
300
+ disabled={isFirst}
301
+ className="flex items-center justify-center text-[12px] text-white/80 hover:text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
302
+ title="Move up"
303
+ aria-label="Move section up"
304
+ >
305
+
306
+ </button>
307
+ <button
308
+ onClick={(e) => { e.stopPropagation(); onMoveDown(); }}
309
+ onPointerDown={(e) => e.stopPropagation()}
310
+ disabled={isLast}
311
+ className="flex items-center justify-center text-[12px] text-white/80 hover:text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
312
+ title="Move down"
313
+ aria-label="Move section down"
314
+ >
315
+
316
+ </button>
317
+ </div>
318
+
319
+ {/* Add column — shown for V2 sections and regular rows, hidden for closed sections (V1 PageSection) */}
320
+ {(!sectionLabel || isV2Section) && (
321
+ <button
322
+ onClick={(e) => { e.stopPropagation(); onAddColumn(); }}
323
+ onPointerDown={(e) => e.stopPropagation()}
324
+ className="flex items-center gap-1 text-[11px] text-white/80 hover:text-white transition-colors py-0.5"
325
+ title="Add column"
326
+ aria-label="Add column"
327
+ >
328
+ <span className="text-white/50">+</span> Col
329
+ </button>
330
+ )}
331
+
332
+ {/* Delete */}
333
+ <button
334
+ onClick={(e) => { e.stopPropagation(); onDelete(); }}
335
+ onPointerDown={(e) => e.stopPropagation()}
336
+ className="flex items-center gap-1 text-[11px] text-white/80 hover:text-red-300 transition-colors py-0.5"
337
+ title="Delete section"
338
+ aria-label="Delete section"
339
+ >
340
+ <span className="text-white/50">-</span> Delete
341
+ </button>
342
+ </div>
343
+ </div>
344
+
345
+ {/* Row bg color indicator */}
346
+ {bgColor !== "transparent" && isSelected && (
347
+ <div className="absolute top-1 right-1 z-[5]" style={{ transform: `scale(${1 / canvasZoom})`, transformOrigin: "top right" }}>
348
+ <span
349
+ className="w-4 h-4 rounded-full border-2 border-white/50 block shadow-sm"
350
+ style={{ backgroundColor: bgColor }}
351
+ title={`Background: ${bgColor}`}
352
+ />
353
+ </div>
354
+ )}
355
+
356
+ {/* Content — same layout as Preview */}
357
+ <div style={coverRow ? undefined : { maxWidth, margin: "0 auto", paddingLeft: maxWidth !== "100%" ? gridPadding : undefined, paddingRight: maxWidth !== "100%" ? gridPadding : undefined }} className="relative z-[2]">
358
+ {children}
359
+ </div>
360
+ </div>
361
+ );
362
+ }