@morphika/andami 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/admin/assets.ts +4 -0
  4. package/admin/database.ts +4 -0
  5. package/admin/index.ts +6 -0
  6. package/admin/login.ts +4 -0
  7. package/admin/navigation.ts +4 -0
  8. package/admin/pages-editor.ts +4 -0
  9. package/admin/pages.ts +4 -0
  10. package/admin/projects-editor.ts +4 -0
  11. package/admin/projects.ts +4 -0
  12. package/admin/settings.ts +4 -0
  13. package/admin/setup.ts +4 -0
  14. package/admin/storage.ts +4 -0
  15. package/admin/styles.ts +4 -0
  16. package/app/(site)/[slug]/loading.tsx +20 -0
  17. package/app/(site)/[slug]/page.tsx +83 -0
  18. package/app/(site)/error.tsx +32 -0
  19. package/app/(site)/layout.tsx +53 -0
  20. package/app/(site)/loading.tsx +20 -0
  21. package/app/(site)/not-found.tsx +41 -0
  22. package/app/(site)/page.tsx +43 -0
  23. package/app/(site)/preview/page.tsx +99 -0
  24. package/app/(site)/work/[slug]/loading.tsx +23 -0
  25. package/app/(site)/work/[slug]/page.tsx +84 -0
  26. package/app/admin/assets/page.tsx +573 -0
  27. package/app/admin/database/page.tsx +302 -0
  28. package/app/admin/error.tsx +53 -0
  29. package/app/admin/layout.tsx +273 -0
  30. package/app/admin/login/page.tsx +88 -0
  31. package/app/admin/navigation/page.tsx +157 -0
  32. package/app/admin/page.tsx +17 -0
  33. package/app/admin/pages/[slug]/page.tsx +849 -0
  34. package/app/admin/pages/page.tsx +588 -0
  35. package/app/admin/projects/[slug]/page.tsx +3 -0
  36. package/app/admin/projects/page.tsx +669 -0
  37. package/app/admin/settings/page.tsx +132 -0
  38. package/app/admin/setup/page.tsx +64 -0
  39. package/app/admin/storage/page.tsx +518 -0
  40. package/app/admin/styles/page.tsx +243 -0
  41. package/app/api/admin/assets/file/route.ts +81 -0
  42. package/app/api/admin/assets/health/route.ts +170 -0
  43. package/app/api/admin/assets/register/route.ts +163 -0
  44. package/app/api/admin/assets/registry/route.ts +98 -0
  45. package/app/api/admin/assets/relink/confirm/route.ts +242 -0
  46. package/app/api/admin/assets/relink/route.ts +202 -0
  47. package/app/api/admin/assets/scan/route.ts +271 -0
  48. package/app/api/admin/auth/route.ts +160 -0
  49. package/app/api/admin/custom-sections/[slug]/route.ts +159 -0
  50. package/app/api/admin/custom-sections/route.ts +127 -0
  51. package/app/api/admin/database/route.ts +53 -0
  52. package/app/api/admin/pages/[slug]/duplicate/route.ts +91 -0
  53. package/app/api/admin/pages/[slug]/route.ts +617 -0
  54. package/app/api/admin/pages/[slug]/set-home/route.ts +76 -0
  55. package/app/api/admin/pages/route.ts +129 -0
  56. package/app/api/admin/preview/route.ts +53 -0
  57. package/app/api/admin/r2/connect/route.ts +181 -0
  58. package/app/api/admin/r2/delete/route.ts +198 -0
  59. package/app/api/admin/r2/disconnect/route.ts +42 -0
  60. package/app/api/admin/r2/rename/route.ts +265 -0
  61. package/app/api/admin/r2/status/route.ts +106 -0
  62. package/app/api/admin/r2/upload-url/route.ts +148 -0
  63. package/app/api/admin/revalidate/route.ts +55 -0
  64. package/app/api/admin/settings/route.ts +279 -0
  65. package/app/api/admin/setup/complete/route.ts +51 -0
  66. package/app/api/admin/setup/route.ts +118 -0
  67. package/app/api/admin/storage/switch/route.ts +117 -0
  68. package/app/api/admin/styles/fonts/route.ts +97 -0
  69. package/app/api/admin/styles/route.ts +304 -0
  70. package/app/api/assets/[...path]/route.ts +98 -0
  71. package/app/api/custom-sections/[id]/route.ts +43 -0
  72. package/app/api/draft-mode/disable/route.ts +10 -0
  73. package/app/api/draft-mode/enable/route.ts +26 -0
  74. package/app/api/projects/route.ts +42 -0
  75. package/app/api/styles/route.ts +88 -0
  76. package/app/favicon.ico +0 -0
  77. package/app/globals.css +7 -0
  78. package/app/layout.tsx +53 -0
  79. package/app/robots.ts +17 -0
  80. package/app/sitemap.ts +48 -0
  81. package/app/studio/[[...index]]/page.tsx +8 -0
  82. package/components/admin/MetadataEditor.tsx +173 -0
  83. package/components/admin/PublishToggle.tsx +130 -0
  84. package/components/admin/icons.tsx +40 -0
  85. package/components/admin/nav-builder/NavBuilder.tsx +182 -0
  86. package/components/admin/nav-builder/NavBuilderGrid.tsx +326 -0
  87. package/components/admin/nav-builder/NavGeneralSettings.tsx +275 -0
  88. package/components/admin/nav-builder/NavGridCell.tsx +48 -0
  89. package/components/admin/nav-builder/NavGridItem.tsx +189 -0
  90. package/components/admin/nav-builder/NavItemSettings.tsx +288 -0
  91. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -0
  92. package/components/admin/nav-builder/NavLivePreview.tsx +125 -0
  93. package/components/admin/nav-builder/NavSettingsFields.tsx +248 -0
  94. package/components/admin/nav-builder/NavSettingsPanel.tsx +127 -0
  95. package/components/admin/nav-builder/index.ts +10 -0
  96. package/components/admin/nav-builder/nav-builder-utils.ts +238 -0
  97. package/components/admin/setup-wizard/BrandingStep.tsx +218 -0
  98. package/components/admin/setup-wizard/DatabaseStep.tsx +331 -0
  99. package/components/admin/setup-wizard/DoneStep.tsx +187 -0
  100. package/components/admin/setup-wizard/SetupWizard.tsx +166 -0
  101. package/components/admin/setup-wizard/StorageStep.tsx +308 -0
  102. package/components/admin/setup-wizard/WelcomeStep.tsx +96 -0
  103. package/components/admin/setup-wizard/index.ts +9 -0
  104. package/components/admin/styles/ColorsEditor.tsx +214 -0
  105. package/components/admin/styles/FontsEditor.tsx +258 -0
  106. package/components/admin/styles/GridLayoutEditor.tsx +292 -0
  107. package/components/admin/styles/LinksButtonsEditor.tsx +120 -0
  108. package/components/admin/styles/TypographyEditor.tsx +266 -0
  109. package/components/admin/styles/index.ts +9 -0
  110. package/components/admin/styles/shared.tsx +68 -0
  111. package/components/blocks/BlockRenderer.tsx +404 -0
  112. package/components/blocks/ButtonBlockRenderer.tsx +52 -0
  113. package/components/blocks/CoverBlockRenderer.tsx +239 -0
  114. package/components/blocks/CustomSectionInstanceRenderer.tsx +82 -0
  115. package/components/blocks/EnterAnimationWrapper.tsx +140 -0
  116. package/components/blocks/HoverAnimationWrapper.tsx +308 -0
  117. package/components/blocks/ImageBlockRenderer.tsx +61 -0
  118. package/components/blocks/ImageGridBlockRenderer.tsx +545 -0
  119. package/components/blocks/PageBackground.tsx +28 -0
  120. package/components/blocks/PageNavAnimation.tsx +35 -0
  121. package/components/blocks/PageNavColor.tsx +24 -0
  122. package/components/blocks/PageRenderer.tsx +142 -0
  123. package/components/blocks/ParallaxGroupRenderer.tsx +448 -0
  124. package/components/blocks/ParallaxSlideRenderer.tsx +175 -0
  125. package/components/blocks/ProjectGridBlockRenderer.tsx +556 -0
  126. package/components/blocks/SectionRenderer.tsx +170 -0
  127. package/components/blocks/SectionV2Renderer.tsx +330 -0
  128. package/components/blocks/ShaderCanvas.tsx +392 -0
  129. package/components/blocks/SpacerBlockRenderer.tsx +17 -0
  130. package/components/blocks/TextBlockRenderer.tsx +87 -0
  131. package/components/blocks/TypewriterRichText.tsx +464 -0
  132. package/components/blocks/TypewriterWrapper.tsx +149 -0
  133. package/components/blocks/VideoBlockRenderer.tsx +304 -0
  134. package/components/blocks/index.ts +2 -0
  135. package/components/builder/AssetBrowser.tsx +2 -0
  136. package/components/builder/BlockLivePreview.tsx +101 -0
  137. package/components/builder/BlockTypePicker.tsx +178 -0
  138. package/components/builder/BuilderCanvas.tsx +354 -0
  139. package/components/builder/CanvasMinimap.tsx +200 -0
  140. package/components/builder/CanvasToolbar.tsx +202 -0
  141. package/components/builder/ColorPicker.tsx +243 -0
  142. package/components/builder/ColorSwatchPicker.tsx +274 -0
  143. package/components/builder/ColumnDragContext.tsx +51 -0
  144. package/components/builder/ColumnDragOverlay.tsx +110 -0
  145. package/components/builder/CustomSectionInstanceCard.tsx +97 -0
  146. package/components/builder/DeviceFrame.tsx +123 -0
  147. package/components/builder/DndWrapper.tsx +337 -0
  148. package/components/builder/InsertionLines.tsx +186 -0
  149. package/components/builder/ParallaxGroupCanvas.tsx +228 -0
  150. package/components/builder/ParallaxSlideHeader.tsx +113 -0
  151. package/components/builder/ReadOnlyFrame.tsx +417 -0
  152. package/components/builder/SectionEditorBar.tsx +288 -0
  153. package/components/builder/SectionTypePicker.tsx +422 -0
  154. package/components/builder/SectionV2Canvas.tsx +297 -0
  155. package/components/builder/SectionV2Column.tsx +488 -0
  156. package/components/builder/SettingsPanel.tsx +911 -0
  157. package/components/builder/SortableBlock.tsx +230 -0
  158. package/components/builder/SortableRow.tsx +362 -0
  159. package/components/builder/VirtualAssetGrid.tsx +397 -0
  160. package/components/builder/asset-browser/AssetBrowser.tsx +178 -0
  161. package/components/builder/asset-browser/FileLightbox.tsx +116 -0
  162. package/components/builder/asset-browser/FolderTreeItem.tsx +55 -0
  163. package/components/builder/asset-browser/R2BrowserContent.tsx +436 -0
  164. package/components/builder/asset-browser/R2ContextMenu.tsx +98 -0
  165. package/components/builder/asset-browser/VideoThumbnail.tsx +63 -0
  166. package/components/builder/asset-browser/helpers.ts +88 -0
  167. package/components/builder/asset-browser/index.ts +1 -0
  168. package/components/builder/asset-browser/types.ts +49 -0
  169. package/components/builder/asset-browser/useAssetBrowser.ts +344 -0
  170. package/components/builder/asset-browser/useR2DragDrop.ts +116 -0
  171. package/components/builder/asset-browser/useR2Operations.ts +189 -0
  172. package/components/builder/blockStyles.tsx +295 -0
  173. package/components/builder/editors/ButtonBlockEditor.tsx +184 -0
  174. package/components/builder/editors/CoverBlockEditor.tsx +488 -0
  175. package/components/builder/editors/EnterAnimationPicker.tsx +297 -0
  176. package/components/builder/editors/HoverEffectPicker.tsx +209 -0
  177. package/components/builder/editors/ImageBlockEditor.tsx +206 -0
  178. package/components/builder/editors/ImageGridBlockEditor.tsx +386 -0
  179. package/components/builder/editors/ProjectGridEditor.tsx +648 -0
  180. package/components/builder/editors/SpacerBlockEditor.tsx +167 -0
  181. package/components/builder/editors/StaggerSettings.tsx +108 -0
  182. package/components/builder/editors/TextAlignmentIcons.tsx +39 -0
  183. package/components/builder/editors/TextBlockEditor.tsx +462 -0
  184. package/components/builder/editors/TextStylePicker.tsx +183 -0
  185. package/components/builder/editors/VideoBlockEditor.tsx +278 -0
  186. package/components/builder/editors/index.ts +10 -0
  187. package/components/builder/editors/shared.tsx +345 -0
  188. package/components/builder/hooks/useColumnDrag.ts +472 -0
  189. package/components/builder/hooks/useColumnResize.ts +221 -0
  190. package/components/builder/index.ts +12 -0
  191. package/components/builder/live-preview/LiveButtonPreview.tsx +38 -0
  192. package/components/builder/live-preview/LiveCoverPreview.tsx +146 -0
  193. package/components/builder/live-preview/LiveImageGridPreview.tsx +123 -0
  194. package/components/builder/live-preview/LiveImagePreview.tsx +107 -0
  195. package/components/builder/live-preview/LiveProjectGridPreview.tsx +1010 -0
  196. package/components/builder/live-preview/LiveSpacerPreview.tsx +9 -0
  197. package/components/builder/live-preview/LiveTextEditor.tsx +198 -0
  198. package/components/builder/live-preview/LiveVideoPreview.tsx +98 -0
  199. package/components/builder/live-preview/index.ts +10 -0
  200. package/components/builder/live-preview/shared.tsx +153 -0
  201. package/components/builder/settings-panel/BlockLayoutTab.tsx +532 -0
  202. package/components/builder/settings-panel/BlockSettings.tsx +94 -0
  203. package/components/builder/settings-panel/ColumnV2Settings.tsx +160 -0
  204. package/components/builder/settings-panel/LayoutTab.tsx +310 -0
  205. package/components/builder/settings-panel/PageSettings.tsx +200 -0
  206. package/components/builder/settings-panel/ParallaxGroupSettings.tsx +118 -0
  207. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +178 -0
  208. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +103 -0
  209. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +312 -0
  210. package/components/builder/settings-panel/SectionV2Settings.tsx +323 -0
  211. package/components/builder/settings-panel/TRBLInputs.tsx +51 -0
  212. package/components/builder/settings-panel/index.ts +19 -0
  213. package/components/builder/settings-panel/responsive-helpers.ts +524 -0
  214. package/components/ui/CustomCursor.tsx +118 -0
  215. package/components/ui/NavContentLightbox.tsx +152 -0
  216. package/components/ui/Navbar.tsx +582 -0
  217. package/components/ui/PortfolioTracker.tsx +87 -0
  218. package/components/ui/ScrollToTop.tsx +47 -0
  219. package/lib/animation/enter-presets.ts +147 -0
  220. package/lib/animation/enter-resolve.ts +90 -0
  221. package/lib/animation/enter-types.ts +128 -0
  222. package/lib/animation/hover-effect-presets.ts +210 -0
  223. package/lib/animation/hover-effect-types.ts +126 -0
  224. package/lib/asset-retry.ts +111 -0
  225. package/lib/assets.ts +92 -0
  226. package/lib/audit.ts +35 -0
  227. package/lib/auth-token.ts +94 -0
  228. package/lib/auth.ts +13 -0
  229. package/lib/builder/cascade-helpers.ts +51 -0
  230. package/lib/builder/cascade.ts +533 -0
  231. package/lib/builder/constants.ts +103 -0
  232. package/lib/builder/defaults.ts +182 -0
  233. package/lib/builder/history.ts +48 -0
  234. package/lib/builder/index.ts +21 -0
  235. package/lib/builder/layout-styles.ts +344 -0
  236. package/lib/builder/masonry.ts +166 -0
  237. package/lib/builder/responsive.ts +156 -0
  238. package/lib/builder/serializer.ts +845 -0
  239. package/lib/builder/store-blocks.ts +193 -0
  240. package/lib/builder/store-canvas.ts +319 -0
  241. package/lib/builder/store-helpers.ts +490 -0
  242. package/lib/builder/store-sections.ts +709 -0
  243. package/lib/builder/store.ts +333 -0
  244. package/lib/builder/templates.ts +297 -0
  245. package/lib/builder/types.ts +374 -0
  246. package/lib/builder/utils.ts +37 -0
  247. package/lib/color-utils.ts +116 -0
  248. package/lib/config/index.ts +57 -0
  249. package/lib/config/types.ts +122 -0
  250. package/lib/contexts/AssetContext.tsx +79 -0
  251. package/lib/contexts/NavAnimationContext.tsx +44 -0
  252. package/lib/contexts/NavColorContext.tsx +38 -0
  253. package/lib/contexts/PageExitContext.tsx +194 -0
  254. package/lib/contexts/ThumbStatusContext.tsx +83 -0
  255. package/lib/csrf-client.ts +34 -0
  256. package/lib/csrf.ts +68 -0
  257. package/lib/format-utils.ts +24 -0
  258. package/lib/hooks/useViewport.ts +42 -0
  259. package/lib/logger.ts +81 -0
  260. package/lib/revalidate.ts +23 -0
  261. package/lib/sanitize.ts +91 -0
  262. package/lib/sanity/client.ts +8 -0
  263. package/lib/sanity/queries.ts +486 -0
  264. package/lib/sanity/types.ts +869 -0
  265. package/lib/sanity/writeClient.ts +24 -0
  266. package/lib/security.ts +402 -0
  267. package/lib/setup/detect.ts +156 -0
  268. package/lib/shader/glsl/index.ts +27 -0
  269. package/lib/shader/glsl/pixelate.ts +51 -0
  270. package/lib/shader/glsl/rgb-shift.ts +45 -0
  271. package/lib/shader/glsl/ripple.ts +46 -0
  272. package/lib/shader/glsl/vertex.ts +14 -0
  273. package/lib/storage/index.ts +211 -0
  274. package/lib/storage/r2-adapter.ts +286 -0
  275. package/lib/storage/types.ts +125 -0
  276. package/lib/styles/provider.tsx +267 -0
  277. package/lib/thumbnails/generate.ts +151 -0
  278. package/lib/utils.ts +6 -0
  279. package/package.json +212 -0
  280. package/sanity/compose.ts +65 -0
  281. package/sanity/sanity.config.ts +126 -0
  282. package/sanity/schemas/assetRegistry.ts +301 -0
  283. package/sanity/schemas/blocks/blockLayout.ts +90 -0
  284. package/sanity/schemas/blocks/buttonBlock.ts +82 -0
  285. package/sanity/schemas/blocks/coverBlock.ts +229 -0
  286. package/sanity/schemas/blocks/imageBlock.ts +58 -0
  287. package/sanity/schemas/blocks/imageGridBlock.ts +112 -0
  288. package/sanity/schemas/blocks/index.ts +9 -0
  289. package/sanity/schemas/blocks/projectGridBlock.ts +251 -0
  290. package/sanity/schemas/blocks/spacerBlock.ts +41 -0
  291. package/sanity/schemas/blocks/textBlock.ts +139 -0
  292. package/sanity/schemas/blocks/videoBlock.ts +80 -0
  293. package/sanity/schemas/customSection.ts +69 -0
  294. package/sanity/schemas/customSectionInstance.ts +163 -0
  295. package/sanity/schemas/index.ts +111 -0
  296. package/sanity/schemas/objects/enterAnimationConfig.ts +72 -0
  297. package/sanity/schemas/objects/hoverEffectConfig.ts +90 -0
  298. package/sanity/schemas/objects/parallaxGroup.ts +66 -0
  299. package/sanity/schemas/objects/parallaxSlide.ts +217 -0
  300. package/sanity/schemas/objects/typewriterConfig.ts +38 -0
  301. package/sanity/schemas/page.ts +162 -0
  302. package/sanity/schemas/pageSection.ts +157 -0
  303. package/sanity/schemas/pageSectionV2.ts +269 -0
  304. package/sanity/schemas/siteSettings.ts +256 -0
  305. package/sanity/schemas/siteStyles.ts +212 -0
  306. package/site/error.ts +4 -0
  307. package/site/index.ts +8 -0
  308. package/site/not-found.ts +4 -0
  309. package/site/page.ts +4 -0
  310. package/site/preview.ts +4 -0
  311. package/site/robots.ts +4 -0
  312. package/site/sitemap.ts +4 -0
  313. package/site/work.ts +4 -0
  314. package/studio/index.ts +4 -0
  315. package/styles/admin.css +85 -0
  316. package/styles/animations.css +237 -0
  317. package/styles/base.css +148 -0
  318. package/styles/globals.css +10 -0
  319. package/tsconfig.json +25 -0
