@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,911 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SettingsPanel — Right panel shell with header, tabs, and content routing.
5
+ *
6
+ * Sub-components extracted in Session 64:
7
+ * settings-panel/LayoutTab.tsx — LayoutTab, BlockLayoutTab, TRBLInputs, RowLayoutPresetPicker
8
+ * settings-panel/PageSettings.tsx — Page general, appearance, SEO
9
+ * settings-panel/RowSettings.tsx — Row layout, appearance, responsive
10
+ * settings-panel/ColumnSettings.tsx — Column width, alignment, gap
11
+ * settings-panel/BlockSettings.tsx — Block type editor router
12
+ * settings-panel/responsive-helpers.ts — Viewport-aware setting resolution
13
+ */
14
+
15
+ import { useState, useEffect, useRef, useCallback } from "react";
16
+ import { useBuilderStore } from "../../lib/builder/store";
17
+ import { ALL_BLOCK_INFO } from "../../lib/builder/types";
18
+ import { BUILDER_VIOLET } from "../../lib/builder/constants";
19
+ import { BLOCK_GRADIENTS, BLOCK_ICON_COMPONENTS } from "./blockStyles";
20
+ import type { ContentBlock, ContentItem, PageSection, PageSectionV2, CustomSectionInstance, ParallaxGroup, ParallaxSlideV2, SectionColumn, ProjectGridBlock, CardEntranceConfig } from "../../lib/sanity/types";
21
+ import { isPageSection, isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../lib/sanity/types";
22
+
23
+ import type { HoverEffectConfig } from "../../lib/animation/hover-effect-types";
24
+ import EnterAnimationPicker from "./editors/EnterAnimationPicker";
25
+ import HoverEffectPicker from "./editors/HoverEffectPicker";
26
+ import {
27
+ getBlockAnimationValue,
28
+ hasBlockAnimationOverride,
29
+ setBlockAnimationOverride,
30
+ } from "./settings-panel/responsive-helpers";
31
+
32
+ /** Safely extract hover_effect (new unified type) from any content block.
33
+ * ProjectGridBlock has a legacy `hover_effect: "3d" | "scale" | "none"` string field
34
+ * that collides — skip it (returns undefined). Other blocks have HoverEffectConfig. */
35
+ function getBlockHoverEffect(block: ContentBlock): HoverEffectConfig | undefined {
36
+ // ProjectGridBlock hover_effect is the old per-card string — not HoverEffectConfig
37
+ if (block._type === "projectGridBlock") return undefined;
38
+ const val = (block as unknown as Record<string, unknown>).hover_effect;
39
+ if (val === undefined || val === null) return undefined;
40
+ if (typeof val === "object") return val as HoverEffectConfig;
41
+ return undefined;
42
+ }
43
+ import {
44
+ LayoutTab,
45
+ BlockLayoutTab,
46
+ PageSettings,
47
+ PageSeoSettings,
48
+ BlockSettings,
49
+ SectionV2Settings,
50
+ SectionV2LayoutTab,
51
+ SectionV2AnimationTab,
52
+ ColumnV2Settings,
53
+ ParallaxSlideSettings,
54
+ ParallaxGroupSettings,
55
+ } from "./settings-panel";
56
+
57
+ type SettingsTab = "settings" | "layout" | "seo" | "animation";
58
+
59
+ export default function SettingsPanel() {
60
+ const store = useBuilderStore();
61
+ const [activeTab, setActiveTab] = useState<SettingsTab>("settings");
62
+
63
+ // Find selected elements — handle page sections, V2 sections, and parallax groups/slides
64
+ const selectedItem: ContentItem | undefined = store.rows.find((r) => r._key === store.selectedRowKey);
65
+ const selectedSection: PageSection | null = selectedItem && isPageSection(selectedItem) ? selectedItem : null;
66
+ const selectedSectionV2: PageSectionV2 | null = selectedItem && isPageSectionV2(selectedItem) ? selectedItem : null;
67
+ const selectedCustomSectionInstance: CustomSectionInstance | null = selectedItem && isCustomSectionInstance(selectedItem) ? selectedItem as CustomSectionInstance : null;
68
+
69
+ // Parallax detection: group selected directly, or slide selected (search inside groups)
70
+ const selectedParallaxGroup: ParallaxGroup | null = selectedItem && isParallaxGroup(selectedItem) ? selectedItem as ParallaxGroup : null;
71
+ const selectedParallaxSlide: { group: ParallaxGroup; slide: ParallaxSlideV2; virtualSection: PageSectionV2 } | null = (() => {
72
+ if (!store.selectedRowKey) return null;
73
+ for (const item of store.rows) {
74
+ if (!isParallaxGroup(item)) continue;
75
+ const group = item as ParallaxGroup;
76
+ const slide = group.slides.find((s) => s._key === store.selectedRowKey);
77
+ if (slide) {
78
+ // Create a virtual PageSectionV2 for the slide so we can delegate to SectionV2Settings etc.
79
+ const virtualSection: PageSectionV2 = {
80
+ _type: "pageSectionV2",
81
+ _key: slide._key,
82
+ section_type: "empty-v2",
83
+ columns: slide.columns,
84
+ settings: slide.section_settings,
85
+ };
86
+ return { group, slide, virtualSection };
87
+ }
88
+ }
89
+ return null;
90
+ })();
91
+
92
+ // V2 column: when a V2 section (or parallax slide) is selected and a column key is set
93
+ const effectiveSectionV2 = selectedSectionV2 || selectedParallaxSlide?.virtualSection || null;
94
+ const selectedColumnV2: SectionColumn | null = effectiveSectionV2 && store.selectedColumnKey
95
+ ? effectiveSectionV2.columns.find((c) => c._key === store.selectedColumnKey) || null
96
+ : null;
97
+
98
+ // For PageSections, the "block" is section.block[0] — selected automatically
99
+ const selectedBlock = (() => {
100
+ // If a PageSection is selected, its block is the section block
101
+ if (selectedSection) {
102
+ const block = selectedSection.block[0];
103
+ if (block) return { block, rowKey: selectedSection._key, colKey: "", isSection: true };
104
+ return null;
105
+ }
106
+ // Regular block search inside rows, V2 sections, and parallax slides
107
+ if (!store.selectedBlockKey) return null;
108
+ for (const item of store.rows) {
109
+ // V2 sections: search inside columns
110
+ if (isPageSectionV2(item)) {
111
+ for (const col of (item as PageSectionV2).columns || []) {
112
+ const block = (col.blocks || []).find(
113
+ (b) => b._key === store.selectedBlockKey
114
+ );
115
+ if (block) return { block, rowKey: item._key, colKey: col._key, isSection: false };
116
+ }
117
+ }
118
+ // Parallax groups: search inside slide columns
119
+ if (isParallaxGroup(item)) {
120
+ const group = item as ParallaxGroup;
121
+ for (const slide of group.slides) {
122
+ for (const col of slide.columns || []) {
123
+ const block = (col.blocks || []).find(
124
+ (b) => b._key === store.selectedBlockKey
125
+ );
126
+ if (block) return { block, rowKey: slide._key, colKey: col._key, isSection: false };
127
+ }
128
+ }
129
+ }
130
+ }
131
+ return null;
132
+ })();
133
+
134
+ // Derive the panel title + icon from what's selected
135
+ const blockInfo = selectedBlock
136
+ ? ALL_BLOCK_INFO.find((b) => b.type === selectedBlock.block._type)
137
+ : null;
138
+
139
+ // BUG-V2-003 fix: Block selection takes priority over V2 column/section
140
+ const panelTitle = selectedBlock
141
+ ? blockInfo?.label || selectedBlock.block._type
142
+ : selectedColumnV2
143
+ ? "Column"
144
+ : selectedParallaxSlide
145
+ ? `Slide ${selectedParallaxSlide.group.slides.findIndex((s) => s._key === selectedParallaxSlide.slide._key) + 1}`
146
+ : selectedParallaxGroup
147
+ ? "Parallax Showcase"
148
+ : selectedCustomSectionInstance
149
+ ? (selectedCustomSectionInstance.custom_section_title || "Saved Section")
150
+ : selectedSectionV2
151
+ ? "Section"
152
+ : selectedSection
153
+ ? (selectedSection.section_type === "projectGrid" ? "Project Grid" : "Parallax Section")
154
+ : "Page";
155
+
156
+ // Resolve gradient + icon component for the header
157
+ const headerStyleKey = selectedBlock
158
+ ? selectedBlock.block._type
159
+ : selectedColumnV2
160
+ ? "column"
161
+ : (selectedParallaxSlide || selectedParallaxGroup)
162
+ ? "parallaxGroup"
163
+ : selectedCustomSectionInstance
164
+ ? "customSectionInstance"
165
+ : selectedSectionV2
166
+ ? "row"
167
+ : selectedSection
168
+ ? (selectedSection.block[0]?._type || "row")
169
+ : "page";
170
+ const headerGradient = BLOCK_GRADIENTS[headerStyleKey] || BLOCK_GRADIENTS.page;
171
+ const HeaderIconComponent = BLOCK_ICON_COMPONENTS[headerStyleKey];
172
+
173
+ const hasSelection = !!(store.selectedRowKey || store.selectedColumnKey || store.selectedBlockKey);
174
+ // V2 columns: show Settings + Animation tabs (not Layout) — but NOT when a block inside the column is selected
175
+ const isColumnOnly = !!(selectedColumnV2 && !selectedBlock);
176
+ // Parallax group header: show Settings + Animation (no Layout)
177
+ const isParallaxGroupOnly = !!(selectedParallaxGroup && !selectedParallaxSlide && !selectedBlock);
178
+ // Custom section instance: show all 3 tabs (Settings with Edit/Detach, Layout, Animation)
179
+ const isCustomSectionOnly = !!(selectedCustomSectionInstance && !selectedBlock);
180
+ // Page level: nothing selected — show Settings + SEO + Animation (no Layout)
181
+ const isPageLevel = !hasSelection;
182
+
183
+ // Reset to "settings" tab when selection changes
184
+ const selectionKey = `${store.selectedRowKey}-${store.selectedColumnKey}-${store.selectedBlockKey}`;
185
+ const prevSelectionKey = useRef(selectionKey);
186
+ useEffect(() => {
187
+ if (prevSelectionKey.current !== selectionKey) {
188
+ setActiveTab("settings");
189
+ prevSelectionKey.current = selectionKey;
190
+ }
191
+ }, [selectionKey]);
192
+
193
+ // Columns have Settings + Animation — fall back if Layout or SEO tab was active
194
+ // Parallax group header has only Settings — fall back if Layout/SEO or Animation tab was active
195
+ // Page level has Settings + SEO + Animation — fall back if Layout tab was active
196
+ useEffect(() => {
197
+ if (isColumnOnly && (activeTab === "layout" || activeTab === "seo")) {
198
+ setActiveTab("settings");
199
+ }
200
+ if (isParallaxGroupOnly && (activeTab === "layout" || activeTab === "seo" || activeTab === "animation")) {
201
+ setActiveTab("settings");
202
+ }
203
+ if (isPageLevel && activeTab === "layout") {
204
+ setActiveTab("settings");
205
+ }
206
+ }, [isColumnOnly, isParallaxGroupOnly, isPageLevel, activeTab]);
207
+
208
+ return (
209
+ <div className="w-72 border-l border-[#f0f0f0] bg-white overflow-y-auto shrink-0 flex flex-col">
210
+ {/* Panel header — gradient + icon, matching add-block card style */}
211
+ <div
212
+ className="relative flex items-center px-3.5 py-3 shrink-0 overflow-hidden"
213
+ style={{ background: headerGradient }}
214
+ >
215
+ {/* Glass overlay */}
216
+ <div
217
+ className="absolute inset-0 pointer-events-none"
218
+ style={{
219
+ background: "linear-gradient(135deg, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0.05) 100%)",
220
+ }}
221
+ />
222
+
223
+ {/* Icon container — frosted glass */}
224
+ <div
225
+ className="relative shrink-0 flex items-center justify-center"
226
+ style={{
227
+ width: 36,
228
+ height: 36,
229
+ borderRadius: 10,
230
+ background: "rgba(255,255,255,0.4)",
231
+ backdropFilter: "blur(8px)",
232
+ boxShadow: "0 2px 8px rgba(0,0,0,0.06), inset 0 1px 0 rgba(255,255,255,0.5)",
233
+ }}
234
+ >
235
+ {HeaderIconComponent ? <HeaderIconComponent size={22} /> : null}
236
+ </div>
237
+
238
+ {/* Title */}
239
+ <h3
240
+ className="relative z-10 ml-2.5 text-[13px] font-semibold truncate"
241
+ style={{ color: "rgba(0,0,0,0.72)", textShadow: "0 1px 0 rgba(255,255,255,0.3)" }}
242
+ >
243
+ {panelTitle}
244
+ </h3>
245
+
246
+ {/* Action button — single delete for the active selection (block > column > section priority) */}
247
+ <div className="relative z-10 flex items-center gap-0.5 ml-auto">
248
+ {(() => {
249
+ // Determine the single delete action based on selection priority: block > column > section
250
+ let onDelete: (() => void) | null = null;
251
+ let deleteTitle = "";
252
+
253
+ if (selectedBlock && !selectedBlock.isSection) {
254
+ onDelete = () => store.deleteBlock(selectedBlock.block._key);
255
+ deleteTitle = "Delete";
256
+ } else if (selectedColumnV2 && effectiveSectionV2) {
257
+ onDelete = () => store.deleteColumnV2(effectiveSectionV2._key, selectedColumnV2._key);
258
+ deleteTitle = "Delete Column";
259
+ } else if (selectedParallaxGroup && !selectedParallaxSlide) {
260
+ onDelete = () => store.deleteSection(selectedParallaxGroup._key);
261
+ deleteTitle = "Delete Parallax Group";
262
+ } else if (selectedSectionV2) {
263
+ onDelete = () => store.deleteSection(selectedSectionV2._key);
264
+ deleteTitle = "Delete Section";
265
+ } else if (selectedSection) {
266
+ onDelete = () => store.deleteSection(selectedSection._key);
267
+ deleteTitle = "Delete Section";
268
+ }
269
+
270
+ if (!onDelete) return null;
271
+
272
+ return (
273
+ <button
274
+ onClick={onDelete}
275
+ className="p-1.5 rounded-md hover:bg-red-500/20 transition-colors group"
276
+ title={deleteTitle}
277
+ >
278
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="text-black/40 group-hover:text-[var(--admin-error)] transition-colors">
279
+ <polyline points="3 6 5 6 21 6" />
280
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
281
+ </svg>
282
+ </button>
283
+ );
284
+ })()}
285
+ </div>
286
+ </div>
287
+
288
+ {/* Segmented tab toggle — always 3 tabs (Settings, Layout, Animation) */}
289
+ {(() => {
290
+ const tabs: { id: SettingsTab; label: string; shortLabel: string; icon: React.ReactNode }[] = [
291
+ {
292
+ id: "settings",
293
+ label: "Settings",
294
+ shortLabel: "Settings",
295
+ icon: (
296
+ <svg width="13" height="13" viewBox="0 0 48 48" fill="none" className="shrink-0">
297
+ <path d="M42.5,35.5h-5.1c-0.8-3-3.1-5.1-5.9-5.2l-0.2,0c-2.9,0-5.3,2.1-6.1,5.2H5.7l-0.2,0v0c-0.3,0-0.6,0.2-0.9,0.4c-0.3,0.3-0.4,0.7-0.4,1.1c0,0.8,0.7,1.5,1.5,1.5h19.5c0.8,3.1,3.2,5.2,6.1,5.2c2.9,0,5.3-2.1,6.1-5.2h5.1c0.8,0,1.5-0.7,1.5-1.5c0-0.4-0.2-0.8-0.4-1.1C43.3,35.7,42.9,35.5,42.5,35.5z M35,37c0,2-1.6,3.7-3.7,3.7s-3.7-1.6-3.7-3.7s1.6-3.7,3.7-3.7S35,35,35,37z" fill="currentColor"/>
298
+ <path d="M5.7,11.8h19.6c0.8,3.1,3.2,5.2,6.1,5.2c2.9,0,5.3-2.1,6.1-5.2h5.1c0.4,0,0.8-0.2,1.1-0.4c0.3-0.3,0.4-0.7,0.4-1.1c0-0.8-0.7-1.5-1.5-1.5h-5.1c-0.8-3.1-3.2-5.2-6.1-5.2c-2.9,0-5.3,2.1-6.1,5.2H5.7c-0.8,0-1.5,0.7-1.5,1.5C4.2,11.2,4.9,11.8,5.7,11.8z M31.3,6.7c2,0,3.7,1.6,3.7,3.7S33.4,14,31.3,14s-3.7-1.6-3.7-3.7S29.3,6.7,31.3,6.7z" fill="currentColor"/>
299
+ <path d="M5.7,25.2h5.1c0.8,3.1,3.2,5.2,6.1,5.2c2.9,0,5.3-2.1,6.1-5.2l19.6,0c0.8,0,1.4-0.7,1.4-1.5c0-0.8-0.6-1.5-1.4-1.5H22.9c-0.8-3.1-3.2-5.2-6.1-5.2c-2.9,0-5.3,2.1-6.1,5.2H5.7c-0.8,0-1.5,0.7-1.5,1.5C4.2,24.5,4.9,25.2,5.7,25.2z M13.2,23.7c0-2,1.6-3.7,3.7-3.7s3.7,1.6,3.7,3.7s-1.6,3.7-3.7,3.7S13.2,25.7,13.2,23.7z" fill="currentColor"/>
300
+ </svg>
301
+ ),
302
+ },
303
+ {
304
+ id: "layout",
305
+ label: "Layout",
306
+ shortLabel: "Layout",
307
+ icon: (
308
+ <svg width="13" height="13" viewBox="0 0 48 48" fill="none" className="shrink-0">
309
+ <path d="M8.7,3.9c-2.8,0-5,2.3-5,5.1v13.6c0,2.8,2.3,5.1,5,5.1h8.4c2.8,0,5-2.3,5-5.1V9c0-2.8-2.3-5.1-5-5.1H8.7z M7.1,9c0-0.9,0.8-1.7,1.7-1.7h8.4c0.9,0,1.7,0.8,1.7,1.7v13.6c0,0.9-0.8,1.7-1.7,1.7H8.7c-0.9,0-1.7-0.8-1.7-1.7V9z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"/>
310
+ <path d="M30.6,3.9c-2.8,0-5,2.3-5,5.1v3.4c0,2.8,2.3,5.1,5,5.1H39c2.8,0,5-2.3,5-5.1V9c0-2.8-2.3-5.1-5-5.1H30.6z M28.9,9c0-0.9,0.8-1.7,1.7-1.7H39c0.9,0,1.7,0.8,1.7,1.7v3.4c0,0.9-0.8,1.7-1.7,1.7h-8.4c-0.9,0-1.7-0.8-1.7-1.7V9z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"/>
311
+ <path d="M3.7,36.2c0-2.8,2.3-5.1,5-5.1h8.4c2.8,0,5,2.3,5,5.1v3.4c0,2.8-2.3,5.1-5,5.1H8.7c-2.8,0-5-2.3-5-5.1V36.2z M8.7,34.5c-0.9,0-1.7,0.8-1.7,1.7v3.4c0,0.9,0.8,1.7,1.7,1.7h8.4c0.9,0,1.7-0.8,1.7-1.7v-3.4c0-0.9-0.8-1.7-1.7-1.7H8.7z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"/>
312
+ <path d="M30.6,20.9c-2.8,0-5,2.3-5,5.1v13.6c0,2.8,2.3,5.1,5,5.1H39c2.8,0,5-2.3,5-5.1V26c0-2.8-2.3-5.1-5-5.1H30.6z M28.9,26c0-0.9,0.8-1.7,1.7-1.7H39c0.9,0,1.7,0.8,1.7,1.7v13.6c0,0.9-0.8,1.7-1.7,1.7h-8.4c-0.9,0-1.7-0.8-1.7-1.7V26z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"/>
313
+ </svg>
314
+ ),
315
+ },
316
+ {
317
+ id: "animation",
318
+ label: "Animation",
319
+ shortLabel: "Anim",
320
+ icon: (
321
+ <svg width="13" height="13" viewBox="0 0 48 48" fill="none" className="shrink-0">
322
+ <path d="M23,8.6C27.5,4,34.8,4,39.4,8.6c4.5,4.5,4.5,11.9,0,16.4c-1.3,1.3-2.9,2.2-4.5,2.8l0,0.1 c-0.6,1.6-1.5,3.1-2.8,4.4c-1.3,1.3-2.9,2.3-4.5,2.8c0,0.1,0,0.2-0.1,0.3c-0.6,1.6-1.5,3-2.7,4.2c-4.5,4.5-11.9,4.5-16.4,0 c-4.5-4.5-4.5-11.9,0-16.4c1.3-1.3,2.8-2.2,4.5-2.8c0.6-1.6,1.5-3.2,2.8-4.5c1.3-1.3,2.8-2.2,4.5-2.8C20.7,11.4,21.7,9.9,23,8.6 L23,8.6z M24.8,10.4c-1.1,1-1.8,2.3-2.2,3.6c0,0.3-0.1,0.5-0.2,0.8c-0.7,2.9,0.1,6.1,2.4,8.4c3.5,3.5,9.2,3.5,12.8,0 c3.5-3.5,3.5-9.2,0-12.8C34,6.8,28.3,6.8,24.8,10.4L24.8,10.4z M17.5,17.7c-3.5,3.5-3.5,9.2,0,12.8c3.5,3.5,9.2,3.5,12.8,0 c0.6-0.6,1.2-1.3,1.6-2.1c-3.2,0.2-6.4-0.9-8.9-3.4c-2.4-2.4-3.6-5.7-3.4-8.8C18.8,16.5,18.1,17,17.5,17.7L17.5,17.7z M10.2,25 c-3.5,3.5-3.5,9.2,0,12.8c3.5,3.5,9.2,3.5,12.8,0c0.6-0.6,1.2-1.3,1.6-2.1c-3.2,0.2-6.4-0.9-8.9-3.4c-2.4-2.4-3.6-5.7-3.4-8.8 C11.5,23.8,10.8,24.3,10.2,25L10.2,25z" fill="currentColor"/>
323
+ </svg>
324
+ ),
325
+ },
326
+ ];
327
+
328
+ // Page level: replace Layout with SEO (Settings + SEO + Animation)
329
+ if (isPageLevel) {
330
+ const layoutIdx = tabs.findIndex((t) => t.id === "layout");
331
+ if (layoutIdx >= 0) {
332
+ tabs.splice(layoutIdx, 1, {
333
+ id: "seo" as SettingsTab,
334
+ label: "SEO",
335
+ shortLabel: "SEO",
336
+ icon: (
337
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="shrink-0">
338
+ <circle cx="11" cy="11" r="8" />
339
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
340
+ </svg>
341
+ ),
342
+ });
343
+ }
344
+ }
345
+ // Columns: remove Layout tab (keep Settings + Animation)
346
+ if (isColumnOnly) {
347
+ const layoutIdx = tabs.findIndex((t) => t.id === "layout");
348
+ if (layoutIdx >= 0) tabs.splice(layoutIdx, 1);
349
+ }
350
+ // Parallax group header: only Settings (no Layout, no Animation)
351
+ if (isParallaxGroupOnly) {
352
+ const layoutIdx = tabs.findIndex((t) => t.id === "layout");
353
+ if (layoutIdx >= 0) tabs.splice(layoutIdx, 1);
354
+ const animIdx = tabs.findIndex((t) => t.id === "animation");
355
+ if (animIdx >= 0) tabs.splice(animIdx, 1);
356
+ }
357
+ // Custom section editor mode: only Settings (Layout & Animation are per-instance on the parent page)
358
+ if (store.editorMode === "customSection") {
359
+ const layoutIdx = tabs.findIndex((t) => t.id === "layout");
360
+ if (layoutIdx >= 0) tabs.splice(layoutIdx, 1);
361
+ const animIdx = tabs.findIndex((t) => t.id === "animation");
362
+ if (animIdx >= 0) tabs.splice(animIdx, 1);
363
+ }
364
+
365
+ const tabCount = tabs.length;
366
+ const is4Tabs = false; // always 2 or 3 tabs now
367
+ const activeIndex = tabs.findIndex((t) => t.id === activeTab);
368
+ const pillIndex = activeIndex >= 0 ? activeIndex : 0;
369
+
370
+ return (
371
+ <div className="px-3 py-2 border-b border-[#f0f0f0] shrink-0 bg-[#fafafa]">
372
+ <div className="relative flex items-center bg-[#f0f0f0] rounded-lg p-[3px]">
373
+ {/* Animated sliding pill */}
374
+ <div
375
+ className="absolute top-[3px] bottom-[3px] rounded-md bg-white shadow-sm border border-[#e5e5e5] transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)]"
376
+ style={{
377
+ width: `calc((100% - 6px) / ${tabCount})`,
378
+ left: `calc(${pillIndex} * (100% - 6px) / ${tabCount} + 3px)`,
379
+ }}
380
+ />
381
+ {tabs.map((tab) => (
382
+ <button
383
+ key={tab.id}
384
+ onClick={() => setActiveTab(tab.id)}
385
+ className={`relative z-10 flex-1 flex items-center justify-center gap-0.5 py-1.5 rounded-md transition-colors duration-200 ${
386
+ is4Tabs ? "text-[10px]" : "text-[11px]"
387
+ } font-medium ${
388
+ activeTab === tab.id
389
+ ? "text-neutral-900"
390
+ : "text-neutral-400 hover:text-neutral-500"
391
+ }`}
392
+ >
393
+ {tab.icon}
394
+ {is4Tabs ? tab.shortLabel : tab.label}
395
+ </button>
396
+ ))}
397
+ </div>
398
+ </div>
399
+ );
400
+ })()}
401
+
402
+ {/* Panel body */}
403
+ <div className="flex-1 overflow-y-auto">
404
+ {/* ---- Parallax Group / Slide routing ---- */}
405
+ {/* Parallax group header selected (no specific slide) */}
406
+ {selectedParallaxGroup && !selectedParallaxSlide && !selectedBlock ? (
407
+ <ParallaxGroupSettings group={selectedParallaxGroup} />
408
+ ) : selectedParallaxSlide && selectedColumnV2 && !selectedBlock ? (
409
+ // Column inside a parallax slide — Settings or Animation tab
410
+ activeTab === "animation" ? (
411
+ <ColumnV2AnimationTab section={selectedParallaxSlide.virtualSection} column={selectedColumnV2} />
412
+ ) : (
413
+ <ColumnV2Settings section={selectedParallaxSlide.virtualSection} column={selectedColumnV2} />
414
+ )
415
+ ) : selectedParallaxSlide && !selectedBlock ? (
416
+ // Parallax slide selected — route by active tab
417
+ activeTab === "animation" ? (
418
+ <SectionV2AnimationTab section={selectedParallaxSlide.virtualSection} />
419
+ ) : activeTab === "layout" ? (
420
+ <SectionV2LayoutTab section={selectedParallaxSlide.virtualSection} />
421
+ ) : (
422
+ <ParallaxSlideSettings group={selectedParallaxSlide.group} slide={selectedParallaxSlide.slide} />
423
+ )
424
+ ) :
425
+ /* ---- Custom Section Instance routing ---- */
426
+ /* Virtual section lets SectionV2LayoutTab/AnimationTab write to instance overrides
427
+ via the store fallback in updateSectionV2Settings */
428
+ selectedCustomSectionInstance && !selectedBlock ? (
429
+ activeTab === "animation" ? (
430
+ <SectionV2AnimationTab section={{
431
+ _type: "pageSectionV2",
432
+ _key: selectedCustomSectionInstance._key,
433
+ section_type: "empty-v2",
434
+ columns: [],
435
+ settings: { preset: "full", grid_columns: 12, col_gap: 20, row_gap: 20, ...selectedCustomSectionInstance.settings_overrides },
436
+ responsive: selectedCustomSectionInstance.responsive_overrides,
437
+ }} />
438
+ ) : activeTab === "layout" ? (
439
+ <SectionV2LayoutTab section={{
440
+ _type: "pageSectionV2",
441
+ _key: selectedCustomSectionInstance._key,
442
+ section_type: "empty-v2",
443
+ columns: [],
444
+ settings: { preset: "full", grid_columns: 12, col_gap: 20, row_gap: 20, ...selectedCustomSectionInstance.settings_overrides },
445
+ responsive: selectedCustomSectionInstance.responsive_overrides,
446
+ }} />
447
+ ) : (
448
+ <CustomSectionSettings instance={selectedCustomSectionInstance} />
449
+ )
450
+ ) :
451
+ /* ---- V2 Section / Column / Block routing ---- */
452
+ /* BUG-V2-003 fix: When a block inside a V2 column is selected, show BlockSettings
453
+ instead of ColumnV2Settings. Block selection takes priority over column. */
454
+ selectedColumnV2 && selectedSectionV2 && !selectedBlock ? (
455
+ // V2 Column selected (no block) — Settings or Animation tab
456
+ activeTab === "animation" ? (
457
+ <ColumnV2AnimationTab section={selectedSectionV2} column={selectedColumnV2} />
458
+ ) : (
459
+ <ColumnV2Settings section={selectedSectionV2} column={selectedColumnV2} />
460
+ )
461
+ ) : selectedSectionV2 && !selectedBlock ? (
462
+ // V2 Section selected — route by active tab
463
+ activeTab === "animation" ? (
464
+ <SectionV2AnimationTab section={selectedSectionV2} />
465
+ ) : activeTab === "layout" ? (
466
+ <SectionV2LayoutTab section={selectedSectionV2} />
467
+ ) : (
468
+ <SectionV2Settings section={selectedSectionV2} />
469
+ )
470
+ ) : activeTab === "animation" ? (
471
+ <AnimationTab
472
+ selectedBlock={selectedBlock}
473
+ selectedSection={selectedSection}
474
+ />
475
+ ) : activeTab === "layout" ? (
476
+ (() => {
477
+ // PageSection: show section layout settings (spacing, background, border)
478
+ if (selectedSection) {
479
+ return <LayoutTab section={selectedSection} sectionKey={selectedSection._key} />;
480
+ }
481
+ if (selectedBlock && !selectedBlock.isSection) {
482
+ return <BlockLayoutTab block={selectedBlock.block} />;
483
+ }
484
+ return (
485
+ <div className="p-4">
486
+ <div className="rounded-lg bg-[#f5f5f5] p-4 text-center">
487
+ <p className="text-xs text-neutral-400">
488
+ Select an element to edit layout properties.
489
+ </p>
490
+ </div>
491
+ </div>
492
+ );
493
+ })()
494
+ ) : activeTab === "seo" ? (
495
+ <PageSeoSettings />
496
+ ) : selectedSection ? (
497
+ // PageSection selected → show the section block's settings directly
498
+ <BlockSettings
499
+ block={selectedSection.block[0]}
500
+ />
501
+ ) : selectedBlock ? (
502
+ <BlockSettings
503
+ block={selectedBlock.block}
504
+ />
505
+ ) : (
506
+ <PageSettings />
507
+ )}
508
+ </div>
509
+ </div>
510
+ );
511
+ }
512
+
513
+ // ============================================
514
+ // Animation Tab — extracted inline for clarity
515
+ // ============================================
516
+
517
+ function AnimationTab({
518
+ selectedBlock,
519
+ selectedSection,
520
+ }: {
521
+ selectedBlock: { block: ContentBlock; rowKey: string; colKey: string; isSection: boolean } | null;
522
+ selectedSection: PageSection | null;
523
+ }) {
524
+ const store = useBuilderStore();
525
+
526
+ // PageSection (V1): enter animation on the section settings level
527
+ if (selectedSection) {
528
+ return (
529
+ <EnterAnimationPicker
530
+ mode={{ level: "section", parentConfig: store.pageSettings.enter_animation }}
531
+ config={selectedSection.settings?.enter_animation}
532
+ onChange={(cfg) => {
533
+ store.updateSectionSettings(selectedSection._key, { enter_animation: cfg });
534
+ }}
535
+ />
536
+ );
537
+ }
538
+
539
+ // Block level: type-specific enter picker + card entrance for projectGrid
540
+ if (selectedBlock) {
541
+ const isProjectGrid = selectedBlock.block._type === "projectGridBlock";
542
+ const pgBlock = isProjectGrid ? (selectedBlock.block as ProjectGridBlock) : null;
543
+ const bvp = store.activeViewport;
544
+ const isBlockResponsive = bvp !== "desktop";
545
+
546
+ const effectiveEnterAnim = getBlockAnimationValue(
547
+ selectedBlock.block, bvp, "enter_animation", undefined
548
+ );
549
+ const effectiveHoverEffect = getBlockAnimationValue(
550
+ selectedBlock.block, bvp, "hover_effect", undefined
551
+ ) as HoverEffectConfig | undefined;
552
+
553
+ const hasEnterOverride = hasBlockAnimationOverride(selectedBlock.block, bvp, "enter_animation");
554
+ const hasHoverOverride = hasBlockAnimationOverride(selectedBlock.block, bvp, "hover_effect");
555
+
556
+ return (
557
+ <>
558
+ {isBlockResponsive && (
559
+ <div className="px-4 pt-3">
560
+ <div className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[#076bff]/8 border border-[#076bff]/15">
561
+ <span className="text-[11px] font-medium text-[#076bff]">
562
+ Editing {bvp === "tablet" ? "Tablet" : "Phone"} overrides
563
+ </span>
564
+ </div>
565
+ </div>
566
+ )}
567
+ <div className="relative">
568
+ {hasEnterOverride && (
569
+ <div className="flex items-center justify-between px-4 pt-2">
570
+ <span className="text-[9px] text-[#076bff] font-medium">overridden</span>
571
+ <button
572
+ onClick={() => {
573
+ const updates = setBlockAnimationOverride(selectedBlock.block, bvp, "enter_animation", undefined);
574
+ store.updateBlock(selectedBlock.block._key, updates);
575
+ }}
576
+ className="text-[10px] text-neutral-400 hover:text-[var(--admin-error)] transition-colors"
577
+ >
578
+ Reset
579
+ </button>
580
+ </div>
581
+ )}
582
+ <EnterAnimationPicker
583
+ mode={{ level: "block", blockType: selectedBlock.block._type }}
584
+ config={effectiveEnterAnim}
585
+ onChange={(cfg) => {
586
+ const updates = setBlockAnimationOverride(selectedBlock.block, bvp, "enter_animation", cfg);
587
+ store.updateBlock(selectedBlock.block._key, updates);
588
+ }}
589
+ />
590
+ </div>
591
+ {/* Hover Effect — block-level only, shown if block type has hover presets */}
592
+ <div className="border-t border-neutral-200 my-1" />
593
+ <div className="relative">
594
+ {hasHoverOverride && (
595
+ <div className="flex items-center justify-between px-4 pt-2">
596
+ <span className="text-[9px] text-[#076bff] font-medium">overridden</span>
597
+ <button
598
+ onClick={() => {
599
+ const updates = setBlockAnimationOverride(selectedBlock.block, bvp, "hover_effect", undefined);
600
+ store.updateBlock(selectedBlock.block._key, updates);
601
+ }}
602
+ className="text-[10px] text-neutral-400 hover:text-[var(--admin-error)] transition-colors"
603
+ >
604
+ Reset
605
+ </button>
606
+ </div>
607
+ )}
608
+ <HoverEffectPicker
609
+ blockType={selectedBlock.block._type}
610
+ config={effectiveHoverEffect ?? getBlockHoverEffect(selectedBlock.block)}
611
+ onChange={(cfg) => {
612
+ const updates = setBlockAnimationOverride(selectedBlock.block, bvp, "hover_effect", cfg);
613
+ store.updateBlock(selectedBlock.block._key, updates);
614
+ }}
615
+ />
616
+ </div>
617
+ {isProjectGrid && pgBlock && (
618
+ <>
619
+ <div className="border-t border-neutral-200 my-1" />
620
+ <CardEntranceSection block={pgBlock} />
621
+ </>
622
+ )}
623
+ </>
624
+ );
625
+ }
626
+
627
+ // Page-level: generic enter animation (no hover at page level)
628
+ return (
629
+ <EnterAnimationPicker
630
+ mode={{ level: "page" }}
631
+ config={store.pageSettings.enter_animation}
632
+ onChange={(cfg) => {
633
+ store.updatePageSettings({ enter_animation: cfg });
634
+ }}
635
+ />
636
+ );
637
+ }
638
+
639
+ // ============================================
640
+ // Column V2 Animation Tab — enter animation with inherit from section
641
+ // ============================================
642
+
643
+ function ColumnV2AnimationTab({
644
+ section,
645
+ column,
646
+ }: {
647
+ section: PageSectionV2;
648
+ column: SectionColumn;
649
+ }) {
650
+ const store = useBuilderStore();
651
+
652
+ return (
653
+ <EnterAnimationPicker
654
+ mode={{ level: "column", parentConfig: section.settings.enter_animation }}
655
+ config={column.enter_animation}
656
+ onChange={(cfg) => {
657
+ store.updateColumnEnterAnimation(section._key, column._key, cfg);
658
+ }}
659
+ />
660
+ );
661
+ }
662
+
663
+ // ============================================
664
+ // Card Entrance Section — for ProjectGridBlock Animation tab
665
+ // ============================================
666
+
667
+ const ENTRANCE_PRESETS = [
668
+ { value: "fade", label: "Fade" },
669
+ { value: "slide-up", label: "Slide Up" },
670
+ { value: "scale", label: "Scale" },
671
+ ] as const;
672
+
673
+ const CARD_ENTRANCE_SELECT_CLASS =
674
+ "w-full rounded-lg border border-transparent bg-[#f5f5f5] px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none transition-all hover:bg-[#efefef] focus:bg-white focus:border-[#076bff] focus:shadow-[0_0_0_3px_rgba(7,107,255,0.06)]";
675
+
676
+ const CARD_ENTRANCE_SLIDER_CLASS =
677
+ "w-full h-1.5 rounded-full bg-[#e5e5e5] appearance-none cursor-pointer accent-[#076bff] [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[#076bff] [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:shadow-sm";
678
+
679
+ function CardEntranceSection({ block }: { block: ProjectGridBlock }) {
680
+ const updateBlock = useBuilderStore((s) => s.updateBlock);
681
+ const entrance = block.card_entrance;
682
+ const enabled = entrance?.enabled ?? false;
683
+
684
+ const update = (updates: Partial<CardEntranceConfig>) => {
685
+ updateBlock(block._key, {
686
+ card_entrance: { ...entrance, ...updates },
687
+ } as Partial<ProjectGridBlock>);
688
+ };
689
+
690
+ return (
691
+ <div className="px-4 py-3">
692
+ <div className="flex items-center justify-between mb-2.5">
693
+ <span className="text-xs font-medium text-neutral-700">Card Entrance</span>
694
+ <button
695
+ type="button"
696
+ onClick={() => update({ enabled: !enabled })}
697
+ className={`relative w-8 h-[18px] rounded-full transition-colors ${
698
+ enabled ? "bg-[#076bff]" : "bg-neutral-300"
699
+ }`}
700
+ >
701
+ <span
702
+ className={`absolute top-[2px] w-[14px] h-[14px] rounded-full bg-white shadow transition-transform ${
703
+ enabled ? "translate-x-[16px]" : "translate-x-[2px]"
704
+ }`}
705
+ />
706
+ </button>
707
+ </div>
708
+
709
+ {enabled && (
710
+ <div className="space-y-3">
711
+ {/* Preset — dropdown instead of segmented buttons */}
712
+ <div>
713
+ <label className="text-[11px] text-neutral-500 mb-1 block">Preset</label>
714
+ <select
715
+ value={entrance?.preset || "slide-up"}
716
+ onChange={(e) => update({ preset: e.target.value as "fade" | "slide-up" | "scale" })}
717
+ className={CARD_ENTRANCE_SELECT_CLASS}
718
+ >
719
+ {ENTRANCE_PRESETS.map((opt) => (
720
+ <option key={opt.value} value={opt.value}>
721
+ {opt.label}
722
+ </option>
723
+ ))}
724
+ </select>
725
+ </div>
726
+
727
+ {/* Stagger delay */}
728
+ <div>
729
+ <div className="flex items-center justify-between mb-1">
730
+ <label className="text-[11px] text-neutral-500">Stagger</label>
731
+ <span className="text-[11px] text-neutral-500 tabular-nums">
732
+ {entrance?.stagger_delay ?? 80}ms
733
+ </span>
734
+ </div>
735
+ <input
736
+ type="range"
737
+ min={0}
738
+ max={5000}
739
+ step={10}
740
+ value={entrance?.stagger_delay ?? 80}
741
+ onChange={(e) => update({ stagger_delay: Number(e.target.value) })}
742
+ className={CARD_ENTRANCE_SLIDER_CLASS}
743
+ />
744
+ </div>
745
+
746
+ {/* Duration */}
747
+ <div>
748
+ <div className="flex items-center justify-between mb-1">
749
+ <label className="text-[11px] text-neutral-500">Duration</label>
750
+ <span className="text-[11px] text-neutral-500 tabular-nums">
751
+ {entrance?.duration ?? 500}ms
752
+ </span>
753
+ </div>
754
+ <input
755
+ type="range"
756
+ min={200}
757
+ max={5000}
758
+ step={50}
759
+ value={entrance?.duration ?? 500}
760
+ onChange={(e) => update({ duration: Number(e.target.value) })}
761
+ className={CARD_ENTRANCE_SLIDER_CLASS}
762
+ />
763
+ </div>
764
+ </div>
765
+ )}
766
+ </div>
767
+ );
768
+ }
769
+
770
+ // ============================================
771
+ // Custom Section Instance Settings — Edit & Detach actions
772
+ // ============================================
773
+
774
+ function CustomSectionSettings({ instance }: { instance: CustomSectionInstance }) {
775
+ const store = useBuilderStore();
776
+ const [showDetachConfirm, setShowDetachConfirm] = useState(false);
777
+ const [sectionData, setSectionData] = useState<PageSectionV2 | null>(null);
778
+ const [loadingEdit, setLoadingEdit] = useState(false);
779
+
780
+ // Fetch section data for detach
781
+ useEffect(() => {
782
+ let cancelled = false;
783
+ fetch(`/api/custom-sections/${instance.custom_section_id}`)
784
+ .then((res) => res.ok ? res.json() : null)
785
+ .then((data) => {
786
+ if (!cancelled && data?.section) setSectionData(data.section);
787
+ })
788
+ .catch(() => {});
789
+ return () => { cancelled = true; };
790
+ }, [instance.custom_section_id]);
791
+
792
+ const handleEdit = useCallback(async () => {
793
+ setLoadingEdit(true);
794
+ try {
795
+ const res = await fetch(`/api/admin/custom-sections/${instance.custom_section_slug}`);
796
+ if (!res.ok) throw new Error("Failed to load section");
797
+ const data = await res.json();
798
+
799
+ const remoteTitle = data.section.title;
800
+ if (remoteTitle && remoteTitle !== instance.custom_section_title) {
801
+ store.updateCustomSectionInstanceTitle(instance._key, remoteTitle);
802
+ }
803
+
804
+ store.enterSectionEditor(
805
+ instance.custom_section_slug,
806
+ remoteTitle,
807
+ data.section.section
808
+ );
809
+ } catch {
810
+ if (sectionData) {
811
+ store.enterSectionEditor(
812
+ instance.custom_section_slug,
813
+ instance.custom_section_title,
814
+ sectionData
815
+ );
816
+ }
817
+ } finally {
818
+ setLoadingEdit(false);
819
+ }
820
+ }, [instance, sectionData, store]);
821
+
822
+ const handleDetach = useCallback(() => {
823
+ if (!sectionData) return;
824
+ store.detachCustomSectionInstance(instance._key, sectionData);
825
+ setShowDetachConfirm(false);
826
+ }, [instance._key, sectionData, store]);
827
+
828
+ return (
829
+ <div className="px-4 py-3 space-y-3">
830
+ {/* Linked badge */}
831
+ <div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-[#f3f0ff] border border-[#8b5cf6]/20">
832
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#8b5cf6" strokeWidth="2" strokeLinecap="round" className="shrink-0">
833
+ <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
834
+ <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
835
+ </svg>
836
+ <span className="text-[11px] text-[#8b5cf6] font-medium truncate">
837
+ Linked Section
838
+ </span>
839
+ </div>
840
+
841
+ {/* Edit button */}
842
+ <button
843
+ onClick={handleEdit}
844
+ disabled={loadingEdit}
845
+ className="w-full flex items-center justify-center gap-2 px-3 py-2.5 rounded-lg text-xs font-medium text-white transition-colors disabled:opacity-50"
846
+ style={{ backgroundColor: BUILDER_VIOLET }}
847
+ >
848
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
849
+ <path d="M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
850
+ <path d="m15 5 4 4" />
851
+ </svg>
852
+ {loadingEdit ? "Loading..." : "Edit Section"}
853
+ </button>
854
+ <p className="text-[10px] text-neutral-400 -mt-1">
855
+ Changes apply to all pages using this section.
856
+ </p>
857
+
858
+ {/* Detach button */}
859
+ <button
860
+ onClick={() => setShowDetachConfirm(true)}
861
+ disabled={!sectionData}
862
+ className="w-full flex items-center justify-center gap-2 px-3 py-2.5 rounded-lg text-xs font-medium text-neutral-600 bg-[#f5f5f5] hover:bg-[#ebebeb] transition-colors disabled:opacity-30"
863
+ >
864
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
865
+ <path d="M18.84 12.25l1.72-1.71a5 5 0 0 0-7.07-7.07l-3 3a5 5 0 0 0 .54 7.54" />
866
+ <path d="M5.16 11.75l-1.72 1.71a5 5 0 0 0 7.07 7.07l3-3a5 5 0 0 0-.54-7.54" />
867
+ <line x1="2" y1="2" x2="22" y2="22" />
868
+ </svg>
869
+ Detach
870
+ </button>
871
+ <p className="text-[10px] text-neutral-400 -mt-1">
872
+ Convert to an independent inline section on this page.
873
+ </p>
874
+
875
+ {/* Detach confirmation dialog */}
876
+ {showDetachConfirm && (
877
+ <div
878
+ className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60"
879
+ onClick={(e) => { e.stopPropagation(); setShowDetachConfirm(false); }}
880
+ >
881
+ <div
882
+ className="bg-white rounded-lg border border-[#e5e5e5] p-6 max-w-sm shadow-xl"
883
+ onClick={(e) => e.stopPropagation()}
884
+ >
885
+ <h3 className="text-neutral-900 text-sm font-medium mb-2">Detach section?</h3>
886
+ <p className="text-neutral-500 text-xs mb-4">
887
+ This will create an independent copy of &ldquo;{instance.custom_section_title}&rdquo;.
888
+ Future changes to the saved section won&apos;t affect this page.
889
+ </p>
890
+ <div className="flex justify-end gap-2">
891
+ <button
892
+ onClick={() => setShowDetachConfirm(false)}
893
+ className="px-3 py-1.5 text-sm text-neutral-500 hover:text-neutral-900 rounded border border-[#e5e5e5] hover:border-[#ccc] transition-colors"
894
+ >
895
+ Cancel
896
+ </button>
897
+ <button
898
+ onClick={handleDetach}
899
+ className="px-3 py-1.5 text-sm text-white rounded font-medium transition-colors"
900
+ style={{ backgroundColor: BUILDER_VIOLET }}
901
+ >
902
+ Detach
903
+ </button>
904
+ </div>
905
+ </div>
906
+ </div>
907
+ )}
908
+ </div>
909
+ );
910
+ }
911
+