@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,488 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback, type ReactNode } from "react";
4
+ import { useDroppable } from "@dnd-kit/core";
5
+ import {
6
+ SortableContext,
7
+ verticalListSortingStrategy,
8
+ } from "@dnd-kit/sortable";
9
+ import { useBuilderStore } from "../../lib/builder/store";
10
+ import { makeBlockId, makeColumnDroppableId } from "./DndWrapper";
11
+ import type { SectionColumn, ContentBlock, PageSectionV2 } from "../../lib/sanity/types";
12
+ import { getColumnVerticalAlign } from "../../lib/builder/layout-styles";
13
+ import { BUILDER_BLUE, BUILDER_GREEN } from "../../lib/builder/constants";
14
+
15
+ // ============================================
16
+ // SectionV2Column — Individual column in a V2 section grid
17
+ //
18
+ // Phase 3: Full handler implementation
19
+ // - Drag grip (top-left): @dnd-kit useDraggable for column-level drag
20
+ // - Resize handles (left + right): pill handles with scale-up hover
21
+ // - Delete button (top-right): red ✕ circle
22
+ // - Snapping guide feedback during resize
23
+ //
24
+ // Visual design (from PDF mockup):
25
+ // - Section hover: dashed outline at ~30% opacity, +Add Block at ~30%
26
+ // - Column hover: solid outline 100%, +Add Block 100%,
27
+ // 3 handlers appear (stretch left, stretch right, drag grip top-left),
28
+ // delete button (red ✕ top-right), span badge top-right
29
+ // - Handler hover: scale-up 1.15× animation
30
+ // ============================================
31
+
32
+ // ---- ResizeHandle ----
33
+ // Extracted from the duplicated left/right resize handle blocks.
34
+ // Encapsulates: hit area, 3-state pill visual (inactive/hover/active),
35
+ // mousedown, mouse enter/leave for hoveredEdge.
36
+
37
+ interface ResizeHandleProps {
38
+ edge: "left" | "right";
39
+ showChrome: boolean;
40
+ showFaintOutline: boolean;
41
+ resizingEdge: "left" | "right" | null;
42
+ hoveredEdge: "left" | "right" | null;
43
+ onHoverEdge: (edge: "left" | "right" | null) => void;
44
+ onResizeStart: (e: React.MouseEvent) => void;
45
+ }
46
+
47
+ function ResizeHandle({
48
+ edge,
49
+ showChrome,
50
+ showFaintOutline,
51
+ resizingEdge,
52
+ hoveredEdge,
53
+ onHoverEdge,
54
+ onResizeStart,
55
+ }: ResizeHandleProps) {
56
+ const isActive = resizingEdge === edge;
57
+ const isHoveredEdge = hoveredEdge === edge;
58
+ const isVisible = showChrome || isActive || showFaintOutline;
59
+
60
+ return (
61
+ <div
62
+ role="separator"
63
+ aria-orientation="vertical"
64
+ aria-label={`Resize column ${edge} edge`}
65
+ className={`absolute top-0 ${edge === "left" ? "left-0" : "right-0"} w-5 h-full z-[4] cursor-col-resize flex items-center justify-center transition-opacity ${
66
+ isVisible ? "opacity-100" : "opacity-0 pointer-events-none"
67
+ }`}
68
+ style={{ transform: edge === "left" ? "translateX(-50%)" : "translateX(50%)" }}
69
+ onMouseEnter={() => onHoverEdge(edge)}
70
+ onMouseLeave={() => { if (!isActive) onHoverEdge(null); }}
71
+ onMouseDown={(e) => {
72
+ e.stopPropagation();
73
+ e.preventDefault();
74
+ onResizeStart(e);
75
+ }}
76
+ >
77
+ <div
78
+ className="pointer-events-none shadow-sm"
79
+ style={{
80
+ // 3-state visual: ACTIVE → circle (16×16px), HOVER → wide pill (10×56px), INACTIVE → thin pill (4–6px × 32–56px).
81
+ // showChrome (selected/hovered column) gets a slightly wider and taller pill for better discoverability.
82
+ width: isActive ? 16 : isHoveredEdge ? 10 : showChrome ? 6 : 4,
83
+ height: isActive ? 16 : isHoveredEdge ? 56 : showChrome ? 56 : 32,
84
+ borderRadius: isActive ? "50%" : 999,
85
+ backgroundColor: isActive
86
+ ? "rgba(7, 107, 255, 0.9)"
87
+ : isHoveredEdge
88
+ ? "rgba(7, 107, 255, 0.7)"
89
+ : showChrome
90
+ ? "rgba(7, 107, 255, 0.5)"
91
+ : "rgba(7, 107, 255, 0.2)",
92
+ transition: "width 150ms ease-out, height 150ms ease-out, border-radius 150ms ease-out, background-color 150ms, box-shadow 150ms",
93
+ boxShadow: isActive
94
+ ? "0 0 10px rgba(7, 107, 255, 0.5)"
95
+ : isHoveredEdge
96
+ ? "0 0 6px rgba(7, 107, 255, 0.2)"
97
+ : undefined,
98
+ }}
99
+ />
100
+ </div>
101
+ );
102
+ }
103
+
104
+ interface SectionV2ColumnProps {
105
+ column: SectionColumn;
106
+ sectionKey: string;
107
+ section: PageSectionV2;
108
+ isSelected: boolean;
109
+ isSectionHovered: boolean;
110
+ isResizing?: boolean;
111
+ snappingInfo?: {
112
+ columnKey: string;
113
+ edge: "left" | "right";
114
+ willOverflow: boolean;
115
+ compressedNeighborKey?: string | null;
116
+ neighborAtMinimum?: boolean;
117
+ } | null;
118
+ /** Make-room animation direction when an insert target is between this and an adjacent column */
119
+ insertNudge?: "left" | "right" | null;
120
+ /** Green highlight when this column is the swap target */
121
+ isSwapTarget?: boolean;
122
+ /** Replaces old isDragging from useDraggable — ghost effect when this column is being dragged */
123
+ isDraggedColumn?: boolean;
124
+ /** Calls into useColumnDrag.startDrag */
125
+ onStartDrag?: (e: React.MouseEvent) => void;
126
+ /** Container width in pixels (for nudge offset computation) */
127
+ containerWidth?: number;
128
+ onSelect: () => void;
129
+ onDelete: () => void;
130
+ onAddBlock: (insertIndex?: number) => void;
131
+ onResizeRight: (columnKey: string, startX: number, startSpan: number, containerEl: HTMLElement) => void;
132
+ onResizeLeft: (columnKey: string, startX: number, startGridCol: number, startSpan: number, containerEl: HTMLElement) => void;
133
+ children: ReactNode;
134
+ }
135
+
136
+ export default function SectionV2Column({
137
+ column,
138
+ sectionKey,
139
+ section,
140
+ isSelected,
141
+ isSectionHovered,
142
+ isResizing = false,
143
+ snappingInfo,
144
+ insertNudge,
145
+ isSwapTarget = false,
146
+ isDraggedColumn = false,
147
+ onStartDrag,
148
+ containerWidth = 0,
149
+ onSelect,
150
+ onDelete,
151
+ onAddBlock,
152
+ onResizeRight,
153
+ onResizeLeft,
154
+ children,
155
+ }: SectionV2ColumnProps) {
156
+ const previewMode = useBuilderStore((s) => s.previewMode);
157
+ const canvasZoom = useBuilderStore((s) => s.canvasZoom);
158
+ const [isHovered, setIsHovered] = useState(false);
159
+ const [resizingEdge, setResizingEdge] = useState<"left" | "right" | null>(null);
160
+ const [hoveredEdge, setHoveredEdge] = useState<"left" | "right" | null>(null);
161
+
162
+ const gridColumns = section.settings.grid_columns || 12;
163
+
164
+ // ---- Block drop target ----
165
+ const blockDropId = makeColumnDroppableId(sectionKey, column._key);
166
+ const { setNodeRef: setBlockDropRef, isOver: isBlockOver } = useDroppable({
167
+ id: blockDropId,
168
+ disabled: previewMode,
169
+ });
170
+
171
+ // Block IDs for sortable context
172
+ const blockIds = (column.blocks || []).map((b: ContentBlock) =>
173
+ makeBlockId(sectionKey, column._key, b._key)
174
+ );
175
+
176
+ // ---- Stable callbacks ----
177
+ const handleClick = useCallback((e: React.MouseEvent) => {
178
+ e.stopPropagation();
179
+ onSelect();
180
+ }, [onSelect]);
181
+
182
+ const handleMouseEnter = useCallback(() => setIsHovered(true), []);
183
+ const handleMouseLeave = useCallback(() => setIsHovered(false), []);
184
+
185
+ const handleDelete = useCallback((e: React.MouseEvent) => {
186
+ e.stopPropagation();
187
+ onDelete();
188
+ }, [onDelete]);
189
+
190
+ const handleAddBlockEmpty = useCallback((e: React.MouseEvent) => {
191
+ e.stopPropagation();
192
+ onAddBlock(0);
193
+ }, [onAddBlock]);
194
+
195
+ const handleAddBlockBelow = useCallback((e: React.MouseEvent) => {
196
+ e.stopPropagation();
197
+ onAddBlock((column.blocks || []).length);
198
+ }, [onAddBlock, column.blocks]);
199
+
200
+ const hasBlocks = (column.blocks || []).length > 0;
201
+ const showChrome = (isSelected || isHovered) && !isDraggedColumn;
202
+ // Show faint outlines when section is hovered but not this specific column
203
+ const showFaintOutline = isSectionHovered && !isHovered && !isSelected && !isDraggedColumn;
204
+
205
+ // Column-level vertical alignment from blocks' align_v settings
206
+ const colJustify = getColumnVerticalAlign(column.blocks || []);
207
+
208
+ // ---- Preview mode ----
209
+ if (previewMode) {
210
+ return (
211
+ <div
212
+ ref={setBlockDropRef}
213
+ style={{
214
+ gridColumn: `${column.grid_column} / span ${column.span}`,
215
+ gridRow: column.grid_row,
216
+ display: "flex",
217
+ flexDirection: "column",
218
+ ...(colJustify ? { justifyContent: colJustify } : {}),
219
+ height: "100%",
220
+ minHeight: 0,
221
+ }}
222
+ >
223
+ <SortableContext items={blockIds} strategy={verticalListSortingStrategy}>
224
+ {children}
225
+ </SortableContext>
226
+ </div>
227
+ );
228
+ }
229
+
230
+ // Compute nudge offset for make-room animation.
231
+ // The column slides horizontally by 25% of one grid unit (containerWidth / gridColumns / 4)
232
+ // to visually "make room" for an insertion line drop target between adjacent columns.
233
+ const nudgeOffset = insertNudge
234
+ ? (containerWidth / gridColumns / 4) * (insertNudge === "left" ? -1 : 1)
235
+ : 0;
236
+
237
+ // ---- Design mode ----
238
+ return (
239
+ <div
240
+ className="relative group"
241
+ data-col-v2-droptarget=""
242
+ data-section-key={sectionKey}
243
+ data-column-key={column._key}
244
+ style={{
245
+ gridColumn: `${column.grid_column} / span ${column.span}`,
246
+ gridRow: column.grid_row,
247
+ display: "flex",
248
+ flexDirection: "column",
249
+ ...(colJustify ? { justifyContent: colJustify } : {}),
250
+ height: "100%",
251
+ minHeight: 0,
252
+ opacity: isDraggedColumn ? 0.3 : 1,
253
+ transform: nudgeOffset ? `translateX(${nudgeOffset}px)` : undefined,
254
+ transition: isDraggedColumn
255
+ ? "none"
256
+ : "opacity 150ms, box-shadow 150ms, transform 150ms ease-out",
257
+ }}
258
+ ref={setBlockDropRef}
259
+ onClick={handleClick}
260
+ onMouseEnter={handleMouseEnter}
261
+ onMouseLeave={handleMouseLeave}
262
+ >
263
+ {/* Column outline */}
264
+ <div
265
+ className="pointer-events-none absolute inset-0 z-[1] rounded"
266
+ style={{
267
+ transition: "box-shadow 150ms, border 150ms",
268
+ ...(isSwapTarget
269
+ ? { boxShadow: `inset 0 0 0 2px ${BUILDER_GREEN}`, background: "rgba(34, 197, 94, 0.08)" }
270
+ : isBlockOver
271
+ ? { boxShadow: `inset 0 0 0 2px ${BUILDER_BLUE}` }
272
+ : isSelected
273
+ ? { boxShadow: `inset 0 0 0 2px rgba(7, 107, 255, 0.6)` }
274
+ : isHovered
275
+ ? { boxShadow: `inset 0 0 0 1.5px rgba(7, 107, 255, 0.5)` }
276
+ : showFaintOutline
277
+ ? { border: `1px dashed rgba(7, 107, 255, 0.2)`, borderRadius: 4 }
278
+ : undefined),
279
+ }}
280
+ />
281
+
282
+ {/* Snapping warning indicator — overflow on right edge */}
283
+ {snappingInfo?.willOverflow && (
284
+ <div
285
+ className="pointer-events-none absolute z-[8]"
286
+ style={{
287
+ top: 0,
288
+ bottom: 0,
289
+ ...(snappingInfo.edge === "right"
290
+ ? { right: -2, width: 3 }
291
+ : { left: -2, width: 3 }),
292
+ background: "#ff6b35",
293
+ borderRadius: 2,
294
+ animation: "pulse 0.8s ease-in-out infinite",
295
+ }}
296
+ />
297
+ )}
298
+
299
+ {/* Neighbor-at-minimum indicator — pulsing orange border + badge when this
300
+ column has been compressed to span 1 during a left-edge resize */}
301
+ {snappingInfo?.compressedNeighborKey === column._key && snappingInfo?.neighborAtMinimum && (
302
+ <>
303
+ <div
304
+ className="pointer-events-none absolute inset-0 z-[8] rounded"
305
+ style={{
306
+ boxShadow: "inset 0 0 0 2px #ff6b35",
307
+ animation: "pulse 0.8s ease-in-out infinite",
308
+ }}
309
+ />
310
+ <div
311
+ className="pointer-events-none absolute z-[9]"
312
+ style={{
313
+ bottom: -2,
314
+ left: "50%",
315
+ transform: `translateX(-50%) scale(${1 / canvasZoom})`,
316
+ transformOrigin: "bottom center",
317
+ background: "#ff6b35",
318
+ color: "white",
319
+ fontSize: 10,
320
+ fontWeight: 600,
321
+ padding: "2px 8px",
322
+ borderRadius: 4,
323
+ whiteSpace: "nowrap",
324
+ boxShadow: "0 2px 6px rgba(255, 107, 53, 0.4)",
325
+ }}
326
+ >
327
+ Min width
328
+ </div>
329
+ </>
330
+ )}
331
+
332
+ {/* Span badge — top right */}
333
+ <div
334
+ className={`absolute top-0 right-0 z-[5] transition-opacity ${
335
+ isSelected || isHovered ? "opacity-100" : showFaintOutline ? "opacity-40" : "opacity-0"
336
+ }`}
337
+ style={{ transform: `scale(${1 / canvasZoom})`, transformOrigin: "top right" }}
338
+ >
339
+ <span className="text-[11px] px-1.5 py-0.5 rounded-bl rounded-tr bg-[#076bff]/80 text-white/90 font-medium tabular-nums">
340
+ {column.span}/{gridColumns}
341
+ </span>
342
+ </div>
343
+
344
+ {/* Delete button — red circle top right, positioned outside the column box.
345
+ Nested pattern: outer div positions, inner div counter-scales. */}
346
+ <div
347
+ className={`absolute z-[6] transition-opacity ${
348
+ showChrome ? "opacity-100" : "opacity-0 pointer-events-none"
349
+ }`}
350
+ style={{
351
+ top: 0,
352
+ right: 0,
353
+ transform: "translate(40%, -40%)",
354
+ }}
355
+ onClick={(e) => e.stopPropagation()}
356
+ >
357
+ <div style={{ transform: `scale(${1 / canvasZoom})`, transformOrigin: "center" }}>
358
+ <button
359
+ onClick={handleDelete}
360
+ className="w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center shadow-md transition-transform hover:scale-[1.15] hover:bg-red-600 hover:shadow-red-500/30 hover:shadow-lg"
361
+ title="Delete column"
362
+ aria-label="Delete column"
363
+ >
364
+ <svg width="10" height="10" viewBox="0 0 10 10">
365
+ <path d="M2 2l6 6M8 2l-6 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
366
+ </svg>
367
+ </button>
368
+ </div>
369
+ </div>
370
+
371
+ {/* Drag grip — top left corner, uses @dnd-kit useDraggable.
372
+ Nested pattern: outer div positions, inner div counter-scales. */}
373
+ <div
374
+ className={`absolute z-[6] transition-opacity ${
375
+ showChrome ? "opacity-100" : "opacity-0 pointer-events-none"
376
+ }`}
377
+ style={{
378
+ top: 0,
379
+ left: 0,
380
+ transform: "translate(-30%, -30%)",
381
+ }}
382
+ >
383
+ <div style={{ transform: `scale(${1 / canvasZoom})`, transformOrigin: "center" }}>
384
+ <div
385
+ className="w-5 h-5 rounded-full bg-[#076bff] text-white flex items-center justify-center shadow-md cursor-grab active:cursor-grabbing transition-transform hover:scale-[1.15] hover:bg-[#0559d4] hover:shadow-blue-500/30 hover:shadow-lg"
386
+ title="Drag to move column"
387
+ aria-label="Move column"
388
+ onMouseDown={(e) => {
389
+ e.stopPropagation();
390
+ onStartDrag?.(e);
391
+ }}
392
+ onClick={(e) => {
393
+ e.stopPropagation();
394
+ onSelect();
395
+ }}
396
+ >
397
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
398
+ <path d="M8 0l2.5 3h-2v4.5H13v-2L16 8l-3 2.5v-2H8.5V13h2L8 16l-2.5-3h2V8.5H3v2L0 8l3-2.5v2h4.5V3h-2L8 0z" />
399
+ </svg>
400
+ </div>
401
+ </div>
402
+ </div>
403
+
404
+ {/* Resize handles — left and right edges */}
405
+ <ResizeHandle
406
+ edge="left"
407
+ showChrome={showChrome}
408
+ showFaintOutline={showFaintOutline}
409
+ resizingEdge={resizingEdge}
410
+ hoveredEdge={hoveredEdge}
411
+ onHoverEdge={setHoveredEdge}
412
+ onResizeStart={(e) => {
413
+ setResizingEdge("left");
414
+ const onUp = () => { setResizingEdge(null); setHoveredEdge(null); document.removeEventListener("mouseup", onUp); };
415
+ document.addEventListener("mouseup", onUp);
416
+ const container = (e.currentTarget as HTMLElement).closest("[data-v2-grid-container]") as HTMLElement;
417
+ if (container) {
418
+ onResizeLeft(column._key, e.clientX, column.grid_column, column.span, container);
419
+ }
420
+ }}
421
+ />
422
+ <ResizeHandle
423
+ edge="right"
424
+ showChrome={showChrome}
425
+ showFaintOutline={showFaintOutline}
426
+ resizingEdge={resizingEdge}
427
+ hoveredEdge={hoveredEdge}
428
+ onHoverEdge={setHoveredEdge}
429
+ onResizeStart={(e) => {
430
+ setResizingEdge("right");
431
+ const onUp = () => { setResizingEdge(null); setHoveredEdge(null); document.removeEventListener("mouseup", onUp); };
432
+ document.addEventListener("mouseup", onUp);
433
+ const container = (e.currentTarget as HTMLElement).closest("[data-v2-grid-container]") as HTMLElement;
434
+ if (container) {
435
+ onResizeRight(column._key, e.clientX, column.span, container);
436
+ }
437
+ }}
438
+ />
439
+
440
+ {/* Blocks content */}
441
+ <SortableContext items={blockIds} strategy={verticalListSortingStrategy}>
442
+ {!hasBlocks ? (
443
+ /* Empty column: show + Add Block */
444
+ <div
445
+ className="relative flex items-center justify-center"
446
+ style={{ minHeight: 80, padding: "16px 12px" }}
447
+ >
448
+ <button
449
+ onClick={handleAddBlockEmpty}
450
+ aria-label="Add block to empty column"
451
+ className={`w-full py-2 rounded-lg text-xs font-medium transition-all flex items-center justify-center ${
452
+ showChrome
453
+ ? "bg-[#e28b00] text-white hover:bg-[#c67a00] shadow-sm opacity-100"
454
+ : showFaintOutline
455
+ ? "bg-[#e28b00]/30 text-white/50 opacity-40"
456
+ : "bg-transparent text-transparent opacity-0 pointer-events-none"
457
+ }`}
458
+ style={{ pointerEvents: showChrome || showFaintOutline ? "auto" : "none" }}
459
+ >
460
+ + Add Block
461
+ </button>
462
+ </div>
463
+ ) : (
464
+ children
465
+ )}
466
+ </SortableContext>
467
+
468
+ {/* "+" add block button below blocks — absolutely positioned to avoid disrupting flex alignment */}
469
+ {hasBlocks && (
470
+ <div
471
+ className={`absolute left-0 right-0 z-[3] transition-all ${
472
+ showChrome ? "opacity-100" : showFaintOutline ? "opacity-30" : "opacity-0 pointer-events-none"
473
+ }`}
474
+ style={{ bottom: 0, transform: "translateY(100%)", padding: "4px 12px 0" }}
475
+ >
476
+ <button
477
+ onClick={handleAddBlockBelow}
478
+ aria-label="Add block below existing blocks"
479
+ className="w-full py-1.5 text-[11px] font-medium rounded bg-[#e28b00] text-white hover:bg-[#c67a00] transition-all shadow-sm"
480
+ style={{ pointerEvents: showChrome ? "auto" : "none" }}
481
+ >
482
+ + Add Block
483
+ </button>
484
+ </div>
485
+ )}
486
+ </div>
487
+ );
488
+ }