@@ -0,0 +1,337 @@
1
+ "use client";
2
+
3
+ import {
4
+ DndContext,
5
+ DragOverlay,
6
+ closestCenter,
7
+ KeyboardSensor,
8
+ PointerSensor,
9
+ useSensor,
10
+ useSensors,
11
+ type DragStartEvent,
12
+ type DragEndEvent,
13
+ } from "@dnd-kit/core";
14
+ import { useState, useCallback, useMemo, useEffect, memo, type ReactNode } from "react";
15
+ import { createPortal } from "react-dom";
16
+ import { useBuilderStore } from "../../lib/builder/store";
17
+ import { ALL_BLOCK_INFO } from "../../lib/builder/types";
18
+ import type { PageSectionV2, ParallaxGroup } from "../../lib/sanity/types";
19
+ import { isPageSectionV2, isParallaxGroup } from "../../lib/sanity/types";
20
+
21
+ // ============================================
22
+ // Scale Modifier for dnd-kit inside CSS-scaled canvas
23
+ // ============================================
24
+
25
+ function createScaleModifier(zoom: number) {
26
+ return ({ transform }: { transform: { x: number; y: number; scaleX: number; scaleY: number } }) => ({
27
+ ...transform,
28
+ x: transform.x / zoom,
29
+ y: transform.y / zoom,
30
+ });
31
+ }
32
+
33
+ // ============================================
34
+ // DnD ID Helpers
35
+ // ============================================
36
+
37
+ // IDs encode context: "row:KEY" or "block:ROWKEY:COLKEY:BLOCKKEY"
38
+ export function makeRowId(rowKey: string) {
39
+ return `row:${rowKey}`;
40
+ }
41
+
42
+ export function makeBlockId(rowKey: string, colKey: string, blockKey: string) {
43
+ return `block:${rowKey}:${colKey}:${blockKey}`;
44
+ }
45
+
46
+ export function makeColumnDroppableId(rowKey: string, colKey: string) {
47
+ return `col-drop:${rowKey}:${colKey}`;
48
+ }
49
+
50
+ function parseId(id: string) {
51
+ const parts = id.split(":");
52
+ if (parts[0] === "row" && parts.length >= 2) {
53
+ return { type: "row" as const, rowKey: parts[1] };
54
+ }
55
+ if (parts[0] === "block" && parts.length >= 4) {
56
+ return { type: "block" as const, rowKey: parts[1], colKey: parts[2], blockKey: parts[3] };
57
+ }
58
+ if (parts[0] === "col-drop" && parts.length >= 3) {
59
+ return { type: "column" as const, rowKey: parts[1], colKey: parts[2] };
60
+ }
61
+ return null;
62
+ }
63
+
64
+ // ============================================
65
+ // Drag Overlay Components
66
+ // ============================================
67
+
68
+ function RowDragOverlay({ rowKey }: { rowKey: string }) {
69
+ const rows = useBuilderStore((s) => s.rows);
70
+ const item = rows.find((r) => r._key === rowKey);
71
+ if (!item) return null;
72
+
73
+ // PageSectionV2
74
+ if (isPageSectionV2(item)) {
75
+ const v2Section = item as PageSectionV2;
76
+ const colCount = v2Section.columns?.length || 0;
77
+ return (
78
+ <div className="rounded border border-[#076bff] bg-[#1a1a1a]/90 px-4 py-3 shadow-lg shadow-[#076bff]/20 backdrop-blur-sm">
79
+ <div className="flex items-center gap-2">
80
+ <span className="text-[#076bff]">⠿</span>
81
+ <span className="text-xs text-white">V2 Section · {colCount} col{colCount !== 1 ? "s" : ""}</span>
82
+ </div>
83
+ </div>
84
+ );
85
+ }
86
+
87
+ // ParallaxGroup
88
+ if (isParallaxGroup(item)) {
89
+ const group = item as ParallaxGroup;
90
+ return (
91
+ <div className="rounded border border-[#8b5cf6] bg-[#1a1a1a]/90 px-4 py-3 shadow-lg shadow-[#8b5cf6]/20 backdrop-blur-sm">
92
+ <div className="flex items-center gap-2">
93
+ <span className="text-[#8b5cf6]">⠿</span>
94
+ <span className="text-xs text-white">Parallax Showcase · {group.slides.length} slide{group.slides.length !== 1 ? "s" : ""}</span>
95
+ </div>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ // PageSection
101
+ if ((item as { _type?: string })._type === "pageSection") {
102
+ const section = item as import("../../lib/sanity/types").PageSection;
103
+ const label = section.section_type === "projectGrid"
104
+ ? "Project Grid"
105
+ : "Section";
106
+ return (
107
+ <div className="rounded border border-[#93278f] bg-[#1a1a1a]/90 px-4 py-3 shadow-lg shadow-[#93278f]/20 backdrop-blur-sm">
108
+ <div className="flex items-center gap-2">
109
+ <span className="text-[#93278f]">⠿</span>
110
+ <span className="text-xs text-white">{label}</span>
111
+ </div>
112
+ </div>
113
+ );
114
+ }
115
+
116
+ return null;
117
+ }
118
+
119
+ const BlockDragOverlay = memo(function BlockDragOverlay({ blockKey, rowKey }: { blockKey: string; rowKey: string }) {
120
+ const rows = useBuilderStore((s) => s.rows);
121
+
122
+ // Find the content item — could be a top-level row or a parallax slide
123
+ let item = rows.find((r) => r._key === rowKey);
124
+ // If not found at top level, search inside parallax groups for a matching slide
125
+ if (!item) {
126
+ for (const r of rows) {
127
+ if (isParallaxGroup(r)) {
128
+ const group = r as ParallaxGroup;
129
+ const slide = group.slides.find((s) => s._key === rowKey);
130
+ if (slide) {
131
+ // Create a virtual section item for block lookup
132
+ item = { _type: "pageSectionV2", _key: slide._key, section_type: "empty-v2", columns: slide.columns, settings: slide.section_settings } as PageSectionV2;
133
+ break;
134
+ }
135
+ }
136
+ }
137
+ }
138
+ if (!item) return null;
139
+
140
+ // Handle V2 sections, PageSections, and virtual sections from parallax slides
141
+ let block: import("../../lib/sanity/types").ContentBlock | undefined;
142
+ if (isPageSectionV2(item)) {
143
+ const v2Section = item as PageSectionV2;
144
+ for (const col of v2Section.columns || []) {
145
+ const found = (col.blocks || []).find((b) => b._key === blockKey);
146
+ if (found) { block = found; break; }
147
+ }
148
+ } else if ((item as { _type?: string })._type === "pageSection") {
149
+ const section = item as import("../../lib/sanity/types").PageSection;
150
+ const sBlock = Array.isArray(section.block) ? section.block[0] : undefined;
151
+ if (sBlock && sBlock._key === blockKey) {
152
+ block = sBlock as import("../../lib/sanity/types").ContentBlock;
153
+ }
154
+ }
155
+
156
+ if (!block) return null;
157
+ const info = ALL_BLOCK_INFO.find((b) => b.type === block!._type);
158
+ return (
159
+ <div className="rounded border border-[#e28b00] bg-[#e28b00]/10 px-3 py-2 shadow-lg shadow-[#e28b00]/20 backdrop-blur-sm">
160
+ <div className="flex items-center gap-2">
161
+ <span className="text-xs">{info?.icon || "▪"}</span>
162
+ <span className="text-xs text-white">{info?.label || block._type}</span>
163
+ </div>
164
+ </div>
165
+ );
166
+ });
167
+
168
+ // ============================================
169
+ // DndWrapper — handles row reordering and block DnD
170
+ //
171
+ // Column drag is now handled by the custom useColumnDrag
172
+ // hook + ColumnDragContext (Session 111).
173
+ // ============================================
174
+
175
+ interface DndWrapperProps {
176
+ children: ReactNode;
177
+ }
178
+
179
+ interface ActiveDrag {
180
+ type: "row" | "block";
181
+ rowKey: string;
182
+ colKey?: string;
183
+ blockKey?: string;
184
+ }
185
+
186
+ export default function DndWrapper({ children }: DndWrapperProps) {
187
+ // Use individual selectors — avoid subscribing to the entire store which
188
+ // causes re-renders on every zoom/pan/selection/text change.
189
+ const canvasZoom = useBuilderStore((s) => s.canvasZoom);
190
+ const reorderRows = useBuilderStore((s) => s.reorderRows);
191
+ const reorderBlocks = useBuilderStore((s) => s.reorderBlocks);
192
+ const moveBlock = useBuilderStore((s) => s.moveBlock);
193
+ const [activeDrag, setActiveDrag] = useState<ActiveDrag | null>(null);
194
+
195
+ // Portal target for DragOverlay — must render outside the canvas transform
196
+ // container so overlay positioning uses viewport coordinates directly.
197
+ const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);
198
+ useEffect(() => {
199
+ setPortalTarget(document.body);
200
+ }, []);
201
+
202
+ const scaleModifier = useMemo(() => createScaleModifier(canvasZoom), [canvasZoom]);
203
+
204
+ const sensors = useSensors(
205
+ useSensor(PointerSensor, {
206
+ activationConstraint: {
207
+ distance: 8, // 8px before drag starts (avoids accidental drags)
208
+ },
209
+ }),
210
+ useSensor(KeyboardSensor)
211
+ );
212
+
213
+ const handleDragStart = useCallback((event: DragStartEvent) => {
214
+ const parsed = parseId(event.active.id as string);
215
+ if (!parsed) return;
216
+
217
+ if (parsed.type === "row") {
218
+ setActiveDrag({ type: "row", rowKey: parsed.rowKey });
219
+ } else if (parsed.type === "block") {
220
+ setActiveDrag({
221
+ type: "block",
222
+ rowKey: parsed.rowKey,
223
+ colKey: parsed.colKey,
224
+ blockKey: parsed.blockKey,
225
+ });
226
+ }
227
+ }, []);
228
+
229
+ const handleDragEnd = useCallback(
230
+ (event: DragEndEvent) => {
231
+ const { active, over } = event;
232
+ setActiveDrag(null);
233
+
234
+ if (!over || active.id === over.id) return;
235
+
236
+ const activeParsed = parseId(active.id as string);
237
+ const overParsed = parseId(over.id as string);
238
+
239
+ if (!activeParsed || !overParsed) return;
240
+
241
+ // Read rows at drag-end time (not subscribed — avoids re-renders)
242
+ const rows = useBuilderStore.getState().rows;
243
+
244
+ // Row reordering
245
+ if (activeParsed.type === "row" && overParsed.type === "row") {
246
+ const fromIndex = rows.findIndex((r) => r._key === activeParsed.rowKey);
247
+ const toIndex = rows.findIndex((r) => r._key === overParsed.rowKey);
248
+ if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
249
+ reorderRows(fromIndex, toIndex);
250
+ }
251
+ return;
252
+ }
253
+
254
+ // Block reordering / cross-column move
255
+ if (activeParsed.type === "block") {
256
+ const fromRowKey = activeParsed.rowKey;
257
+ const fromColKey = activeParsed.colKey!;
258
+ const blockKey = activeParsed.blockKey!;
259
+
260
+ if (overParsed.type === "block") {
261
+ const toRowKey = overParsed.rowKey;
262
+ const toColKey = overParsed.colKey!;
263
+ const toBlockKey = overParsed.blockKey!;
264
+
265
+ // Find target index
266
+ let toCol: { blocks?: { _key: string }[] } | undefined;
267
+
268
+ // Check if target is a V2 section
269
+ const toItem = rows.find((r) => r._key === toRowKey);
270
+ if (toItem && isPageSectionV2(toItem)) {
271
+ const v2Section = toItem as PageSectionV2;
272
+ toCol = v2Section.columns?.find((c) => c._key === toColKey);
273
+ }
274
+
275
+ const toIndex = (toCol?.blocks || []).findIndex((b) => b._key === toBlockKey);
276
+
277
+ if (fromRowKey === toRowKey && fromColKey === toColKey) {
278
+ // Same column reorder
279
+ let fromCol: { blocks?: { _key: string }[] } | undefined;
280
+ const fromItem = rows.find((r) => r._key === fromRowKey);
281
+ if (fromItem && isPageSectionV2(fromItem)) {
282
+ fromCol = (fromItem as PageSectionV2).columns?.find((c) => c._key === fromColKey);
283
+ }
284
+ const fromIndex = (fromCol?.blocks || []).findIndex((b) => b._key === blockKey);
285
+ if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
286
+ reorderBlocks(fromRowKey, fromColKey, fromIndex, toIndex);
287
+ }
288
+ } else {
289
+ // Cross-column move
290
+ moveBlock(blockKey, toRowKey, toColKey, toIndex);
291
+ }
292
+ }
293
+ }
294
+ },
295
+ [reorderRows, reorderBlocks, moveBlock]
296
+ );
297
+
298
+ return (
299
+ <DndContext
300
+ sensors={sensors}
301
+ collisionDetection={closestCenter}
302
+ onDragStart={handleDragStart}
303
+ onDragEnd={handleDragEnd}
304
+ modifiers={[scaleModifier]}
305
+ >
306
+ {children}
307
+ {/* Portal the DragOverlay to document.body so it escapes the canvas
308
+ transform container (translate3d + scale). Without this, the overlay
309
+ inherits the canvas CSS transform and its viewport-based positioning
310
+ is offset by the pan/zoom values — causing the ghost to jump far
311
+ from the cursor on drag start. React Context crosses portals, so
312
+ DragOverlay still communicates with DndContext correctly. */}
313
+ {portalTarget
314
+ ? createPortal(
315
+ <DragOverlay dropAnimation={null}>
316
+ {activeDrag?.type === "row" && (
317
+ <RowDragOverlay rowKey={activeDrag.rowKey} />
318
+ )}
319
+ {activeDrag?.type === "block" && activeDrag.blockKey && (
320
+ <BlockDragOverlay blockKey={activeDrag.blockKey} rowKey={activeDrag.rowKey} />
321
+ )}
322
+ </DragOverlay>,
323
+ portalTarget
324
+ )
325
+ : (
326
+ <DragOverlay dropAnimation={null}>
327
+ {activeDrag?.type === "row" && (
328
+ <RowDragOverlay rowKey={activeDrag.rowKey} />
329
+ )}
330
+ {activeDrag?.type === "block" && activeDrag.blockKey && (
331
+ <BlockDragOverlay blockKey={activeDrag.blockKey} rowKey={activeDrag.rowKey} />
332
+ )}
333
+ </DragOverlay>
334
+ )}
335
+ </DndContext>
336
+ );
337
+ }
@@ -0,0 +1,186 @@
1
+ "use client";
2
+
3
+ import { useMemo, memo } from "react";
4
+ import type { CascadeColumn } from "../../lib/builder/cascade-helpers";
5
+ import type { DropTarget } from "./hooks/useColumnDrag";
6
+ import { BUILDER_GREEN } from "../../lib/builder/constants";
7
+
8
+ // ============================================
9
+ // InsertionLines — Visual insertion indicators between adjacent columns
10
+ // ============================================
11
+ // Rendered during custom column drag. Shows thin green lines with a circle
12
+ // indicator between columns that have no gap between them, serving as
13
+ // drop targets for inserting a column between two neighbours.
14
+ //
15
+ // Extracted from SectionV2Canvas (Session G refactor).
16
+
17
+ /** Insertion point between two adjacent columns (no gap between them) */
18
+ interface InsertionPoint {
19
+ row: number;
20
+ gridColumn: number; // The grid column where the insertion line sits (= right column's start)
21
+ leftColKey: string;
22
+ rightColKey: string;
23
+ }
24
+
25
+ interface InsertionLinesProps {
26
+ displayCols: (CascadeColumn & { _key: string })[];
27
+ draggingColumnKey: string | null;
28
+ containerWidth: number;
29
+ gridColumns: number;
30
+ colGap: number;
31
+ sectionKey: string;
32
+ dropTarget: DropTarget | null;
33
+ }
34
+
35
+ export const InsertionLines = memo(function InsertionLines({
36
+ displayCols,
37
+ draggingColumnKey,
38
+ containerWidth,
39
+ gridColumns,
40
+ colGap,
41
+ sectionKey,
42
+ dropTarget,
43
+ }: InsertionLinesProps) {
44
+ // Compute insertion points between adjacent columns (no gap between them)
45
+ // Exclude the currently dragged column from consideration
46
+ const insertionPoints = useMemo((): InsertionPoint[] => {
47
+ const points: InsertionPoint[] = [];
48
+ // Group display columns by row
49
+ const rowMap = new Map<number, (CascadeColumn & { _key: string })[]>();
50
+ for (const dc of displayCols) {
51
+ // Skip the dragged column — it's "lifted" from the grid
52
+ if (dc._key === draggingColumnKey) continue;
53
+ const row = dc.grid_row;
54
+ if (!rowMap.has(row)) rowMap.set(row, []);
55
+ rowMap.get(row)!.push(dc);
56
+ }
57
+
58
+ for (const [row, cols] of rowMap) {
59
+ const sorted = [...cols].sort((a, b) => a.grid_column - b.grid_column);
60
+ for (let i = 0; i < sorted.length - 1; i++) {
61
+ const colA = sorted[i];
62
+ const colB = sorted[i + 1];
63
+ const aEnd = colA.grid_column + colA.span; // 1 past end
64
+ // Adjacent = no gap between them
65
+ if (aEnd === colB.grid_column) {
66
+ points.push({
67
+ row,
68
+ gridColumn: colB.grid_column,
69
+ leftColKey: colA._key,
70
+ rightColKey: colB._key,
71
+ });
72
+ }
73
+ }
74
+ }
75
+
76
+ return points;
77
+ }, [displayCols, draggingColumnKey]);
78
+
79
+ // Compute pixel positions for insertion lines
80
+ const insertionLinePositions = useMemo(() => {
81
+ if (containerWidth === 0) return [];
82
+
83
+ // Each 1fr unit width: (containerWidth - totalGaps) / gridColumns
84
+ const totalGaps = (gridColumns - 1) * colGap;
85
+ const unitWidth = (containerWidth - totalGaps) / gridColumns;
86
+
87
+ return insertionPoints.map((pt) => {
88
+ // The left pixel of the grid column `gc` is:
89
+ // (gc - 1) * unitWidth + (gc - 1) * colGap = (gc - 1) * (unitWidth + colGap)
90
+ const leftPxOfRightCol = (pt.gridColumn - 1) * (unitWidth + colGap);
91
+ // The insertion line sits in the middle of the gap between the two columns
92
+ const linePx = leftPxOfRightCol - colGap / 2;
93
+ return { ...pt, pixelLeft: linePx };
94
+ });
95
+ }, [containerWidth, insertionPoints, gridColumns, colGap]);
96
+
97
+ if (insertionLinePositions.length === 0) return null;
98
+
99
+ return (
100
+ <>
101
+ {insertionLinePositions.map((pt) => {
102
+ const isActive =
103
+ dropTarget?.type === "insert" &&
104
+ dropTarget.sectionKey === sectionKey &&
105
+ dropTarget.insertRow === pt.row &&
106
+ dropTarget.insertCol === pt.gridColumn;
107
+ // Hit-test zone: at least the gap width + 10px padding, but never less than 24px.
108
+ // The 24px minimum ensures the target is usable even with very small (or zero) column gaps.
109
+ const hitWidth = Math.max(colGap + 10, 24);
110
+
111
+ return (
112
+ <div
113
+ key={`insert-${pt.row}-${pt.gridColumn}`}
114
+ style={{
115
+ gridColumn: "1 / -1",
116
+ gridRow: pt.row,
117
+ position: "relative",
118
+ pointerEvents: "none",
119
+ zIndex: 20,
120
+ alignSelf: "stretch",
121
+ minHeight: 0,
122
+ }}
123
+ >
124
+ <div
125
+ data-col-v2-insert=""
126
+ data-section-key={sectionKey}
127
+ data-insert-row={pt.row}
128
+ data-insert-col={pt.gridColumn}
129
+ data-insert-left-key={pt.leftColKey}
130
+ data-insert-right-key={pt.rightColKey}
131
+ style={{
132
+ position: "absolute",
133
+ left: pt.pixelLeft - hitWidth / 2,
134
+ top: 0,
135
+ bottom: 0,
136
+ width: hitWidth,
137
+ pointerEvents: "auto",
138
+ }}
139
+ >
140
+ {/* Visual insertion line — green when active */}
141
+ <div
142
+ style={{
143
+ position: "absolute",
144
+ left: "50%",
145
+ top: 4,
146
+ bottom: 4,
147
+ width: isActive ? 4 : 0,
148
+ transform: "translateX(-50%)",
149
+ background: BUILDER_GREEN,
150
+ borderRadius: 2,
151
+ transition: "width 100ms ease-out, opacity 100ms ease-out",
152
+ opacity: isActive ? 1 : 0,
153
+ }}
154
+ />
155
+ {/* Circle indicator at center */}
156
+ {isActive && (
157
+ <div
158
+ style={{
159
+ position: "absolute",
160
+ left: "50%",
161
+ top: "50%",
162
+ transform: "translate(-50%, -50%)",
163
+ width: 14,
164
+ height: 14,
165
+ borderRadius: "50%",
166
+ background: BUILDER_GREEN,
167
+ border: "2px solid white",
168
+ boxShadow: "0 1px 4px rgba(34, 197, 94, 0.4)",
169
+ display: "flex",
170
+ alignItems: "center",
171
+ justifyContent: "center",
172
+ fontSize: 10,
173
+ color: "white",
174
+ fontWeight: "bold",
175
+ }}
176
+ >
177
+ +
178
+ </div>
179
+ )}
180
+ </div>
181
+ </div>
182
+ );
183
+ })}
184
+ </>
185
+ );
186
+ });