@morphika/webframe 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/admin/assets.ts +4 -0
- package/admin/database.ts +4 -0
- package/admin/index.ts +6 -0
- package/admin/login.ts +4 -0
- package/admin/navigation.ts +4 -0
- package/admin/pages-editor.ts +4 -0
- package/admin/pages.ts +4 -0
- package/admin/projects-editor.ts +4 -0
- package/admin/projects.ts +4 -0
- package/admin/settings.ts +4 -0
- package/admin/setup.ts +4 -0
- package/admin/storage.ts +4 -0
- package/admin/styles.ts +4 -0
- package/app/(site)/[slug]/loading.tsx +20 -0
- package/app/(site)/[slug]/page.tsx +83 -0
- package/app/(site)/error.tsx +32 -0
- package/app/(site)/layout.tsx +53 -0
- package/app/(site)/loading.tsx +20 -0
- package/app/(site)/not-found.tsx +41 -0
- package/app/(site)/page.tsx +43 -0
- package/app/(site)/preview/page.tsx +99 -0
- package/app/(site)/work/[slug]/loading.tsx +23 -0
- package/app/(site)/work/[slug]/page.tsx +84 -0
- package/app/admin/assets/page.tsx +573 -0
- package/app/admin/database/page.tsx +302 -0
- package/app/admin/error.tsx +53 -0
- package/app/admin/layout.tsx +273 -0
- package/app/admin/login/page.tsx +88 -0
- package/app/admin/navigation/page.tsx +157 -0
- package/app/admin/page.tsx +17 -0
- package/app/admin/pages/[slug]/page.tsx +849 -0
- package/app/admin/pages/page.tsx +588 -0
- package/app/admin/projects/[slug]/page.tsx +3 -0
- package/app/admin/projects/page.tsx +669 -0
- package/app/admin/settings/page.tsx +132 -0
- package/app/admin/setup/page.tsx +64 -0
- package/app/admin/storage/page.tsx +518 -0
- package/app/admin/styles/page.tsx +243 -0
- package/app/api/admin/assets/file/route.ts +81 -0
- package/app/api/admin/assets/health/route.ts +170 -0
- package/app/api/admin/assets/register/route.ts +163 -0
- package/app/api/admin/assets/registry/route.ts +98 -0
- package/app/api/admin/assets/relink/confirm/route.ts +242 -0
- package/app/api/admin/assets/relink/route.ts +202 -0
- package/app/api/admin/assets/scan/route.ts +271 -0
- package/app/api/admin/auth/route.ts +160 -0
- package/app/api/admin/custom-sections/[slug]/route.ts +159 -0
- package/app/api/admin/custom-sections/route.ts +127 -0
- package/app/api/admin/database/route.ts +53 -0
- package/app/api/admin/pages/[slug]/duplicate/route.ts +91 -0
- package/app/api/admin/pages/[slug]/route.ts +617 -0
- package/app/api/admin/pages/[slug]/set-home/route.ts +76 -0
- package/app/api/admin/pages/route.ts +129 -0
- package/app/api/admin/preview/route.ts +53 -0
- package/app/api/admin/r2/connect/route.ts +181 -0
- package/app/api/admin/r2/delete/route.ts +198 -0
- package/app/api/admin/r2/disconnect/route.ts +42 -0
- package/app/api/admin/r2/rename/route.ts +265 -0
- package/app/api/admin/r2/status/route.ts +106 -0
- package/app/api/admin/r2/upload-url/route.ts +148 -0
- package/app/api/admin/revalidate/route.ts +55 -0
- package/app/api/admin/settings/route.ts +279 -0
- package/app/api/admin/setup/complete/route.ts +51 -0
- package/app/api/admin/setup/route.ts +118 -0
- package/app/api/admin/storage/switch/route.ts +117 -0
- package/app/api/admin/styles/fonts/route.ts +97 -0
- package/app/api/admin/styles/route.ts +304 -0
- package/app/api/assets/[...path]/route.ts +98 -0
- package/app/api/custom-sections/[id]/route.ts +43 -0
- package/app/api/draft-mode/disable/route.ts +10 -0
- package/app/api/draft-mode/enable/route.ts +26 -0
- package/app/api/projects/route.ts +42 -0
- package/app/api/styles/route.ts +88 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +7 -0
- package/app/layout.tsx +53 -0
- package/app/robots.ts +17 -0
- package/app/sitemap.ts +48 -0
- package/app/studio/[[...index]]/page.tsx +8 -0
- package/components/admin/MetadataEditor.tsx +173 -0
- package/components/admin/PublishToggle.tsx +130 -0
- package/components/admin/icons.tsx +40 -0
- package/components/admin/nav-builder/NavBuilder.tsx +182 -0
- package/components/admin/nav-builder/NavBuilderGrid.tsx +326 -0
- package/components/admin/nav-builder/NavGeneralSettings.tsx +275 -0
- package/components/admin/nav-builder/NavGridCell.tsx +48 -0
- package/components/admin/nav-builder/NavGridItem.tsx +189 -0
- package/components/admin/nav-builder/NavItemSettings.tsx +288 -0
- package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -0
- package/components/admin/nav-builder/NavLivePreview.tsx +125 -0
- package/components/admin/nav-builder/NavSettingsFields.tsx +248 -0
- package/components/admin/nav-builder/NavSettingsPanel.tsx +127 -0
- package/components/admin/nav-builder/index.ts +10 -0
- package/components/admin/nav-builder/nav-builder-utils.ts +238 -0
- package/components/admin/setup-wizard/BrandingStep.tsx +218 -0
- package/components/admin/setup-wizard/DatabaseStep.tsx +331 -0
- package/components/admin/setup-wizard/DoneStep.tsx +187 -0
- package/components/admin/setup-wizard/SetupWizard.tsx +166 -0
- package/components/admin/setup-wizard/StorageStep.tsx +308 -0
- package/components/admin/setup-wizard/WelcomeStep.tsx +96 -0
- package/components/admin/setup-wizard/index.ts +9 -0
- package/components/admin/styles/ColorsEditor.tsx +214 -0
- package/components/admin/styles/FontsEditor.tsx +258 -0
- package/components/admin/styles/GridLayoutEditor.tsx +292 -0
- package/components/admin/styles/LinksButtonsEditor.tsx +120 -0
- package/components/admin/styles/TypographyEditor.tsx +266 -0
- package/components/admin/styles/index.ts +9 -0
- package/components/admin/styles/shared.tsx +68 -0
- package/components/blocks/BlockRenderer.tsx +404 -0
- package/components/blocks/ButtonBlockRenderer.tsx +52 -0
- package/components/blocks/CoverBlockRenderer.tsx +239 -0
- package/components/blocks/CustomSectionInstanceRenderer.tsx +82 -0
- package/components/blocks/EnterAnimationWrapper.tsx +140 -0
- package/components/blocks/HoverAnimationWrapper.tsx +308 -0
- package/components/blocks/ImageBlockRenderer.tsx +61 -0
- package/components/blocks/ImageGridBlockRenderer.tsx +545 -0
- package/components/blocks/PageBackground.tsx +28 -0
- package/components/blocks/PageNavAnimation.tsx +35 -0
- package/components/blocks/PageNavColor.tsx +24 -0
- package/components/blocks/PageRenderer.tsx +142 -0
- package/components/blocks/ParallaxGroupRenderer.tsx +448 -0
- package/components/blocks/ParallaxSlideRenderer.tsx +175 -0
- package/components/blocks/ProjectGridBlockRenderer.tsx +556 -0
- package/components/blocks/SectionRenderer.tsx +170 -0
- package/components/blocks/SectionV2Renderer.tsx +330 -0
- package/components/blocks/ShaderCanvas.tsx +392 -0
- package/components/blocks/SpacerBlockRenderer.tsx +17 -0
- package/components/blocks/TextBlockRenderer.tsx +87 -0
- package/components/blocks/TypewriterRichText.tsx +464 -0
- package/components/blocks/TypewriterWrapper.tsx +149 -0
- package/components/blocks/VideoBlockRenderer.tsx +304 -0
- package/components/blocks/index.ts +2 -0
- package/components/builder/AssetBrowser.tsx +2 -0
- package/components/builder/BlockLivePreview.tsx +101 -0
- package/components/builder/BlockTypePicker.tsx +178 -0
- package/components/builder/BuilderCanvas.tsx +354 -0
- package/components/builder/CanvasMinimap.tsx +200 -0
- package/components/builder/CanvasToolbar.tsx +202 -0
- package/components/builder/ColorPicker.tsx +243 -0
- package/components/builder/ColorSwatchPicker.tsx +274 -0
- package/components/builder/ColumnDragContext.tsx +51 -0
- package/components/builder/ColumnDragOverlay.tsx +110 -0
- package/components/builder/CustomSectionInstanceCard.tsx +97 -0
- package/components/builder/DeviceFrame.tsx +123 -0
- package/components/builder/DndWrapper.tsx +337 -0
- package/components/builder/InsertionLines.tsx +186 -0
- package/components/builder/ParallaxGroupCanvas.tsx +228 -0
- package/components/builder/ParallaxSlideHeader.tsx +113 -0
- package/components/builder/ReadOnlyFrame.tsx +417 -0
- package/components/builder/SectionEditorBar.tsx +288 -0
- package/components/builder/SectionTypePicker.tsx +422 -0
- package/components/builder/SectionV2Canvas.tsx +297 -0
- package/components/builder/SectionV2Column.tsx +488 -0
- package/components/builder/SettingsPanel.tsx +911 -0
- package/components/builder/SortableBlock.tsx +230 -0
- package/components/builder/SortableRow.tsx +362 -0
- package/components/builder/VirtualAssetGrid.tsx +397 -0
- package/components/builder/asset-browser/AssetBrowser.tsx +178 -0
- package/components/builder/asset-browser/FileLightbox.tsx +116 -0
- package/components/builder/asset-browser/FolderTreeItem.tsx +55 -0
- package/components/builder/asset-browser/R2BrowserContent.tsx +436 -0
- package/components/builder/asset-browser/R2ContextMenu.tsx +98 -0
- package/components/builder/asset-browser/VideoThumbnail.tsx +63 -0
- package/components/builder/asset-browser/helpers.ts +88 -0
- package/components/builder/asset-browser/index.ts +1 -0
- package/components/builder/asset-browser/types.ts +49 -0
- package/components/builder/asset-browser/useAssetBrowser.ts +344 -0
- package/components/builder/asset-browser/useR2DragDrop.ts +116 -0
- package/components/builder/asset-browser/useR2Operations.ts +189 -0
- package/components/builder/blockStyles.tsx +295 -0
- package/components/builder/editors/ButtonBlockEditor.tsx +184 -0
- package/components/builder/editors/CoverBlockEditor.tsx +488 -0
- package/components/builder/editors/EnterAnimationPicker.tsx +297 -0
- package/components/builder/editors/HoverEffectPicker.tsx +209 -0
- package/components/builder/editors/ImageBlockEditor.tsx +206 -0
- package/components/builder/editors/ImageGridBlockEditor.tsx +386 -0
- package/components/builder/editors/ProjectGridEditor.tsx +648 -0
- package/components/builder/editors/SpacerBlockEditor.tsx +167 -0
- package/components/builder/editors/StaggerSettings.tsx +108 -0
- package/components/builder/editors/TextAlignmentIcons.tsx +39 -0
- package/components/builder/editors/TextBlockEditor.tsx +462 -0
- package/components/builder/editors/TextStylePicker.tsx +183 -0
- package/components/builder/editors/VideoBlockEditor.tsx +278 -0
- package/components/builder/editors/index.ts +10 -0
- package/components/builder/editors/shared.tsx +345 -0
- package/components/builder/hooks/useColumnDrag.ts +472 -0
- package/components/builder/hooks/useColumnResize.ts +221 -0
- package/components/builder/index.ts +12 -0
- package/components/builder/live-preview/LiveButtonPreview.tsx +38 -0
- package/components/builder/live-preview/LiveCoverPreview.tsx +146 -0
- package/components/builder/live-preview/LiveImageGridPreview.tsx +123 -0
- package/components/builder/live-preview/LiveImagePreview.tsx +107 -0
- package/components/builder/live-preview/LiveProjectGridPreview.tsx +1010 -0
- package/components/builder/live-preview/LiveSpacerPreview.tsx +9 -0
- package/components/builder/live-preview/LiveTextEditor.tsx +198 -0
- package/components/builder/live-preview/LiveVideoPreview.tsx +98 -0
- package/components/builder/live-preview/index.ts +10 -0
- package/components/builder/live-preview/shared.tsx +153 -0
- package/components/builder/settings-panel/BlockLayoutTab.tsx +532 -0
- package/components/builder/settings-panel/BlockSettings.tsx +94 -0
- package/components/builder/settings-panel/ColumnV2Settings.tsx +160 -0
- package/components/builder/settings-panel/LayoutTab.tsx +310 -0
- package/components/builder/settings-panel/PageSettings.tsx +200 -0
- package/components/builder/settings-panel/ParallaxGroupSettings.tsx +118 -0
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +178 -0
- package/components/builder/settings-panel/SectionV2AnimationTab.tsx +103 -0
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +312 -0
- package/components/builder/settings-panel/SectionV2Settings.tsx +323 -0
- package/components/builder/settings-panel/TRBLInputs.tsx +51 -0
- package/components/builder/settings-panel/index.ts +19 -0
- package/components/builder/settings-panel/responsive-helpers.ts +524 -0
- package/components/ui/CustomCursor.tsx +118 -0
- package/components/ui/NavContentLightbox.tsx +152 -0
- package/components/ui/Navbar.tsx +582 -0
- package/components/ui/PortfolioTracker.tsx +87 -0
- package/components/ui/ScrollToTop.tsx +47 -0
- package/lib/animation/enter-presets.ts +147 -0
- package/lib/animation/enter-resolve.ts +90 -0
- package/lib/animation/enter-types.ts +128 -0
- package/lib/animation/hover-effect-presets.ts +210 -0
- package/lib/animation/hover-effect-types.ts +126 -0
- package/lib/asset-retry.ts +111 -0
- package/lib/assets.ts +92 -0
- package/lib/audit.ts +35 -0
- package/lib/auth-token.ts +94 -0
- package/lib/auth.ts +13 -0
- package/lib/builder/cascade-helpers.ts +51 -0
- package/lib/builder/cascade.ts +533 -0
- package/lib/builder/constants.ts +103 -0
- package/lib/builder/defaults.ts +182 -0
- package/lib/builder/history.ts +48 -0
- package/lib/builder/index.ts +21 -0
- package/lib/builder/layout-styles.ts +344 -0
- package/lib/builder/masonry.ts +166 -0
- package/lib/builder/responsive.ts +156 -0
- package/lib/builder/serializer.ts +845 -0
- package/lib/builder/store-blocks.ts +193 -0
- package/lib/builder/store-canvas.ts +319 -0
- package/lib/builder/store-helpers.ts +490 -0
- package/lib/builder/store-sections.ts +709 -0
- package/lib/builder/store.ts +333 -0
- package/lib/builder/templates.ts +297 -0
- package/lib/builder/types.ts +374 -0
- package/lib/builder/utils.ts +37 -0
- package/lib/color-utils.ts +116 -0
- package/lib/config/index.ts +57 -0
- package/lib/config/types.ts +122 -0
- package/lib/contexts/AssetContext.tsx +79 -0
- package/lib/contexts/NavAnimationContext.tsx +44 -0
- package/lib/contexts/NavColorContext.tsx +38 -0
- package/lib/contexts/PageExitContext.tsx +194 -0
- package/lib/contexts/ThumbStatusContext.tsx +83 -0
- package/lib/csrf-client.ts +34 -0
- package/lib/csrf.ts +68 -0
- package/lib/format-utils.ts +24 -0
- package/lib/hooks/useViewport.ts +42 -0
- package/lib/logger.ts +81 -0
- package/lib/revalidate.ts +23 -0
- package/lib/sanitize.ts +91 -0
- package/lib/sanity/client.ts +8 -0
- package/lib/sanity/queries.ts +486 -0
- package/lib/sanity/types.ts +869 -0
- package/lib/sanity/writeClient.ts +24 -0
- package/lib/security.ts +402 -0
- package/lib/setup/detect.ts +156 -0
- package/lib/shader/glsl/index.ts +27 -0
- package/lib/shader/glsl/pixelate.ts +51 -0
- package/lib/shader/glsl/rgb-shift.ts +45 -0
- package/lib/shader/glsl/ripple.ts +46 -0
- package/lib/shader/glsl/vertex.ts +14 -0
- package/lib/storage/index.ts +211 -0
- package/lib/storage/r2-adapter.ts +286 -0
- package/lib/storage/types.ts +125 -0
- package/lib/styles/provider.tsx +267 -0
- package/lib/thumbnails/generate.ts +151 -0
- package/lib/utils.ts +6 -0
- package/package.json +212 -0
- package/sanity/compose.ts +65 -0
- package/sanity/sanity.config.ts +126 -0
- package/sanity/schemas/assetRegistry.ts +301 -0
- package/sanity/schemas/blocks/blockLayout.ts +90 -0
- package/sanity/schemas/blocks/buttonBlock.ts +82 -0
- package/sanity/schemas/blocks/coverBlock.ts +229 -0
- package/sanity/schemas/blocks/imageBlock.ts +58 -0
- package/sanity/schemas/blocks/imageGridBlock.ts +112 -0
- package/sanity/schemas/blocks/index.ts +9 -0
- package/sanity/schemas/blocks/projectGridBlock.ts +251 -0
- package/sanity/schemas/blocks/spacerBlock.ts +41 -0
- package/sanity/schemas/blocks/textBlock.ts +139 -0
- package/sanity/schemas/blocks/videoBlock.ts +80 -0
- package/sanity/schemas/customSection.ts +69 -0
- package/sanity/schemas/customSectionInstance.ts +163 -0
- package/sanity/schemas/index.ts +111 -0
- package/sanity/schemas/objects/enterAnimationConfig.ts +72 -0
- package/sanity/schemas/objects/hoverEffectConfig.ts +90 -0
- package/sanity/schemas/objects/parallaxGroup.ts +66 -0
- package/sanity/schemas/objects/parallaxSlide.ts +217 -0
- package/sanity/schemas/objects/typewriterConfig.ts +38 -0
- package/sanity/schemas/page.ts +162 -0
- package/sanity/schemas/pageSection.ts +157 -0
- package/sanity/schemas/pageSectionV2.ts +269 -0
- package/sanity/schemas/siteSettings.ts +256 -0
- package/sanity/schemas/siteStyles.ts +210 -0
- package/site/error.ts +4 -0
- package/site/index.ts +8 -0
- package/site/not-found.ts +4 -0
- package/site/page.ts +4 -0
- package/site/preview.ts +4 -0
- package/site/robots.ts +4 -0
- package/site/sitemap.ts +4 -0
- package/site/work.ts +4 -0
- package/studio/index.ts +4 -0
- package/styles/admin.css +85 -0
- package/styles/animations.css +237 -0
- package/styles/base.css +148 -0
- package/styles/globals.css +10 -0
- 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 “{instance.custom_section_title}”.
|
|
888
|
+
Future changes to the saved section won'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
|
+
|