@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.
- package/LICENSE +21 -0
- package/README.md +50 -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 +212 -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,417 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ReadOnlyFrame — renders the page content as a read-only mirror.
|
|
5
|
+
*
|
|
6
|
+
* Used for inactive device frames (Tablet, Phone) in the multi-device preview.
|
|
7
|
+
* Reads rows directly from the Zustand store so it stays in sync in real-time.
|
|
8
|
+
* No dnd-kit, no selection, no toolbars — pure visual rendering.
|
|
9
|
+
*
|
|
10
|
+
* Accepts a `viewport` prop so blocks resolve their responsive overrides
|
|
11
|
+
* for the correct device size.
|
|
12
|
+
*
|
|
13
|
+
* Performance: Each row is memoized individually so that editing a block in
|
|
14
|
+
* row N only re-renders that single ReadOnlyRow — not the entire frame.
|
|
15
|
+
*
|
|
16
|
+
* Session 76: Updated to handle first-class PageSections alongside Rows.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { memo, useMemo, useState, useEffect } from "react";
|
|
20
|
+
import { useBuilderStore } from "../../lib/builder/store";
|
|
21
|
+
import { DEFAULT_GRID_WIDTH } from "../../lib/builder/constants";
|
|
22
|
+
import type { DeviceViewport } from "../../lib/builder/types";
|
|
23
|
+
import type { ContentItem, PageSection, PageSectionV2, CustomSectionInstance, ParallaxGroup, ParallaxSlideV2 } from "../../lib/sanity/types";
|
|
24
|
+
import { isPageSection, isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../lib/sanity/types";
|
|
25
|
+
import { DEVICE_HEIGHTS } from "../../lib/builder/types";
|
|
26
|
+
import { getEffectiveColumnsV2, getSectionV2SettingValue, getRowSettingValue } from "./settings-panel/responsive-helpers";
|
|
27
|
+
import BlockLivePreview from "./BlockLivePreview";
|
|
28
|
+
import { getRowLayoutStyles, getColumnVerticalAlign } from "../../lib/builder/layout-styles";
|
|
29
|
+
|
|
30
|
+
// Shared list of layout keys that support responsive overrides (V1 + V2)
|
|
31
|
+
const OVERRIDABLE_KEYS = [
|
|
32
|
+
"spacing_top", "spacing_right", "spacing_bottom", "spacing_left",
|
|
33
|
+
"offset_top", "offset_right", "offset_bottom", "offset_left",
|
|
34
|
+
"background_color", "background_opacity",
|
|
35
|
+
"border_color", "border_width", "border_style", "border_sides", "border_radius",
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
// ============================================
|
|
39
|
+
// Memoized per-section renderer — renders PageSection block directly
|
|
40
|
+
// ============================================
|
|
41
|
+
|
|
42
|
+
interface ReadOnlySectionProps {
|
|
43
|
+
section: PageSection;
|
|
44
|
+
viewport: DeviceViewport;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const ReadOnlySection = memo(function ReadOnlySection({ section, viewport }: ReadOnlySectionProps) {
|
|
48
|
+
const block = section.block?.[0];
|
|
49
|
+
if (!block) return null;
|
|
50
|
+
|
|
51
|
+
const base = section.settings || {};
|
|
52
|
+
// Merge responsive overrides for V1 PageSection (same approach as V2)
|
|
53
|
+
const resolvedSettings = useMemo(() => {
|
|
54
|
+
const merged: Record<string, unknown> = { ...base };
|
|
55
|
+
for (const key of OVERRIDABLE_KEYS) {
|
|
56
|
+
merged[key] = getRowSettingValue(section, viewport, key, (base as Record<string, unknown>)[key]);
|
|
57
|
+
}
|
|
58
|
+
return merged;
|
|
59
|
+
}, [section, viewport, base]);
|
|
60
|
+
const layoutStyles = getRowLayoutStyles(resolvedSettings as Record<string, unknown>);
|
|
61
|
+
|
|
62
|
+
const sectionStyle: React.CSSProperties = {
|
|
63
|
+
...layoutStyles,
|
|
64
|
+
contentVisibility: "auto",
|
|
65
|
+
containIntrinsicSize: "auto 200px",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div style={sectionStyle}>
|
|
70
|
+
<BlockLivePreview block={block} viewport={viewport} />
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ============================================
|
|
76
|
+
// Memoized per-section V2 renderer — renders PageSectionV2 grid
|
|
77
|
+
// BUG-V2-005 fix: V2 sections were not rendered in inactive device frames.
|
|
78
|
+
// ============================================
|
|
79
|
+
|
|
80
|
+
interface ReadOnlySectionV2Props {
|
|
81
|
+
section: PageSectionV2;
|
|
82
|
+
viewport: DeviceViewport;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const ReadOnlySectionV2 = memo(function ReadOnlySectionV2({ section, viewport }: ReadOnlySectionV2Props) {
|
|
86
|
+
const base = section.settings || {} as PageSectionV2["settings"];
|
|
87
|
+
// Merge responsive overrides for layout properties at this viewport
|
|
88
|
+
const resolvedSettings = useMemo(() => {
|
|
89
|
+
const merged: Record<string, unknown> = { ...base };
|
|
90
|
+
for (const key of OVERRIDABLE_KEYS) {
|
|
91
|
+
merged[key] = getSectionV2SettingValue(section, viewport, key, (base as unknown as Record<string, unknown>)[key]);
|
|
92
|
+
}
|
|
93
|
+
return merged;
|
|
94
|
+
}, [section, viewport, base]);
|
|
95
|
+
const layoutStyles = getRowLayoutStyles(resolvedSettings as Record<string, unknown>);
|
|
96
|
+
|
|
97
|
+
const gridColumns = base.grid_columns || 12;
|
|
98
|
+
const colGap = getSectionV2SettingValue(section, viewport, "col_gap", 20);
|
|
99
|
+
const rowGap = getSectionV2SettingValue(section, viewport, "row_gap", 20);
|
|
100
|
+
const effectiveCols = getEffectiveColumnsV2(section, viewport);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div style={{ ...layoutStyles, contentVisibility: "auto", containIntrinsicSize: "auto 200px" }}>
|
|
104
|
+
<div
|
|
105
|
+
style={{
|
|
106
|
+
display: "grid",
|
|
107
|
+
gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
|
|
108
|
+
columnGap: `${colGap}px`,
|
|
109
|
+
rowGap: `${rowGap}px`,
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
{section.columns.map((col) => {
|
|
113
|
+
const eff = effectiveCols.find((ec) => ec._key === col._key);
|
|
114
|
+
const gridColumn = eff?.grid_column ?? col.grid_column;
|
|
115
|
+
const gridRow = eff?.grid_row ?? col.grid_row;
|
|
116
|
+
const span = eff?.span ?? col.span;
|
|
117
|
+
const colJustify = getColumnVerticalAlign(col.blocks || []);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div
|
|
121
|
+
key={col._key}
|
|
122
|
+
style={{
|
|
123
|
+
gridColumn: `${gridColumn} / span ${span}`,
|
|
124
|
+
gridRow,
|
|
125
|
+
display: "flex",
|
|
126
|
+
flexDirection: "column",
|
|
127
|
+
...(colJustify ? { justifyContent: colJustify } : {}),
|
|
128
|
+
height: "100%",
|
|
129
|
+
minWidth: 0,
|
|
130
|
+
overflow: "hidden",
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
{(col.blocks || []).map((block) => (
|
|
134
|
+
<div key={block._key} style={{ width: "100%", minWidth: 0 }}>
|
|
135
|
+
<BlockLivePreview block={block} viewport={viewport} />
|
|
136
|
+
</div>
|
|
137
|
+
))}
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
})}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ============================================
|
|
147
|
+
// Read-only parallax slide — renders a single slide's V2 grid content
|
|
148
|
+
// ============================================
|
|
149
|
+
|
|
150
|
+
interface ReadOnlyParallaxSlideProps {
|
|
151
|
+
slide: ParallaxSlideV2;
|
|
152
|
+
viewport: DeviceViewport;
|
|
153
|
+
deviceHeight: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const ReadOnlyParallaxSlide = memo(function ReadOnlyParallaxSlide({
|
|
157
|
+
slide,
|
|
158
|
+
viewport,
|
|
159
|
+
deviceHeight,
|
|
160
|
+
}: ReadOnlyParallaxSlideProps) {
|
|
161
|
+
const base = slide.section_settings || {} as ParallaxSlideV2["section_settings"];
|
|
162
|
+
const gridColumns = base.grid_columns || 12;
|
|
163
|
+
const colGap = base.col_gap ?? 20;
|
|
164
|
+
const rowGap = base.row_gap ?? 20;
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div
|
|
168
|
+
className="relative"
|
|
169
|
+
style={{
|
|
170
|
+
minHeight: deviceHeight,
|
|
171
|
+
overflow: "hidden",
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
{/* Background preview — image */}
|
|
175
|
+
{slide.background_type === "image" && slide.background_image && (
|
|
176
|
+
<div
|
|
177
|
+
className="absolute inset-0 bg-cover bg-center pointer-events-none"
|
|
178
|
+
style={{
|
|
179
|
+
backgroundImage: `url(/api/assets/${slide.background_image})`,
|
|
180
|
+
backgroundPosition: slide.background_position || "center center",
|
|
181
|
+
opacity: 0.15,
|
|
182
|
+
}}
|
|
183
|
+
/>
|
|
184
|
+
)}
|
|
185
|
+
{/* Background preview — video (poster frame via <video> element) */}
|
|
186
|
+
{slide.background_type === "video" && slide.background_video && (
|
|
187
|
+
<video
|
|
188
|
+
src={`/api/assets/${slide.background_video}`}
|
|
189
|
+
muted
|
|
190
|
+
playsInline
|
|
191
|
+
autoPlay
|
|
192
|
+
loop
|
|
193
|
+
className="absolute inset-0 w-full h-full object-cover pointer-events-none"
|
|
194
|
+
style={{ opacity: 0.15 }}
|
|
195
|
+
/>
|
|
196
|
+
)}
|
|
197
|
+
{/* Overlay — only when a background is set */}
|
|
198
|
+
{(slide.background_overlay_opacity ?? 0) > 0 && (slide.background_image || slide.background_video) && (
|
|
199
|
+
<div
|
|
200
|
+
className="absolute inset-0 pointer-events-none"
|
|
201
|
+
style={{
|
|
202
|
+
backgroundColor: slide.background_overlay_color || "#000000",
|
|
203
|
+
opacity: (slide.background_overlay_opacity ?? 0) / 100 * 0.3,
|
|
204
|
+
}}
|
|
205
|
+
/>
|
|
206
|
+
)}
|
|
207
|
+
{/* V2 grid content — stretches to fill slide height (matches ParallaxGroupCanvas) */}
|
|
208
|
+
<div
|
|
209
|
+
className="relative"
|
|
210
|
+
style={{
|
|
211
|
+
minHeight: deviceHeight,
|
|
212
|
+
display: "flex",
|
|
213
|
+
flexDirection: "column",
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
<div
|
|
217
|
+
style={{
|
|
218
|
+
display: "grid",
|
|
219
|
+
gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
|
|
220
|
+
gridTemplateRows: "1fr",
|
|
221
|
+
columnGap: `${colGap}px`,
|
|
222
|
+
rowGap: `${rowGap}px`,
|
|
223
|
+
flex: 1,
|
|
224
|
+
minHeight: 0,
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
{slide.columns.map((col) => {
|
|
228
|
+
const colJustify = getColumnVerticalAlign(col.blocks || []);
|
|
229
|
+
return (
|
|
230
|
+
<div
|
|
231
|
+
key={col._key}
|
|
232
|
+
style={{
|
|
233
|
+
gridColumn: `${col.grid_column} / span ${col.span}`,
|
|
234
|
+
gridRow: col.grid_row,
|
|
235
|
+
display: "flex",
|
|
236
|
+
flexDirection: "column",
|
|
237
|
+
...(colJustify ? { justifyContent: colJustify } : {}),
|
|
238
|
+
height: "100%",
|
|
239
|
+
minWidth: 0,
|
|
240
|
+
overflow: "hidden",
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
{(col.blocks || []).map((block) => (
|
|
244
|
+
<div key={block._key} style={{ width: "100%", minWidth: 0 }}>
|
|
245
|
+
<BlockLivePreview block={block} viewport={viewport} />
|
|
246
|
+
</div>
|
|
247
|
+
))}
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
})}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// ============================================
|
|
258
|
+
// Read-only parallax group — renders all slides stacked
|
|
259
|
+
// ============================================
|
|
260
|
+
|
|
261
|
+
interface ReadOnlyParallaxGroupProps {
|
|
262
|
+
group: ParallaxGroup;
|
|
263
|
+
viewport: DeviceViewport;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const ReadOnlyParallaxGroup = memo(function ReadOnlyParallaxGroup({
|
|
267
|
+
group,
|
|
268
|
+
viewport,
|
|
269
|
+
}: ReadOnlyParallaxGroupProps) {
|
|
270
|
+
const deviceHeight = DEVICE_HEIGHTS[viewport];
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<div
|
|
274
|
+
style={{
|
|
275
|
+
borderRadius: 8,
|
|
276
|
+
border: "1px solid rgba(139, 92, 246, 0.15)",
|
|
277
|
+
overflow: "hidden",
|
|
278
|
+
}}
|
|
279
|
+
>
|
|
280
|
+
{/* Compact header */}
|
|
281
|
+
<div
|
|
282
|
+
style={{
|
|
283
|
+
background: "linear-gradient(135deg, #f3f0ff 0%, #ede5ff 100%)",
|
|
284
|
+
padding: "4px 8px",
|
|
285
|
+
borderBottom: "1px solid rgba(139, 92, 246, 0.1)",
|
|
286
|
+
}}
|
|
287
|
+
>
|
|
288
|
+
<span style={{ fontSize: 8, fontWeight: 600, color: "#8b5cf6" }}>
|
|
289
|
+
▽ Parallax · {group.slides.length} slides
|
|
290
|
+
</span>
|
|
291
|
+
</div>
|
|
292
|
+
{/* Slides */}
|
|
293
|
+
{group.slides.map((slide, i) => (
|
|
294
|
+
<div
|
|
295
|
+
key={slide._key}
|
|
296
|
+
style={{
|
|
297
|
+
borderTop: i > 0 ? "1px solid rgba(139, 92, 246, 0.08)" : undefined,
|
|
298
|
+
}}
|
|
299
|
+
>
|
|
300
|
+
<ReadOnlyParallaxSlide
|
|
301
|
+
slide={slide}
|
|
302
|
+
viewport={viewport}
|
|
303
|
+
deviceHeight={deviceHeight}
|
|
304
|
+
/>
|
|
305
|
+
</div>
|
|
306
|
+
))}
|
|
307
|
+
</div>
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// ============================================
|
|
312
|
+
// Read-only custom section instance — fetches and renders linked section data
|
|
313
|
+
// ============================================
|
|
314
|
+
|
|
315
|
+
interface ReadOnlyCustomSectionProps {
|
|
316
|
+
instance: CustomSectionInstance;
|
|
317
|
+
viewport: DeviceViewport;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const ReadOnlyCustomSection = memo(function ReadOnlyCustomSection({
|
|
321
|
+
instance,
|
|
322
|
+
viewport,
|
|
323
|
+
}: ReadOnlyCustomSectionProps) {
|
|
324
|
+
const cacheSettings = useBuilderStore((s) => s.cacheCustomSectionSettings);
|
|
325
|
+
const [sectionData, setSectionData] = useState<PageSectionV2 | null>(null);
|
|
326
|
+
|
|
327
|
+
useEffect(() => {
|
|
328
|
+
let cancelled = false;
|
|
329
|
+
fetch(`/api/custom-sections/${instance.custom_section_id}`)
|
|
330
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
331
|
+
.then((data) => {
|
|
332
|
+
if (!cancelled && data?.section) {
|
|
333
|
+
setSectionData(data.section);
|
|
334
|
+
// Cache base settings so SortableRow can merge with per-instance overrides
|
|
335
|
+
if (data.section.settings) {
|
|
336
|
+
cacheSettings(instance.custom_section_id, data.section.settings);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
.catch(() => {});
|
|
341
|
+
return () => { cancelled = true; };
|
|
342
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
343
|
+
}, [instance.custom_section_id]);
|
|
344
|
+
|
|
345
|
+
if (!sectionData) {
|
|
346
|
+
return (
|
|
347
|
+
<div style={{ padding: "16px 0", textAlign: "center" }}>
|
|
348
|
+
<span style={{ fontSize: 9, color: "#bbb" }}>Loading section...</span>
|
|
349
|
+
</div>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Merge per-instance overrides on top of base section settings
|
|
354
|
+
const mergedSection = instance.settings_overrides
|
|
355
|
+
? { ...sectionData, settings: { ...sectionData.settings, ...instance.settings_overrides } }
|
|
356
|
+
: sectionData;
|
|
357
|
+
|
|
358
|
+
// Also merge responsive overrides for correct viewport-specific rendering
|
|
359
|
+
const withResponsive = instance.responsive_overrides
|
|
360
|
+
? { ...mergedSection, responsive: { ...mergedSection.responsive, ...instance.responsive_overrides } }
|
|
361
|
+
: mergedSection;
|
|
362
|
+
|
|
363
|
+
return <ReadOnlySectionV2 section={withResponsive} viewport={viewport} />;
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// ============================================
|
|
367
|
+
// ReadOnlyFrame — orchestrator
|
|
368
|
+
// ============================================
|
|
369
|
+
|
|
370
|
+
interface ReadOnlyFrameProps {
|
|
371
|
+
/** The device viewport this frame represents */
|
|
372
|
+
viewport: DeviceViewport;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export default function ReadOnlyFrame({ viewport }: ReadOnlyFrameProps) {
|
|
376
|
+
const rows = useBuilderStore((s) => s.rows);
|
|
377
|
+
|
|
378
|
+
if (rows.length === 0) {
|
|
379
|
+
return (
|
|
380
|
+
<div className="flex items-center justify-center min-h-[400px]">
|
|
381
|
+
<p className="text-[10px] text-neutral-300">No content</p>
|
|
382
|
+
</div>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return (
|
|
387
|
+
<div>
|
|
388
|
+
{rows.map((item) =>
|
|
389
|
+
isParallaxGroup(item) ? (
|
|
390
|
+
<ReadOnlyParallaxGroup
|
|
391
|
+
key={item._key}
|
|
392
|
+
group={item as ParallaxGroup}
|
|
393
|
+
viewport={viewport}
|
|
394
|
+
/>
|
|
395
|
+
) : isCustomSectionInstance(item) ? (
|
|
396
|
+
<ReadOnlyCustomSection
|
|
397
|
+
key={item._key}
|
|
398
|
+
instance={item as CustomSectionInstance}
|
|
399
|
+
viewport={viewport}
|
|
400
|
+
/>
|
|
401
|
+
) : isPageSectionV2(item) ? (
|
|
402
|
+
<ReadOnlySectionV2
|
|
403
|
+
key={item._key}
|
|
404
|
+
section={item as PageSectionV2}
|
|
405
|
+
viewport={viewport}
|
|
406
|
+
/>
|
|
407
|
+
) : isPageSection(item) ? (
|
|
408
|
+
<ReadOnlySection
|
|
409
|
+
key={item._key}
|
|
410
|
+
section={item as PageSection}
|
|
411
|
+
viewport={viewport}
|
|
412
|
+
/>
|
|
413
|
+
) : null
|
|
414
|
+
)}
|
|
415
|
+
</div>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
4
|
+
import { useBuilderStore } from "../../lib/builder/store";
|
|
5
|
+
import { getCsrfToken } from "../../lib/csrf-client";
|
|
6
|
+
import { ADMIN_ACCENT } from "../../lib/builder/constants";
|
|
7
|
+
|
|
8
|
+
// ============================================
|
|
9
|
+
// SectionEditorBar — Top bar for custom section editor mode
|
|
10
|
+
// ============================================
|
|
11
|
+
// Shows when editorMode === "customSection". Displays breadcrumbs,
|
|
12
|
+
// an editable section name, Cancel, Save & Exit, and Delete buttons.
|
|
13
|
+
// All text in English. Consistent with builder admin accent (ADMIN_ACCENT).
|
|
14
|
+
|
|
15
|
+
interface SectionEditorBarProps {
|
|
16
|
+
/** Called after save completes — receives the created/updated section info */
|
|
17
|
+
onSaveComplete?: (result: { id: string; slug: string; title: string }) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function SectionEditorBar({ onSaveComplete }: SectionEditorBarProps) {
|
|
21
|
+
const customSectionSlug = useBuilderStore((s) => s.customSectionSlug);
|
|
22
|
+
const customSectionTitle = useBuilderStore((s) => s.customSectionTitle);
|
|
23
|
+
const isSaving = useBuilderStore((s) => s.isSaving);
|
|
24
|
+
const saveError = useBuilderStore((s) => s.saveError);
|
|
25
|
+
const isDirty = useBuilderStore((s) => s.isDirty);
|
|
26
|
+
const exitSectionEditor = useBuilderStore((s) => s.exitSectionEditor);
|
|
27
|
+
const saveSectionEditor = useBuilderStore((s) => s.saveSectionEditor);
|
|
28
|
+
const pageTitle = useBuilderStore((s) => s.pageTitle);
|
|
29
|
+
const pageType = useBuilderStore((s) => s.pageType);
|
|
30
|
+
|
|
31
|
+
const isEditing = !!customSectionSlug;
|
|
32
|
+
const [name, setName] = useState(customSectionTitle || "");
|
|
33
|
+
const [showCancelDialog, setShowCancelDialog] = useState(false);
|
|
34
|
+
const [showSaveDialog, setShowSaveDialog] = useState(false);
|
|
35
|
+
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
36
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
37
|
+
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
38
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
39
|
+
// Track whether the name was changed from its original value
|
|
40
|
+
const nameChanged = name.trim() !== (customSectionTitle || "").trim();
|
|
41
|
+
|
|
42
|
+
// Focus input on mount if creating new
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!isEditing && inputRef.current) {
|
|
45
|
+
inputRef.current.focus();
|
|
46
|
+
}
|
|
47
|
+
}, [isEditing]);
|
|
48
|
+
|
|
49
|
+
const handleCancel = useCallback(() => {
|
|
50
|
+
// m2 fix: only show warning dialog if there are actual unsaved changes (Session 110)
|
|
51
|
+
if (isDirty || nameChanged) {
|
|
52
|
+
setShowCancelDialog(true);
|
|
53
|
+
} else {
|
|
54
|
+
// No changes — exit immediately
|
|
55
|
+
exitSectionEditor();
|
|
56
|
+
}
|
|
57
|
+
}, [isDirty, nameChanged, exitSectionEditor]);
|
|
58
|
+
|
|
59
|
+
const confirmCancel = useCallback(() => {
|
|
60
|
+
setShowCancelDialog(false);
|
|
61
|
+
exitSectionEditor();
|
|
62
|
+
}, [exitSectionEditor]);
|
|
63
|
+
|
|
64
|
+
const handleSave = useCallback(() => {
|
|
65
|
+
if (!name.trim()) {
|
|
66
|
+
inputRef.current?.focus();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (isEditing) {
|
|
70
|
+
// Show warning dialog when editing existing section
|
|
71
|
+
setShowSaveDialog(true);
|
|
72
|
+
} else {
|
|
73
|
+
// Creating new — save directly
|
|
74
|
+
performSave();
|
|
75
|
+
}
|
|
76
|
+
}, [name, isEditing]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
77
|
+
|
|
78
|
+
const performSave = useCallback(async () => {
|
|
79
|
+
setShowSaveDialog(false);
|
|
80
|
+
const result = await saveSectionEditor(name.trim());
|
|
81
|
+
if (result && onSaveComplete) {
|
|
82
|
+
onSaveComplete(result);
|
|
83
|
+
}
|
|
84
|
+
}, [name, saveSectionEditor, onSaveComplete]);
|
|
85
|
+
|
|
86
|
+
const handleDelete = useCallback(async () => {
|
|
87
|
+
if (!customSectionSlug) return;
|
|
88
|
+
setIsDeleting(true);
|
|
89
|
+
setDeleteError(null);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const csrfToken = getCsrfToken();
|
|
93
|
+
const res = await fetch(`/api/admin/custom-sections/${customSectionSlug}`, {
|
|
94
|
+
method: "DELETE",
|
|
95
|
+
headers: {
|
|
96
|
+
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
const data = await res.json().catch(() => ({ error: "Delete failed" }));
|
|
102
|
+
throw new Error(data.error || `Delete failed (${res.status})`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setShowDeleteDialog(false);
|
|
106
|
+
// Exit editor without marking page dirty
|
|
107
|
+
exitSectionEditor();
|
|
108
|
+
} catch (err) {
|
|
109
|
+
setDeleteError(err instanceof Error ? err.message : "Delete failed");
|
|
110
|
+
} finally {
|
|
111
|
+
setIsDeleting(false);
|
|
112
|
+
}
|
|
113
|
+
}, [customSectionSlug, exitSectionEditor]);
|
|
114
|
+
|
|
115
|
+
// Breadcrumb label for parent context
|
|
116
|
+
const parentLabel = pageType === "project" ? "Projects" : "Pages";
|
|
117
|
+
const parentListUrl = pageType === "project" ? "/admin/projects" : "/admin/pages";
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<>
|
|
121
|
+
<div
|
|
122
|
+
className="flex items-center justify-between px-4 py-2 border-b border-[#2a2a2a]"
|
|
123
|
+
style={{ backgroundColor: "#1a1a1a" }}
|
|
124
|
+
>
|
|
125
|
+
{/* Left: breadcrumbs + section name input */}
|
|
126
|
+
<div className="flex items-center gap-3">
|
|
127
|
+
{/* Breadcrumbs */}
|
|
128
|
+
<div className="flex items-center gap-1.5 text-xs mr-1">
|
|
129
|
+
<a
|
|
130
|
+
href={parentListUrl}
|
|
131
|
+
className="text-[#666] hover:text-[#aaa] transition-colors cursor-pointer"
|
|
132
|
+
>
|
|
133
|
+
{parentLabel}
|
|
134
|
+
</a>
|
|
135
|
+
<span className="text-[#444]">/</span>
|
|
136
|
+
<button
|
|
137
|
+
onClick={handleCancel}
|
|
138
|
+
className="text-[#666] hover:text-[#aaa] transition-colors cursor-pointer truncate max-w-[140px]"
|
|
139
|
+
title={`Back to ${pageTitle || "page"}`}
|
|
140
|
+
>
|
|
141
|
+
{pageTitle || "Page"}
|
|
142
|
+
</button>
|
|
143
|
+
<span className="text-[#444]">/</span>
|
|
144
|
+
<span className="text-[#999] font-medium">
|
|
145
|
+
{isEditing ? "Edit Section" : "New Section"}
|
|
146
|
+
</span>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div className="w-px h-4 bg-[#3a3a3a]" />
|
|
150
|
+
|
|
151
|
+
<input
|
|
152
|
+
ref={inputRef}
|
|
153
|
+
type="text"
|
|
154
|
+
value={name}
|
|
155
|
+
onChange={(e) => setName(e.target.value)}
|
|
156
|
+
placeholder="Section name..."
|
|
157
|
+
className="bg-[#2a2a2a] text-white text-sm px-3 py-1.5 rounded border border-[#3a3a3a] focus:border-[#076bff] focus:outline-none w-64 transition-colors"
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{/* Right: actions */}
|
|
162
|
+
<div className="flex items-center gap-2">
|
|
163
|
+
{saveError && (
|
|
164
|
+
<span className="text-xs text-red-400 mr-2">{saveError}</span>
|
|
165
|
+
)}
|
|
166
|
+
{/* Delete button — only for existing sections */}
|
|
167
|
+
{isEditing && (
|
|
168
|
+
<button
|
|
169
|
+
onClick={() => setShowDeleteDialog(true)}
|
|
170
|
+
disabled={isSaving}
|
|
171
|
+
className="px-3 py-1.5 text-sm text-red-400 hover:text-red-300 rounded border border-[#3a3a3a] hover:border-red-500/50 transition-colors disabled:opacity-50"
|
|
172
|
+
>
|
|
173
|
+
Delete
|
|
174
|
+
</button>
|
|
175
|
+
)}
|
|
176
|
+
<button
|
|
177
|
+
onClick={handleCancel}
|
|
178
|
+
disabled={isSaving}
|
|
179
|
+
className="px-3 py-1.5 text-sm text-[#aaa] hover:text-white rounded border border-[#3a3a3a] hover:border-[#555] transition-colors disabled:opacity-50"
|
|
180
|
+
>
|
|
181
|
+
Cancel
|
|
182
|
+
</button>
|
|
183
|
+
<button
|
|
184
|
+
onClick={handleSave}
|
|
185
|
+
disabled={isSaving || !name.trim()}
|
|
186
|
+
className="px-4 py-1.5 text-sm text-white rounded font-medium transition-colors disabled:opacity-50"
|
|
187
|
+
style={{
|
|
188
|
+
backgroundColor: isSaving ? "#555" : ADMIN_ACCENT,
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
191
|
+
{isSaving ? "Saving..." : "Save & Exit"}
|
|
192
|
+
</button>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{/* Cancel confirmation dialog */}
|
|
197
|
+
{showCancelDialog && (
|
|
198
|
+
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60">
|
|
199
|
+
<div className="bg-[#1a1a1a] rounded-lg border border-[#3a3a3a] p-6 max-w-sm shadow-xl">
|
|
200
|
+
<h3 className="text-white text-sm font-medium mb-2">
|
|
201
|
+
Unsaved changes
|
|
202
|
+
</h3>
|
|
203
|
+
<p className="text-[#888] text-xs mb-4">
|
|
204
|
+
If you leave now, your changes will be lost.
|
|
205
|
+
</p>
|
|
206
|
+
<div className="flex justify-end gap-2">
|
|
207
|
+
<button
|
|
208
|
+
onClick={() => setShowCancelDialog(false)}
|
|
209
|
+
className="px-3 py-1.5 text-sm text-[#aaa] hover:text-white rounded border border-[#3a3a3a] hover:border-[#555] transition-colors"
|
|
210
|
+
>
|
|
211
|
+
Keep editing
|
|
212
|
+
</button>
|
|
213
|
+
<button
|
|
214
|
+
onClick={confirmCancel}
|
|
215
|
+
className="px-3 py-1.5 text-sm text-white bg-red-600 hover:bg-red-500 rounded transition-colors"
|
|
216
|
+
>
|
|
217
|
+
Discard & exit
|
|
218
|
+
</button>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{/* Save warning dialog (editing existing) */}
|
|
225
|
+
{showSaveDialog && (
|
|
226
|
+
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60">
|
|
227
|
+
<div className="bg-[#1a1a1a] rounded-lg border border-[#3a3a3a] p-6 max-w-sm shadow-xl">
|
|
228
|
+
<h3 className="text-white text-sm font-medium mb-2">
|
|
229
|
+
Save changes
|
|
230
|
+
</h3>
|
|
231
|
+
<p className="text-[#888] text-xs mb-4">
|
|
232
|
+
Changes will apply to all pages using this section.
|
|
233
|
+
</p>
|
|
234
|
+
<div className="flex justify-end gap-2">
|
|
235
|
+
<button
|
|
236
|
+
onClick={() => setShowSaveDialog(false)}
|
|
237
|
+
className="px-3 py-1.5 text-sm text-[#aaa] hover:text-white rounded border border-[#3a3a3a] hover:border-[#555] transition-colors"
|
|
238
|
+
>
|
|
239
|
+
Cancel
|
|
240
|
+
</button>
|
|
241
|
+
<button
|
|
242
|
+
onClick={performSave}
|
|
243
|
+
className="px-3 py-1.5 text-sm text-white rounded font-medium transition-colors"
|
|
244
|
+
style={{ backgroundColor: ADMIN_ACCENT }}
|
|
245
|
+
>
|
|
246
|
+
Save
|
|
247
|
+
</button>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
|
|
253
|
+
{/* Delete confirmation dialog */}
|
|
254
|
+
{showDeleteDialog && (
|
|
255
|
+
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60">
|
|
256
|
+
<div className="bg-[#1a1a1a] rounded-lg border border-[#3a3a3a] p-6 max-w-sm shadow-xl">
|
|
257
|
+
<h3 className="text-white text-sm font-medium mb-2">
|
|
258
|
+
Delete section
|
|
259
|
+
</h3>
|
|
260
|
+
<p className="text-[#888] text-xs mb-4">
|
|
261
|
+
This will permanently delete “{customSectionTitle || name}”.
|
|
262
|
+
If any pages reference this section, deletion will be blocked.
|
|
263
|
+
</p>
|
|
264
|
+
{deleteError && (
|
|
265
|
+
<p className="text-xs text-red-400 mb-3">{deleteError}</p>
|
|
266
|
+
)}
|
|
267
|
+
<div className="flex justify-end gap-2">
|
|
268
|
+
<button
|
|
269
|
+
onClick={() => { setShowDeleteDialog(false); setDeleteError(null); }}
|
|
270
|
+
disabled={isDeleting}
|
|
271
|
+
className="px-3 py-1.5 text-sm text-[#aaa] hover:text-white rounded border border-[#3a3a3a] hover:border-[#555] transition-colors disabled:opacity-50"
|
|
272
|
+
>
|
|
273
|
+
Cancel
|
|
274
|
+
</button>
|
|
275
|
+
<button
|
|
276
|
+
onClick={handleDelete}
|
|
277
|
+
disabled={isDeleting}
|
|
278
|
+
className="px-3 py-1.5 text-sm text-white bg-red-600 hover:bg-red-500 rounded transition-colors disabled:opacity-50"
|
|
279
|
+
>
|
|
280
|
+
{isDeleting ? "Deleting..." : "Delete"}
|
|
281
|
+
</button>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
</>
|
|
287
|
+
);
|
|
288
|
+
}
|