@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,849 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useCallback, useRef } from "react";
4
+ import { useParams, usePathname } from "next/navigation";
5
+ import Link from "next/link";
6
+ import { useBuilderStore } from "../../../../lib/builder/store";
7
+ import {
8
+ DndWrapper,
9
+ SortableRow,
10
+ SortableBlock,
11
+ SectionTypePicker,
12
+ BlockTypePicker,
13
+ SettingsPanel,
14
+ BuilderCanvas,
15
+ makeRowId,
16
+ } from "../../../../components/builder";
17
+ import {
18
+ SortableContext,
19
+ verticalListSortingStrategy,
20
+ } from "@dnd-kit/sortable";
21
+ import type { Page, PageSection, PageSectionV2, ParallaxGroup, SectionColumn, CustomSectionInstance, CustomSectionListItem } from "../../../../lib/sanity/types";
22
+ import { isPageSection, isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../../../lib/sanity/types";
23
+ import SectionEditorBar from "../../../../components/builder/SectionEditorBar";
24
+ import CustomSectionInstanceCard from "../../../../components/builder/CustomSectionInstanceCard";
25
+ import { ColumnDragProvider } from "../../../../components/builder/ColumnDragContext";
26
+ import type { BlockType } from "../../../../lib/builder/types";
27
+ import { findGaps } from "../../../../lib/builder/cascade";
28
+ import { isSectionBlockType } from "../../../../lib/builder/types";
29
+ import BlockLivePreview from "../../../../components/builder/BlockLivePreview";
30
+ import SectionV2Canvas from "../../../../components/builder/SectionV2Canvas";
31
+ import ParallaxGroupCanvas from "../../../../components/builder/ParallaxGroupCanvas";
32
+ import { ThumbStatusProvider } from "../../../../lib/contexts/ThumbStatusContext";
33
+ import PublishToggle from "../../../../components/admin/PublishToggle";
34
+
35
+ // ============================================
36
+ // Preview helper — opens the page in a new tab
37
+ // ============================================
38
+
39
+ function openPreview(store: { pageSlug: string }) {
40
+ // Use the dedicated /preview route which includes draft pages.
41
+ // All pages are fetched by slug.
42
+ const params = new URLSearchParams();
43
+ params.set("slug", store.pageSlug);
44
+ window.open(`/preview?${params}`, "_blank");
45
+ }
46
+
47
+ // ============================================
48
+ // Help Modal — shows all keyboard shortcuts
49
+ // ============================================
50
+
51
+ function HelpModal({ onClose }: { onClose: () => void }) {
52
+ useEffect(() => {
53
+ function handleKey(e: KeyboardEvent) {
54
+ if (e.key === "Escape" || e.key === "?") {
55
+ e.preventDefault();
56
+ onClose();
57
+ }
58
+ }
59
+ window.addEventListener("keydown", handleKey);
60
+ return () => window.removeEventListener("keydown", handleKey);
61
+ }, [onClose]);
62
+
63
+ const shortcuts = [
64
+ { keys: "Ctrl+S", action: "Save page" },
65
+ { keys: "Ctrl+Z", action: "Undo" },
66
+ { keys: "Ctrl+Shift+Z", action: "Redo" },
67
+ { keys: "Ctrl+D", action: "Duplicate selected block" },
68
+ { keys: "Delete / Backspace", action: "Delete selected block" },
69
+ { keys: "Escape", action: "Clear selection" },
70
+ { keys: "P", action: "Open preview in new tab" },
71
+ { keys: "V", action: "Select tool" },
72
+ { keys: "H", action: "Hand (pan) tool" },
73
+ { keys: "Space + drag", action: "Temporary pan" },
74
+ { keys: "Ctrl + / Ctrl −", action: "Zoom in / out" },
75
+ { keys: "Ctrl+0", action: "Zoom to fit" },
76
+ { keys: "Scroll", action: "Pan canvas" },
77
+ { keys: "Ctrl+Scroll", action: "Zoom at cursor" },
78
+ { keys: "?", action: "Toggle this help" },
79
+ ];
80
+
81
+ return (
82
+ <div
83
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
84
+ onClick={onClose}
85
+ >
86
+ <div
87
+ className="bg-white border border-neutral-200 rounded-lg p-6 max-w-md w-full mx-4 shadow-xl"
88
+ onClick={(e) => e.stopPropagation()}
89
+ >
90
+ <div className="flex items-center justify-between mb-5">
91
+ <h2 className="text-sm font-semibold text-neutral-900">Keyboard Shortcuts</h2>
92
+ <button
93
+ onClick={onClose}
94
+ className="text-neutral-400 hover:text-neutral-900 transition-colors text-sm"
95
+ >
96
+ &times;
97
+ </button>
98
+ </div>
99
+ <div className="space-y-2">
100
+ {shortcuts.map((s) => (
101
+ <div key={s.keys} className="flex items-center justify-between py-1.5">
102
+ <span className="text-xs text-neutral-500">{s.action}</span>
103
+ <kbd className="text-[10px] bg-neutral-100 border border-neutral-200 rounded px-2 py-0.5 text-neutral-600">
104
+ {s.keys}
105
+ </kbd>
106
+ </div>
107
+ ))}
108
+ </div>
109
+ <div className="mt-5 pt-4 border-t border-neutral-200">
110
+ <p className="text-[10px] text-neutral-400">
111
+ Right-click a block for context menu (duplicate, delete)
112
+ </p>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ );
117
+ }
118
+
119
+ // ============================================
120
+ // Context Menu for blocks
121
+ // ============================================
122
+
123
+ interface ContextMenuState {
124
+ x: number;
125
+ y: number;
126
+ rowKey: string;
127
+ colKey: string;
128
+ blockKey: string;
129
+ }
130
+
131
+ function BlockContextMenu({
132
+ menu,
133
+ onClose,
134
+ onDuplicate,
135
+ onDelete,
136
+ }: {
137
+ menu: ContextMenuState;
138
+ onClose: () => void;
139
+ onDuplicate: () => void;
140
+ onDelete: () => void;
141
+ }) {
142
+ useEffect(() => {
143
+ const close = () => onClose();
144
+ window.addEventListener("click", close);
145
+ window.addEventListener("contextmenu", close);
146
+ return () => {
147
+ window.removeEventListener("click", close);
148
+ window.removeEventListener("contextmenu", close);
149
+ };
150
+ }, [onClose]);
151
+
152
+ return (
153
+ <div
154
+ className="fixed z-50 bg-white border border-neutral-200 rounded-xl shadow-xl py-1 min-w-[160px]"
155
+ style={{ left: menu.x, top: menu.y }}
156
+ onClick={(e) => e.stopPropagation()}
157
+ >
158
+ <button
159
+ onClick={() => {
160
+ onDuplicate();
161
+ onClose();
162
+ }}
163
+ className="w-full text-left px-3 py-1.5 text-sm text-neutral-600 hover:bg-neutral-50 hover:text-neutral-900 transition-colors flex items-center gap-2"
164
+ >
165
+ <span className="text-neutral-400">⧉</span> Duplicate
166
+ <span className="ml-auto text-[9px] text-neutral-400">Ctrl+D</span>
167
+ </button>
168
+ <button
169
+ onClick={() => {
170
+ onDelete();
171
+ onClose();
172
+ }}
173
+ className="w-full text-left px-3 py-1.5 text-sm text-neutral-600 hover:bg-[var(--admin-error)]/10 hover:text-[var(--admin-error)] transition-colors flex items-center gap-2"
174
+ >
175
+ <span className="text-neutral-400">&times;</span> Delete
176
+ <span className="ml-auto text-[9px] text-neutral-400">Del</span>
177
+ </button>
178
+ </div>
179
+ );
180
+ }
181
+
182
+ // ============================================
183
+ // Editor Shell
184
+ // ============================================
185
+
186
+ export default function PageEditorPage() {
187
+ const params = useParams();
188
+ const pathname = usePathname();
189
+ const slug = params.slug as string;
190
+ const isProjectRoute = pathname.startsWith("/admin/projects/");
191
+
192
+ const [loading, setLoading] = useState(true);
193
+ const [error, setError] = useState<string | null>(null);
194
+ const [showSectionPicker, setShowSectionPicker] = useState(false);
195
+ const [showHelp, setShowHelp] = useState(false);
196
+ const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
197
+ const [addBlockTarget, setAddBlockTarget] = useState<{
198
+ rowKey: string;
199
+ colKey: string;
200
+ insertIndex?: number;
201
+ isV2?: boolean;
202
+ } | null>(null);
203
+
204
+ // Preloaded custom sections list (fetched once, avoids per-modal-open fetch)
205
+ const [cachedCustomSections, setCachedCustomSections] = useState<CustomSectionListItem[] | null>(null);
206
+
207
+ // Preload custom sections list on mount
208
+ useEffect(() => {
209
+ let cancelled = false;
210
+ fetch("/api/admin/custom-sections")
211
+ .then((res) => res.ok ? res.json() : null)
212
+ .then((data) => {
213
+ if (!cancelled && data?.sections) setCachedCustomSections(data.sections);
214
+ })
215
+ .catch(() => { /* silently fail — SectionTypePicker will retry if needed */ });
216
+ return () => { cancelled = true; };
217
+ }, []);
218
+
219
+ /** Refresh the cached custom sections list (call after create/delete) */
220
+ const refreshCustomSections = useCallback(() => {
221
+ fetch("/api/admin/custom-sections")
222
+ .then((res) => res.ok ? res.json() : null)
223
+ .then((data) => {
224
+ if (data?.sections) setCachedCustomSections(data.sections);
225
+ })
226
+ .catch(() => {});
227
+ }, []);
228
+
229
+ // Canvas viewport ref (for zoom-to-fit from keyboard shortcut)
230
+ const canvasViewportRef = useRef<HTMLDivElement>(null);
231
+
232
+ // Zustand store
233
+ const store = useBuilderStore();
234
+
235
+ // Refresh custom sections list when returning from section editor (may have created a new one)
236
+ const prevEditorMode = useRef(store.editorMode);
237
+ useEffect(() => {
238
+ if (prevEditorMode.current === "customSection" && store.editorMode !== "customSection") {
239
+ refreshCustomSections();
240
+ }
241
+ prevEditorMode.current = store.editorMode;
242
+ }, [store.editorMode, refreshCustomSections]);
243
+
244
+ // Load page data on mount
245
+ useEffect(() => {
246
+ async function loadPage() {
247
+ try {
248
+ const res = await fetch(`/api/admin/pages/${slug}`);
249
+ if (!res.ok) {
250
+ setError(res.status === 404 ? "Page not found" : "Failed to load page");
251
+ return;
252
+ }
253
+ const data = await res.json();
254
+ store.loadFromDocument(data.page as Page);
255
+ // Apply global styles as page defaults (background, text color)
256
+ store.applyGlobalStyles();
257
+ } catch {
258
+ setError("Network error");
259
+ } finally {
260
+ setLoading(false);
261
+ }
262
+ }
263
+ loadPage();
264
+ // eslint-disable-next-line react-hooks/exhaustive-deps
265
+ }, [slug]);
266
+
267
+ // Warn before leaving with unsaved changes
268
+ useEffect(() => {
269
+ const handleBeforeUnload = (e: BeforeUnloadEvent) => {
270
+ if (store.isDirty) {
271
+ e.preventDefault();
272
+ }
273
+ };
274
+ window.addEventListener("beforeunload", handleBeforeUnload);
275
+ return () => window.removeEventListener("beforeunload", handleBeforeUnload);
276
+ }, [store.isDirty]);
277
+
278
+ // Keyboard shortcuts
279
+ useEffect(() => {
280
+ function handleKeyDown(e: KeyboardEvent) {
281
+ // Skip all shortcuts when inside asset browser modal (portal on document.body)
282
+ if ((e.target as HTMLElement)?.closest?.("[data-asset-modal]")) return;
283
+
284
+ // Don't intercept when typing in inputs
285
+ const tag = (e.target as HTMLElement)?.tagName;
286
+ const isEditable = (e.target as HTMLElement)?.isContentEditable;
287
+ const isInput = tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || isEditable;
288
+
289
+ // Ctrl/Cmd+S to save
290
+ if ((e.metaKey || e.ctrlKey) && e.key === "s") {
291
+ e.preventDefault();
292
+ if (store.isDirty && !store.isSaving) {
293
+ store.save();
294
+ }
295
+ return;
296
+ }
297
+
298
+ // Ctrl/Cmd+Z to undo (without Shift)
299
+ if ((e.metaKey || e.ctrlKey) && e.key === "z" && !e.shiftKey) {
300
+ e.preventDefault();
301
+ store.undo();
302
+ return;
303
+ }
304
+
305
+ // Ctrl/Cmd+Shift+Z to redo
306
+ if ((e.metaKey || e.ctrlKey) && e.key === "Z" && e.shiftKey) {
307
+ e.preventDefault();
308
+ store.redo();
309
+ return;
310
+ }
311
+ // Also support Ctrl+Y for redo
312
+ if ((e.metaKey || e.ctrlKey) && e.key === "y") {
313
+ e.preventDefault();
314
+ store.redo();
315
+ return;
316
+ }
317
+
318
+ // Ctrl/Cmd+D to duplicate selected block
319
+ if ((e.metaKey || e.ctrlKey) && e.key === "d") {
320
+ e.preventDefault();
321
+ if (store.selectedBlockKey) {
322
+ store.duplicateBlock(store.selectedBlockKey);
323
+ }
324
+ return;
325
+ }
326
+
327
+ // Escape to clear selection / close modals
328
+ if (e.key === "Escape") {
329
+ if (contextMenu) {
330
+ setContextMenu(null);
331
+ } else {
332
+ store.clearSelection();
333
+ }
334
+ return;
335
+ }
336
+
337
+ // Delete/Backspace to delete selected block
338
+ if ((e.key === "Delete" || e.key === "Backspace") && store.selectedBlockKey && !isInput) {
339
+ store.deleteBlock(store.selectedBlockKey);
340
+ }
341
+
342
+ // P to open preview in new tab
343
+ if (e.key === "p" && !isInput && !e.metaKey && !e.ctrlKey) {
344
+ e.preventDefault();
345
+ openPreview(store);
346
+ return;
347
+ }
348
+
349
+ // ? to toggle help modal
350
+ if (e.key === "?" && !isInput) {
351
+ e.preventDefault();
352
+ setShowHelp((prev) => !prev);
353
+ return;
354
+ }
355
+
356
+ // ---- Canvas shortcuts ----
357
+
358
+ // V → Select tool
359
+ if (e.key === "v" && !isInput && !e.metaKey && !e.ctrlKey) {
360
+ e.preventDefault();
361
+ store.setCanvasTool("select");
362
+ return;
363
+ }
364
+
365
+ // H → Hand tool
366
+ if (e.key === "h" && !isInput && !e.metaKey && !e.ctrlKey) {
367
+ e.preventDefault();
368
+ store.setCanvasTool("hand");
369
+ return;
370
+ }
371
+
372
+ // Ctrl/Cmd + = or + → Zoom in
373
+ if ((e.metaKey || e.ctrlKey) && (e.key === "=" || e.key === "+")) {
374
+ e.preventDefault();
375
+ store.setCanvasZoom(store.canvasZoom + 0.1);
376
+ return;
377
+ }
378
+
379
+ // Ctrl/Cmd + - → Zoom out
380
+ if ((e.metaKey || e.ctrlKey) && e.key === "-") {
381
+ e.preventDefault();
382
+ store.setCanvasZoom(store.canvasZoom - 0.1);
383
+ return;
384
+ }
385
+
386
+ // Ctrl/Cmd + 0 → Zoom to fit
387
+ if ((e.metaKey || e.ctrlKey) && e.key === "0") {
388
+ e.preventDefault();
389
+ const el = canvasViewportRef.current;
390
+ if (el) {
391
+ const rect = el.getBoundingClientRect();
392
+ store.zoomToFit(rect.width, rect.height);
393
+ }
394
+ return;
395
+ }
396
+ }
397
+ window.addEventListener("keydown", handleKeyDown);
398
+ return () => window.removeEventListener("keydown", handleKeyDown);
399
+ }, [store, contextMenu]);
400
+
401
+ // Handle save
402
+ const handleSave = useCallback(async () => {
403
+ await store.save();
404
+ }, [store]);
405
+
406
+ // Handle add empty section (with column layout preset)
407
+ // Handle add V2 empty section (with preset)
408
+ // afterRowKey=null → always appends to end of page (bottom "Add Section" button)
409
+ const handleAddEmptySectionV2 = useCallback(
410
+ (preset: "full" | "halves" | "thirds" | "quarters" | "1/3+2/3" | "2/3+1/3") => {
411
+ store.addSectionV2(preset, null);
412
+ setShowSectionPicker(false);
413
+ },
414
+ [store]
415
+ );
416
+
417
+ // Handle add page section (project grid)
418
+ // afterRowKey=null → always appends to end of page (bottom "Add Section" button)
419
+ const handleAddSection = useCallback(
420
+ (blockType: "projectGridBlock") => {
421
+ store.addSection(blockType, null);
422
+ setShowSectionPicker(false);
423
+ },
424
+ [store]
425
+ );
426
+
427
+ // Handle insert custom section instance
428
+ const handleSelectCustomSection = useCallback(
429
+ (section: CustomSectionListItem) => {
430
+ store.addCustomSectionInstance(section._id, section.slug.current, section.title, null);
431
+ setShowSectionPicker(false);
432
+ },
433
+ [store]
434
+ );
435
+
436
+ // Handle create new custom section (enter section editor with blank section)
437
+ const handleCreateCustomSection = useCallback(() => {
438
+ store.enterSectionEditor(null, null, null);
439
+ setShowSectionPicker(false);
440
+ }, [store]);
441
+
442
+ // Handle add parallax group
443
+ const handleAddParallaxGroup = useCallback(() => {
444
+ store.addParallaxGroup(null);
445
+ setShowSectionPicker(false);
446
+ }, [store]);
447
+
448
+ // Handle add block — V2 sections only
449
+ const handleAddBlock = useCallback(
450
+ (type: BlockType) => {
451
+ if (!addBlockTarget) return;
452
+ store.addBlockV2(addBlockTarget.rowKey, addBlockTarget.colKey, type, addBlockTarget.insertIndex);
453
+ setAddBlockTarget(null);
454
+ },
455
+ [store, addBlockTarget]
456
+ );
457
+
458
+ // Handle add block target from V2 sections
459
+ const handleAddBlockTargetV2 = useCallback(
460
+ (sectionKey: string, colKey: string, insertIndex?: number) => {
461
+ setAddBlockTarget({ rowKey: sectionKey, colKey, insertIndex, isV2: true });
462
+ },
463
+ []
464
+ );
465
+
466
+ // Handle block context menu
467
+ const handleBlockContextMenu = useCallback(
468
+ (e: React.MouseEvent, rowKey: string, colKey: string, blockKey: string) => {
469
+ e.preventDefault();
470
+ e.stopPropagation();
471
+ setContextMenu({ x: e.clientX, y: e.clientY, rowKey, colKey, blockKey });
472
+ },
473
+ []
474
+ );
475
+
476
+ // Row IDs for sortable context
477
+ const rowIds = store.rows.map((r) => makeRowId(r._key));
478
+
479
+ // Loading state
480
+ if (loading) {
481
+ return (
482
+ <div className="flex items-center justify-center h-full">
483
+ <p className="text-xs text-neutral-400 animate-pulse">
484
+ Loading editor...
485
+ </p>
486
+ </div>
487
+ );
488
+ }
489
+
490
+ // Error state
491
+ if (error) {
492
+ return (
493
+ <div className="flex flex-col items-center justify-center h-full gap-4">
494
+ <p className="text-sm text-[var(--admin-error)]">{error}</p>
495
+ <Link
496
+ href={isProjectRoute ? "/admin/projects" : "/admin/pages"}
497
+ className="text-xs text-neutral-400 hover:text-neutral-900"
498
+ >
499
+ &larr; Back to {isProjectRoute ? "projects" : "pages"}
500
+ </Link>
501
+ </div>
502
+ );
503
+ }
504
+
505
+ const editorMode = store.editorMode;
506
+ const isInSectionEditor = editorMode === "customSection";
507
+
508
+ return (
509
+ <div className="flex flex-col h-full">
510
+ {/* ---- Section Editor Bar (replaces toolbar in customSection mode) ---- */}
511
+ {isInSectionEditor && <SectionEditorBar />}
512
+
513
+ {/* ---- Toolbar (hidden in section editor mode) ---- */}
514
+ {!isInSectionEditor && <div className="flex items-center justify-between bg-white border-b border-neutral-200 px-4 h-14 shrink-0">
515
+ <div className="flex items-center gap-3">
516
+ <Link
517
+ href={store.pageType === "project" ? "/admin/projects" : "/admin/pages"}
518
+ className="text-xs text-neutral-400 hover:text-neutral-700 transition-colors flex items-center gap-1"
519
+ >
520
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><polyline points="15 18 9 12 15 6" /></svg>
521
+ {store.pageType === "project" ? "Projects" : "Pages"}
522
+ </Link>
523
+ <span className="text-neutral-200">|</span>
524
+ <span className="text-sm font-semibold text-neutral-800">{store.pageTitle}</span>
525
+ <span className="text-xs text-neutral-400 uppercase px-2 py-0.5 border border-neutral-200 rounded-lg bg-neutral-50">
526
+ {store.pageType}
527
+ </span>
528
+ <PublishToggle
529
+ mode="builder"
530
+ isDraft={store.draftMode}
531
+ onPublish={() => store.publishPage()}
532
+ onUnpublish={() => store.unpublishPage()}
533
+ />
534
+ {store.isDirty && (
535
+ <span className="text-xs text-amber-500 animate-pulse">
536
+ Unsaved changes
537
+ </span>
538
+ )}
539
+
540
+ </div>
541
+ <div className="flex items-center gap-2">
542
+ {/* Undo/Redo buttons */}
543
+ <button
544
+ onClick={() => store.undo()}
545
+ disabled={!store.canUndo()}
546
+ className="rounded-lg px-2 py-1 text-xs text-neutral-400 hover:text-neutral-700 hover:bg-neutral-100 transition-colors disabled:opacity-20 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:text-neutral-400"
547
+ title="Undo (Ctrl+Z)"
548
+ >
549
+
550
+ </button>
551
+ <button
552
+ onClick={() => store.redo()}
553
+ disabled={!store.canRedo()}
554
+ className="rounded-lg px-2 py-1 text-xs text-neutral-400 hover:text-neutral-700 hover:bg-neutral-100 transition-colors disabled:opacity-20 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:text-neutral-400"
555
+ title="Redo (Ctrl+Shift+Z)"
556
+ >
557
+
558
+ </button>
559
+ <span className="text-neutral-200 mx-1">|</span>
560
+
561
+ {/* Help button */}
562
+ <button
563
+ onClick={() => setShowHelp(true)}
564
+ className="rounded-lg px-2 py-1 text-xs text-neutral-400 hover:text-neutral-700 hover:bg-neutral-100 transition-colors"
565
+ title="Keyboard shortcuts (?)"
566
+ >
567
+ ?
568
+ </button>
569
+
570
+ {/* Page settings gear button */}
571
+ <button
572
+ onClick={() => store.clearSelection()}
573
+ className="rounded-lg px-2 py-1 text-neutral-400 hover:text-neutral-700 hover:bg-neutral-100 transition-colors"
574
+ title="Page settings"
575
+ >
576
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
577
+ <circle cx="12" cy="12" r="3" />
578
+ <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
579
+ </svg>
580
+ </button>
581
+ <span className="text-neutral-200 mx-1">|</span>
582
+
583
+ {store.saveError && (
584
+ <span className="text-xs text-red-500">
585
+ {store.saveError}
586
+ </span>
587
+ )}
588
+ {store.lastSavedAt && !store.isDirty && (
589
+ <span className="text-xs text-neutral-400">
590
+ Saved {new Date(store.lastSavedAt).toLocaleTimeString()}
591
+ </span>
592
+ )}
593
+ <span className="text-xs text-neutral-300">
594
+ Ctrl+S
595
+ </span>
596
+ {/* Preview in new tab */}
597
+ <button
598
+ onClick={() => openPreview(store)}
599
+ className="flex items-center gap-1.5 rounded-lg border border-neutral-200 bg-white px-3 py-1.5 text-xs font-medium text-neutral-500 hover:text-neutral-700 hover:border-neutral-300 transition-colors"
600
+ title="Open page preview in new tab"
601
+ >
602
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="shrink-0">
603
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
604
+ <polyline points="15 3 21 3 21 9" />
605
+ <line x1="10" y1="14" x2="21" y2="3" />
606
+ </svg>
607
+ Preview
608
+ </button>
609
+ <button
610
+ onClick={handleSave}
611
+ disabled={store.isSaving || !store.isDirty}
612
+ className="rounded-lg bg-[#076bff] px-5 py-1.5 text-sm font-medium text-white hover:bg-[#0559d4] transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
613
+ >
614
+ {store.isSaving ? "Saving..." : "Save"}
615
+ </button>
616
+ </div>
617
+ </div>}
618
+
619
+ {/* ---- Canvas + Settings ---- */}
620
+ <div className="flex flex-1 overflow-hidden">
621
+ {/* Canvas viewport */}
622
+ <ThumbStatusProvider>
623
+ <BuilderCanvas>
624
+ <div onClick={() => store.clearSelection()}>
625
+ <DndWrapper>
626
+ <ColumnDragProvider>
627
+ {isInSectionEditor ? (
628
+ /* Section editor mode: render single V2 section without SortableRow chrome */
629
+ <div>
630
+ {store.rows.length > 0 && isPageSectionV2(store.rows[0]) && (
631
+ <SectionV2Canvas
632
+ section={store.rows[0] as PageSectionV2}
633
+ onAddBlockTarget={handleAddBlockTargetV2}
634
+ />
635
+ )}
636
+ </div>
637
+ ) : store.rows.length === 0 ? (
638
+ /* Empty state */
639
+ <div className="flex flex-col items-center justify-center h-full min-h-[400px] border border-dashed border-neutral-300 rounded-lg py-20">
640
+ <div className="w-12 h-12 rounded-full border border-neutral-300 flex items-center justify-center mb-4">
641
+ <span className="text-neutral-400 text-lg">+</span>
642
+ </div>
643
+ <p className="text-sm text-neutral-500 mb-2">
644
+ This page has no content yet
645
+ </p>
646
+ <p className="text-[10px] text-neutral-400 mb-6">
647
+ Add your first section to start building
648
+ </p>
649
+ <button
650
+ onClick={(e) => {
651
+ e.stopPropagation();
652
+ setShowSectionPicker(true);
653
+ }}
654
+ className="rounded-lg bg-[#076bff] px-4 py-2 text-xs text-white hover:bg-[#0559d4] transition-colors"
655
+ >
656
+ + Add First Section
657
+ </button>
658
+ </div>
659
+ ) : (
660
+ /* Canvas with rows */
661
+ <div>
662
+ <SortableContext
663
+ items={rowIds}
664
+ strategy={verticalListSortingStrategy}
665
+ >
666
+ {store.rows.map((item, rowIndex) => {
667
+ const isV2Section = isPageSectionV2(item);
668
+ const isSection = isPageSection(item);
669
+ const isInstance = isCustomSectionInstance(item);
670
+ const isParallax = isParallaxGroup(item);
671
+ const section = isSection ? (item as PageSection) : null;
672
+ const v2Section = isV2Section ? (item as PageSectionV2) : null;
673
+
674
+ // Custom Section Instance — rendered directly without SortableRow chrome
675
+ if (isInstance) {
676
+ return (
677
+ <SortableRow
678
+ key={item._key}
679
+ rowKey={item._key}
680
+ row={item}
681
+ isSelected={store.selectedRowKey === item._key}
682
+ columnCount={0}
683
+ onSelect={() => store.selectRow(item._key)}
684
+ onDelete={() => store.deleteSection(item._key)}
685
+ onAddColumn={() => {}}
686
+ onDuplicate={() => {}}
687
+ onMoveUp={() => { if (rowIndex > 0) store.reorderRows(rowIndex, rowIndex - 1); }}
688
+ onMoveDown={() => { if (rowIndex < store.rows.length - 1) store.reorderRows(rowIndex, rowIndex + 1); }}
689
+ isFirst={rowIndex === 0}
690
+ isLast={rowIndex === store.rows.length - 1}
691
+ >
692
+ <CustomSectionInstanceCard
693
+ instance={item as CustomSectionInstance}
694
+ onAddBlockTarget={handleAddBlockTargetV2}
695
+ />
696
+ </SortableRow>
697
+ );
698
+ }
699
+
700
+ // Parallax Group — rendered with its own canvas component
701
+ if (isParallax) {
702
+ const group = item as ParallaxGroup;
703
+ return (
704
+ <SortableRow
705
+ key={item._key}
706
+ rowKey={item._key}
707
+ row={item}
708
+ isSelected={store.selectedRowKey === item._key}
709
+ columnCount={0}
710
+ onSelect={() => store.selectRow(item._key)}
711
+ onDelete={() => store.deleteSection(item._key)}
712
+ onAddColumn={() => {}}
713
+ onDuplicate={() => store.duplicateSection(item._key)}
714
+ onMoveUp={() => { if (rowIndex > 0) store.reorderRows(rowIndex, rowIndex - 1); }}
715
+ onMoveDown={() => { if (rowIndex < store.rows.length - 1) store.reorderRows(rowIndex, rowIndex + 1); }}
716
+ isFirst={rowIndex === 0}
717
+ isLast={rowIndex === store.rows.length - 1}
718
+ >
719
+ <ParallaxGroupCanvas
720
+ group={group}
721
+ onAddBlockTarget={handleAddBlockTargetV2}
722
+ />
723
+ </SortableRow>
724
+ );
725
+ }
726
+
727
+ return (
728
+ <SortableRow
729
+ key={item._key}
730
+ rowKey={item._key}
731
+ row={item}
732
+ isSelected={store.selectedRowKey === item._key}
733
+ columnCount={isV2Section ? (v2Section!.columns || []).length : 0}
734
+ onSelect={() => {
735
+ store.selectRow(item._key);
736
+ }}
737
+ onDelete={() => {
738
+ store.deleteSection(item._key);
739
+ }}
740
+ onAddColumn={() => {
741
+ if (isV2Section) {
742
+ const sec = v2Section!;
743
+ const gridCols = sec.settings.grid_columns || 12;
744
+ const cascadeCols = sec.columns.map((c: SectionColumn) => ({
745
+ _key: c._key, grid_column: c.grid_column, grid_row: c.grid_row, span: c.span,
746
+ }));
747
+ const gapList = findGaps(cascadeCols, gridCols);
748
+ if (gapList.length > 0) {
749
+ store.addColumnV2(sec._key, gapList[0].grid_row, gapList[0].grid_column, gapList[0].span);
750
+ } else {
751
+ const maxRow = cascadeCols.reduce((max, c) => Math.max(max, c.grid_row), 1);
752
+ store.addColumnV2(sec._key, maxRow + 1, 1, gridCols);
753
+ }
754
+ }
755
+ }}
756
+ onDuplicate={() => {
757
+ store.duplicateSection(item._key);
758
+ }}
759
+ onMoveUp={() => {
760
+ if (rowIndex > 0) store.reorderRows(rowIndex, rowIndex - 1);
761
+ }}
762
+ onMoveDown={() => {
763
+ if (rowIndex < store.rows.length - 1) store.reorderRows(rowIndex, rowIndex + 1);
764
+ }}
765
+ isFirst={rowIndex === 0}
766
+ isLast={rowIndex === store.rows.length - 1}
767
+ >
768
+ {isV2Section && v2Section ? (
769
+ <SectionV2Canvas
770
+ section={v2Section}
771
+ onAddBlockTarget={handleAddBlockTargetV2}
772
+ />
773
+ ) : isSection && section?.block?.[0] ? (
774
+ <BlockLivePreview block={section.block[0]} viewport={store.activeViewport} />
775
+ ) : null}
776
+ </SortableRow>
777
+ );
778
+ })}
779
+ </SortableContext>
780
+
781
+ {/* Add section button — hidden in section editor mode */}
782
+ {!isInSectionEditor && (
783
+ <div className="relative" style={{ height: 0 }}>
784
+ <div className="absolute left-0 right-0 top-2" style={{ zIndex: 4 }}>
785
+ <button
786
+ onClick={(e) => {
787
+ e.stopPropagation();
788
+ setShowSectionPicker(true);
789
+ }}
790
+ className="w-full rounded-xl py-3 text-xs font-medium text-white bg-[#93278f] hover:bg-[#7a1f76] transition-colors shadow-sm"
791
+ >
792
+ + Add Section
793
+ </button>
794
+ </div>
795
+ </div>
796
+ )}
797
+ </div>
798
+ )}
799
+ </ColumnDragProvider>
800
+ </DndWrapper>
801
+ </div>
802
+ </BuilderCanvas>
803
+ </ThumbStatusProvider>
804
+
805
+ {/* Section type picker modal */}
806
+ {showSectionPicker && (
807
+ <SectionTypePicker
808
+ onSelectEmptyV2={handleAddEmptySectionV2}
809
+ onSelectSection={handleAddSection}
810
+ onSelectParallaxGroup={handleAddParallaxGroup}
811
+ onSelectCustomSection={handleSelectCustomSection}
812
+ onCreateCustomSection={handleCreateCustomSection}
813
+ onClose={() => setShowSectionPicker(false)}
814
+ preloadedSections={cachedCustomSections}
815
+ />
816
+ )}
817
+
818
+ {/* Block type picker modal */}
819
+ {addBlockTarget && (
820
+ <BlockTypePicker
821
+ onSelect={handleAddBlock}
822
+ onClose={() => setAddBlockTarget(null)}
823
+ insertIndex={addBlockTarget.insertIndex}
824
+ />
825
+ )}
826
+
827
+ {/* Settings Panel */}
828
+ <SettingsPanel />
829
+ </div>
830
+
831
+ {/* ---- Help Modal ---- */}
832
+ {showHelp && <HelpModal onClose={() => setShowHelp(false)} />}
833
+
834
+ {/* ---- Block Context Menu ---- */}
835
+ {contextMenu && (
836
+ <BlockContextMenu
837
+ menu={contextMenu}
838
+ onClose={() => setContextMenu(null)}
839
+ onDuplicate={() => {
840
+ store.duplicateBlock(contextMenu.blockKey);
841
+ }}
842
+ onDelete={() =>
843
+ store.deleteBlock(contextMenu.blockKey)
844
+ }
845
+ />
846
+ )}
847
+ </div>
848
+ );
849
+ }