@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,488 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, type ReactNode } from "react";
|
|
4
|
+
import { useDroppable } from "@dnd-kit/core";
|
|
5
|
+
import {
|
|
6
|
+
SortableContext,
|
|
7
|
+
verticalListSortingStrategy,
|
|
8
|
+
} from "@dnd-kit/sortable";
|
|
9
|
+
import { useBuilderStore } from "../../lib/builder/store";
|
|
10
|
+
import { makeBlockId, makeColumnDroppableId } from "./DndWrapper";
|
|
11
|
+
import type { SectionColumn, ContentBlock, PageSectionV2 } from "../../lib/sanity/types";
|
|
12
|
+
import { getColumnVerticalAlign } from "../../lib/builder/layout-styles";
|
|
13
|
+
import { BUILDER_BLUE, BUILDER_GREEN } from "../../lib/builder/constants";
|
|
14
|
+
|
|
15
|
+
// ============================================
|
|
16
|
+
// SectionV2Column — Individual column in a V2 section grid
|
|
17
|
+
//
|
|
18
|
+
// Phase 3: Full handler implementation
|
|
19
|
+
// - Drag grip (top-left): @dnd-kit useDraggable for column-level drag
|
|
20
|
+
// - Resize handles (left + right): pill handles with scale-up hover
|
|
21
|
+
// - Delete button (top-right): red ✕ circle
|
|
22
|
+
// - Snapping guide feedback during resize
|
|
23
|
+
//
|
|
24
|
+
// Visual design (from PDF mockup):
|
|
25
|
+
// - Section hover: dashed outline at ~30% opacity, +Add Block at ~30%
|
|
26
|
+
// - Column hover: solid outline 100%, +Add Block 100%,
|
|
27
|
+
// 3 handlers appear (stretch left, stretch right, drag grip top-left),
|
|
28
|
+
// delete button (red ✕ top-right), span badge top-right
|
|
29
|
+
// - Handler hover: scale-up 1.15× animation
|
|
30
|
+
// ============================================
|
|
31
|
+
|
|
32
|
+
// ---- ResizeHandle ----
|
|
33
|
+
// Extracted from the duplicated left/right resize handle blocks.
|
|
34
|
+
// Encapsulates: hit area, 3-state pill visual (inactive/hover/active),
|
|
35
|
+
// mousedown, mouse enter/leave for hoveredEdge.
|
|
36
|
+
|
|
37
|
+
interface ResizeHandleProps {
|
|
38
|
+
edge: "left" | "right";
|
|
39
|
+
showChrome: boolean;
|
|
40
|
+
showFaintOutline: boolean;
|
|
41
|
+
resizingEdge: "left" | "right" | null;
|
|
42
|
+
hoveredEdge: "left" | "right" | null;
|
|
43
|
+
onHoverEdge: (edge: "left" | "right" | null) => void;
|
|
44
|
+
onResizeStart: (e: React.MouseEvent) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ResizeHandle({
|
|
48
|
+
edge,
|
|
49
|
+
showChrome,
|
|
50
|
+
showFaintOutline,
|
|
51
|
+
resizingEdge,
|
|
52
|
+
hoveredEdge,
|
|
53
|
+
onHoverEdge,
|
|
54
|
+
onResizeStart,
|
|
55
|
+
}: ResizeHandleProps) {
|
|
56
|
+
const isActive = resizingEdge === edge;
|
|
57
|
+
const isHoveredEdge = hoveredEdge === edge;
|
|
58
|
+
const isVisible = showChrome || isActive || showFaintOutline;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
role="separator"
|
|
63
|
+
aria-orientation="vertical"
|
|
64
|
+
aria-label={`Resize column ${edge} edge`}
|
|
65
|
+
className={`absolute top-0 ${edge === "left" ? "left-0" : "right-0"} w-5 h-full z-[4] cursor-col-resize flex items-center justify-center transition-opacity ${
|
|
66
|
+
isVisible ? "opacity-100" : "opacity-0 pointer-events-none"
|
|
67
|
+
}`}
|
|
68
|
+
style={{ transform: edge === "left" ? "translateX(-50%)" : "translateX(50%)" }}
|
|
69
|
+
onMouseEnter={() => onHoverEdge(edge)}
|
|
70
|
+
onMouseLeave={() => { if (!isActive) onHoverEdge(null); }}
|
|
71
|
+
onMouseDown={(e) => {
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
onResizeStart(e);
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<div
|
|
78
|
+
className="pointer-events-none shadow-sm"
|
|
79
|
+
style={{
|
|
80
|
+
// 3-state visual: ACTIVE → circle (16×16px), HOVER → wide pill (10×56px), INACTIVE → thin pill (4–6px × 32–56px).
|
|
81
|
+
// showChrome (selected/hovered column) gets a slightly wider and taller pill for better discoverability.
|
|
82
|
+
width: isActive ? 16 : isHoveredEdge ? 10 : showChrome ? 6 : 4,
|
|
83
|
+
height: isActive ? 16 : isHoveredEdge ? 56 : showChrome ? 56 : 32,
|
|
84
|
+
borderRadius: isActive ? "50%" : 999,
|
|
85
|
+
backgroundColor: isActive
|
|
86
|
+
? "rgba(7, 107, 255, 0.9)"
|
|
87
|
+
: isHoveredEdge
|
|
88
|
+
? "rgba(7, 107, 255, 0.7)"
|
|
89
|
+
: showChrome
|
|
90
|
+
? "rgba(7, 107, 255, 0.5)"
|
|
91
|
+
: "rgba(7, 107, 255, 0.2)",
|
|
92
|
+
transition: "width 150ms ease-out, height 150ms ease-out, border-radius 150ms ease-out, background-color 150ms, box-shadow 150ms",
|
|
93
|
+
boxShadow: isActive
|
|
94
|
+
? "0 0 10px rgba(7, 107, 255, 0.5)"
|
|
95
|
+
: isHoveredEdge
|
|
96
|
+
? "0 0 6px rgba(7, 107, 255, 0.2)"
|
|
97
|
+
: undefined,
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface SectionV2ColumnProps {
|
|
105
|
+
column: SectionColumn;
|
|
106
|
+
sectionKey: string;
|
|
107
|
+
section: PageSectionV2;
|
|
108
|
+
isSelected: boolean;
|
|
109
|
+
isSectionHovered: boolean;
|
|
110
|
+
isResizing?: boolean;
|
|
111
|
+
snappingInfo?: {
|
|
112
|
+
columnKey: string;
|
|
113
|
+
edge: "left" | "right";
|
|
114
|
+
willOverflow: boolean;
|
|
115
|
+
compressedNeighborKey?: string | null;
|
|
116
|
+
neighborAtMinimum?: boolean;
|
|
117
|
+
} | null;
|
|
118
|
+
/** Make-room animation direction when an insert target is between this and an adjacent column */
|
|
119
|
+
insertNudge?: "left" | "right" | null;
|
|
120
|
+
/** Green highlight when this column is the swap target */
|
|
121
|
+
isSwapTarget?: boolean;
|
|
122
|
+
/** Replaces old isDragging from useDraggable — ghost effect when this column is being dragged */
|
|
123
|
+
isDraggedColumn?: boolean;
|
|
124
|
+
/** Calls into useColumnDrag.startDrag */
|
|
125
|
+
onStartDrag?: (e: React.MouseEvent) => void;
|
|
126
|
+
/** Container width in pixels (for nudge offset computation) */
|
|
127
|
+
containerWidth?: number;
|
|
128
|
+
onSelect: () => void;
|
|
129
|
+
onDelete: () => void;
|
|
130
|
+
onAddBlock: (insertIndex?: number) => void;
|
|
131
|
+
onResizeRight: (columnKey: string, startX: number, startSpan: number, containerEl: HTMLElement) => void;
|
|
132
|
+
onResizeLeft: (columnKey: string, startX: number, startGridCol: number, startSpan: number, containerEl: HTMLElement) => void;
|
|
133
|
+
children: ReactNode;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export default function SectionV2Column({
|
|
137
|
+
column,
|
|
138
|
+
sectionKey,
|
|
139
|
+
section,
|
|
140
|
+
isSelected,
|
|
141
|
+
isSectionHovered,
|
|
142
|
+
isResizing = false,
|
|
143
|
+
snappingInfo,
|
|
144
|
+
insertNudge,
|
|
145
|
+
isSwapTarget = false,
|
|
146
|
+
isDraggedColumn = false,
|
|
147
|
+
onStartDrag,
|
|
148
|
+
containerWidth = 0,
|
|
149
|
+
onSelect,
|
|
150
|
+
onDelete,
|
|
151
|
+
onAddBlock,
|
|
152
|
+
onResizeRight,
|
|
153
|
+
onResizeLeft,
|
|
154
|
+
children,
|
|
155
|
+
}: SectionV2ColumnProps) {
|
|
156
|
+
const previewMode = useBuilderStore((s) => s.previewMode);
|
|
157
|
+
const canvasZoom = useBuilderStore((s) => s.canvasZoom);
|
|
158
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
159
|
+
const [resizingEdge, setResizingEdge] = useState<"left" | "right" | null>(null);
|
|
160
|
+
const [hoveredEdge, setHoveredEdge] = useState<"left" | "right" | null>(null);
|
|
161
|
+
|
|
162
|
+
const gridColumns = section.settings.grid_columns || 12;
|
|
163
|
+
|
|
164
|
+
// ---- Block drop target ----
|
|
165
|
+
const blockDropId = makeColumnDroppableId(sectionKey, column._key);
|
|
166
|
+
const { setNodeRef: setBlockDropRef, isOver: isBlockOver } = useDroppable({
|
|
167
|
+
id: blockDropId,
|
|
168
|
+
disabled: previewMode,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Block IDs for sortable context
|
|
172
|
+
const blockIds = (column.blocks || []).map((b: ContentBlock) =>
|
|
173
|
+
makeBlockId(sectionKey, column._key, b._key)
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// ---- Stable callbacks ----
|
|
177
|
+
const handleClick = useCallback((e: React.MouseEvent) => {
|
|
178
|
+
e.stopPropagation();
|
|
179
|
+
onSelect();
|
|
180
|
+
}, [onSelect]);
|
|
181
|
+
|
|
182
|
+
const handleMouseEnter = useCallback(() => setIsHovered(true), []);
|
|
183
|
+
const handleMouseLeave = useCallback(() => setIsHovered(false), []);
|
|
184
|
+
|
|
185
|
+
const handleDelete = useCallback((e: React.MouseEvent) => {
|
|
186
|
+
e.stopPropagation();
|
|
187
|
+
onDelete();
|
|
188
|
+
}, [onDelete]);
|
|
189
|
+
|
|
190
|
+
const handleAddBlockEmpty = useCallback((e: React.MouseEvent) => {
|
|
191
|
+
e.stopPropagation();
|
|
192
|
+
onAddBlock(0);
|
|
193
|
+
}, [onAddBlock]);
|
|
194
|
+
|
|
195
|
+
const handleAddBlockBelow = useCallback((e: React.MouseEvent) => {
|
|
196
|
+
e.stopPropagation();
|
|
197
|
+
onAddBlock((column.blocks || []).length);
|
|
198
|
+
}, [onAddBlock, column.blocks]);
|
|
199
|
+
|
|
200
|
+
const hasBlocks = (column.blocks || []).length > 0;
|
|
201
|
+
const showChrome = (isSelected || isHovered) && !isDraggedColumn;
|
|
202
|
+
// Show faint outlines when section is hovered but not this specific column
|
|
203
|
+
const showFaintOutline = isSectionHovered && !isHovered && !isSelected && !isDraggedColumn;
|
|
204
|
+
|
|
205
|
+
// Column-level vertical alignment from blocks' align_v settings
|
|
206
|
+
const colJustify = getColumnVerticalAlign(column.blocks || []);
|
|
207
|
+
|
|
208
|
+
// ---- Preview mode ----
|
|
209
|
+
if (previewMode) {
|
|
210
|
+
return (
|
|
211
|
+
<div
|
|
212
|
+
ref={setBlockDropRef}
|
|
213
|
+
style={{
|
|
214
|
+
gridColumn: `${column.grid_column} / span ${column.span}`,
|
|
215
|
+
gridRow: column.grid_row,
|
|
216
|
+
display: "flex",
|
|
217
|
+
flexDirection: "column",
|
|
218
|
+
...(colJustify ? { justifyContent: colJustify } : {}),
|
|
219
|
+
height: "100%",
|
|
220
|
+
minHeight: 0,
|
|
221
|
+
}}
|
|
222
|
+
>
|
|
223
|
+
<SortableContext items={blockIds} strategy={verticalListSortingStrategy}>
|
|
224
|
+
{children}
|
|
225
|
+
</SortableContext>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Compute nudge offset for make-room animation.
|
|
231
|
+
// The column slides horizontally by 25% of one grid unit (containerWidth / gridColumns / 4)
|
|
232
|
+
// to visually "make room" for an insertion line drop target between adjacent columns.
|
|
233
|
+
const nudgeOffset = insertNudge
|
|
234
|
+
? (containerWidth / gridColumns / 4) * (insertNudge === "left" ? -1 : 1)
|
|
235
|
+
: 0;
|
|
236
|
+
|
|
237
|
+
// ---- Design mode ----
|
|
238
|
+
return (
|
|
239
|
+
<div
|
|
240
|
+
className="relative group"
|
|
241
|
+
data-col-v2-droptarget=""
|
|
242
|
+
data-section-key={sectionKey}
|
|
243
|
+
data-column-key={column._key}
|
|
244
|
+
style={{
|
|
245
|
+
gridColumn: `${column.grid_column} / span ${column.span}`,
|
|
246
|
+
gridRow: column.grid_row,
|
|
247
|
+
display: "flex",
|
|
248
|
+
flexDirection: "column",
|
|
249
|
+
...(colJustify ? { justifyContent: colJustify } : {}),
|
|
250
|
+
height: "100%",
|
|
251
|
+
minHeight: 0,
|
|
252
|
+
opacity: isDraggedColumn ? 0.3 : 1,
|
|
253
|
+
transform: nudgeOffset ? `translateX(${nudgeOffset}px)` : undefined,
|
|
254
|
+
transition: isDraggedColumn
|
|
255
|
+
? "none"
|
|
256
|
+
: "opacity 150ms, box-shadow 150ms, transform 150ms ease-out",
|
|
257
|
+
}}
|
|
258
|
+
ref={setBlockDropRef}
|
|
259
|
+
onClick={handleClick}
|
|
260
|
+
onMouseEnter={handleMouseEnter}
|
|
261
|
+
onMouseLeave={handleMouseLeave}
|
|
262
|
+
>
|
|
263
|
+
{/* Column outline */}
|
|
264
|
+
<div
|
|
265
|
+
className="pointer-events-none absolute inset-0 z-[1] rounded"
|
|
266
|
+
style={{
|
|
267
|
+
transition: "box-shadow 150ms, border 150ms",
|
|
268
|
+
...(isSwapTarget
|
|
269
|
+
? { boxShadow: `inset 0 0 0 2px ${BUILDER_GREEN}`, background: "rgba(34, 197, 94, 0.08)" }
|
|
270
|
+
: isBlockOver
|
|
271
|
+
? { boxShadow: `inset 0 0 0 2px ${BUILDER_BLUE}` }
|
|
272
|
+
: isSelected
|
|
273
|
+
? { boxShadow: `inset 0 0 0 2px rgba(7, 107, 255, 0.6)` }
|
|
274
|
+
: isHovered
|
|
275
|
+
? { boxShadow: `inset 0 0 0 1.5px rgba(7, 107, 255, 0.5)` }
|
|
276
|
+
: showFaintOutline
|
|
277
|
+
? { border: `1px dashed rgba(7, 107, 255, 0.2)`, borderRadius: 4 }
|
|
278
|
+
: undefined),
|
|
279
|
+
}}
|
|
280
|
+
/>
|
|
281
|
+
|
|
282
|
+
{/* Snapping warning indicator — overflow on right edge */}
|
|
283
|
+
{snappingInfo?.willOverflow && (
|
|
284
|
+
<div
|
|
285
|
+
className="pointer-events-none absolute z-[8]"
|
|
286
|
+
style={{
|
|
287
|
+
top: 0,
|
|
288
|
+
bottom: 0,
|
|
289
|
+
...(snappingInfo.edge === "right"
|
|
290
|
+
? { right: -2, width: 3 }
|
|
291
|
+
: { left: -2, width: 3 }),
|
|
292
|
+
background: "#ff6b35",
|
|
293
|
+
borderRadius: 2,
|
|
294
|
+
animation: "pulse 0.8s ease-in-out infinite",
|
|
295
|
+
}}
|
|
296
|
+
/>
|
|
297
|
+
)}
|
|
298
|
+
|
|
299
|
+
{/* Neighbor-at-minimum indicator — pulsing orange border + badge when this
|
|
300
|
+
column has been compressed to span 1 during a left-edge resize */}
|
|
301
|
+
{snappingInfo?.compressedNeighborKey === column._key && snappingInfo?.neighborAtMinimum && (
|
|
302
|
+
<>
|
|
303
|
+
<div
|
|
304
|
+
className="pointer-events-none absolute inset-0 z-[8] rounded"
|
|
305
|
+
style={{
|
|
306
|
+
boxShadow: "inset 0 0 0 2px #ff6b35",
|
|
307
|
+
animation: "pulse 0.8s ease-in-out infinite",
|
|
308
|
+
}}
|
|
309
|
+
/>
|
|
310
|
+
<div
|
|
311
|
+
className="pointer-events-none absolute z-[9]"
|
|
312
|
+
style={{
|
|
313
|
+
bottom: -2,
|
|
314
|
+
left: "50%",
|
|
315
|
+
transform: `translateX(-50%) scale(${1 / canvasZoom})`,
|
|
316
|
+
transformOrigin: "bottom center",
|
|
317
|
+
background: "#ff6b35",
|
|
318
|
+
color: "white",
|
|
319
|
+
fontSize: 10,
|
|
320
|
+
fontWeight: 600,
|
|
321
|
+
padding: "2px 8px",
|
|
322
|
+
borderRadius: 4,
|
|
323
|
+
whiteSpace: "nowrap",
|
|
324
|
+
boxShadow: "0 2px 6px rgba(255, 107, 53, 0.4)",
|
|
325
|
+
}}
|
|
326
|
+
>
|
|
327
|
+
Min width
|
|
328
|
+
</div>
|
|
329
|
+
</>
|
|
330
|
+
)}
|
|
331
|
+
|
|
332
|
+
{/* Span badge — top right */}
|
|
333
|
+
<div
|
|
334
|
+
className={`absolute top-0 right-0 z-[5] transition-opacity ${
|
|
335
|
+
isSelected || isHovered ? "opacity-100" : showFaintOutline ? "opacity-40" : "opacity-0"
|
|
336
|
+
}`}
|
|
337
|
+
style={{ transform: `scale(${1 / canvasZoom})`, transformOrigin: "top right" }}
|
|
338
|
+
>
|
|
339
|
+
<span className="text-[11px] px-1.5 py-0.5 rounded-bl rounded-tr bg-[#076bff]/80 text-white/90 font-medium tabular-nums">
|
|
340
|
+
{column.span}/{gridColumns}
|
|
341
|
+
</span>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
{/* Delete button — red circle top right, positioned outside the column box.
|
|
345
|
+
Nested pattern: outer div positions, inner div counter-scales. */}
|
|
346
|
+
<div
|
|
347
|
+
className={`absolute z-[6] transition-opacity ${
|
|
348
|
+
showChrome ? "opacity-100" : "opacity-0 pointer-events-none"
|
|
349
|
+
}`}
|
|
350
|
+
style={{
|
|
351
|
+
top: 0,
|
|
352
|
+
right: 0,
|
|
353
|
+
transform: "translate(40%, -40%)",
|
|
354
|
+
}}
|
|
355
|
+
onClick={(e) => e.stopPropagation()}
|
|
356
|
+
>
|
|
357
|
+
<div style={{ transform: `scale(${1 / canvasZoom})`, transformOrigin: "center" }}>
|
|
358
|
+
<button
|
|
359
|
+
onClick={handleDelete}
|
|
360
|
+
className="w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center shadow-md transition-transform hover:scale-[1.15] hover:bg-red-600 hover:shadow-red-500/30 hover:shadow-lg"
|
|
361
|
+
title="Delete column"
|
|
362
|
+
aria-label="Delete column"
|
|
363
|
+
>
|
|
364
|
+
<svg width="10" height="10" viewBox="0 0 10 10">
|
|
365
|
+
<path d="M2 2l6 6M8 2l-6 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
366
|
+
</svg>
|
|
367
|
+
</button>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
{/* Drag grip — top left corner, uses @dnd-kit useDraggable.
|
|
372
|
+
Nested pattern: outer div positions, inner div counter-scales. */}
|
|
373
|
+
<div
|
|
374
|
+
className={`absolute z-[6] transition-opacity ${
|
|
375
|
+
showChrome ? "opacity-100" : "opacity-0 pointer-events-none"
|
|
376
|
+
}`}
|
|
377
|
+
style={{
|
|
378
|
+
top: 0,
|
|
379
|
+
left: 0,
|
|
380
|
+
transform: "translate(-30%, -30%)",
|
|
381
|
+
}}
|
|
382
|
+
>
|
|
383
|
+
<div style={{ transform: `scale(${1 / canvasZoom})`, transformOrigin: "center" }}>
|
|
384
|
+
<div
|
|
385
|
+
className="w-5 h-5 rounded-full bg-[#076bff] text-white flex items-center justify-center shadow-md cursor-grab active:cursor-grabbing transition-transform hover:scale-[1.15] hover:bg-[#0559d4] hover:shadow-blue-500/30 hover:shadow-lg"
|
|
386
|
+
title="Drag to move column"
|
|
387
|
+
aria-label="Move column"
|
|
388
|
+
onMouseDown={(e) => {
|
|
389
|
+
e.stopPropagation();
|
|
390
|
+
onStartDrag?.(e);
|
|
391
|
+
}}
|
|
392
|
+
onClick={(e) => {
|
|
393
|
+
e.stopPropagation();
|
|
394
|
+
onSelect();
|
|
395
|
+
}}
|
|
396
|
+
>
|
|
397
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
|
|
398
|
+
<path d="M8 0l2.5 3h-2v4.5H13v-2L16 8l-3 2.5v-2H8.5V13h2L8 16l-2.5-3h2V8.5H3v2L0 8l3-2.5v2h4.5V3h-2L8 0z" />
|
|
399
|
+
</svg>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
{/* Resize handles — left and right edges */}
|
|
405
|
+
<ResizeHandle
|
|
406
|
+
edge="left"
|
|
407
|
+
showChrome={showChrome}
|
|
408
|
+
showFaintOutline={showFaintOutline}
|
|
409
|
+
resizingEdge={resizingEdge}
|
|
410
|
+
hoveredEdge={hoveredEdge}
|
|
411
|
+
onHoverEdge={setHoveredEdge}
|
|
412
|
+
onResizeStart={(e) => {
|
|
413
|
+
setResizingEdge("left");
|
|
414
|
+
const onUp = () => { setResizingEdge(null); setHoveredEdge(null); document.removeEventListener("mouseup", onUp); };
|
|
415
|
+
document.addEventListener("mouseup", onUp);
|
|
416
|
+
const container = (e.currentTarget as HTMLElement).closest("[data-v2-grid-container]") as HTMLElement;
|
|
417
|
+
if (container) {
|
|
418
|
+
onResizeLeft(column._key, e.clientX, column.grid_column, column.span, container);
|
|
419
|
+
}
|
|
420
|
+
}}
|
|
421
|
+
/>
|
|
422
|
+
<ResizeHandle
|
|
423
|
+
edge="right"
|
|
424
|
+
showChrome={showChrome}
|
|
425
|
+
showFaintOutline={showFaintOutline}
|
|
426
|
+
resizingEdge={resizingEdge}
|
|
427
|
+
hoveredEdge={hoveredEdge}
|
|
428
|
+
onHoverEdge={setHoveredEdge}
|
|
429
|
+
onResizeStart={(e) => {
|
|
430
|
+
setResizingEdge("right");
|
|
431
|
+
const onUp = () => { setResizingEdge(null); setHoveredEdge(null); document.removeEventListener("mouseup", onUp); };
|
|
432
|
+
document.addEventListener("mouseup", onUp);
|
|
433
|
+
const container = (e.currentTarget as HTMLElement).closest("[data-v2-grid-container]") as HTMLElement;
|
|
434
|
+
if (container) {
|
|
435
|
+
onResizeRight(column._key, e.clientX, column.span, container);
|
|
436
|
+
}
|
|
437
|
+
}}
|
|
438
|
+
/>
|
|
439
|
+
|
|
440
|
+
{/* Blocks content */}
|
|
441
|
+
<SortableContext items={blockIds} strategy={verticalListSortingStrategy}>
|
|
442
|
+
{!hasBlocks ? (
|
|
443
|
+
/* Empty column: show + Add Block */
|
|
444
|
+
<div
|
|
445
|
+
className="relative flex items-center justify-center"
|
|
446
|
+
style={{ minHeight: 80, padding: "16px 12px" }}
|
|
447
|
+
>
|
|
448
|
+
<button
|
|
449
|
+
onClick={handleAddBlockEmpty}
|
|
450
|
+
aria-label="Add block to empty column"
|
|
451
|
+
className={`w-full py-2 rounded-lg text-xs font-medium transition-all flex items-center justify-center ${
|
|
452
|
+
showChrome
|
|
453
|
+
? "bg-[#e28b00] text-white hover:bg-[#c67a00] shadow-sm opacity-100"
|
|
454
|
+
: showFaintOutline
|
|
455
|
+
? "bg-[#e28b00]/30 text-white/50 opacity-40"
|
|
456
|
+
: "bg-transparent text-transparent opacity-0 pointer-events-none"
|
|
457
|
+
}`}
|
|
458
|
+
style={{ pointerEvents: showChrome || showFaintOutline ? "auto" : "none" }}
|
|
459
|
+
>
|
|
460
|
+
+ Add Block
|
|
461
|
+
</button>
|
|
462
|
+
</div>
|
|
463
|
+
) : (
|
|
464
|
+
children
|
|
465
|
+
)}
|
|
466
|
+
</SortableContext>
|
|
467
|
+
|
|
468
|
+
{/* "+" add block button below blocks — absolutely positioned to avoid disrupting flex alignment */}
|
|
469
|
+
{hasBlocks && (
|
|
470
|
+
<div
|
|
471
|
+
className={`absolute left-0 right-0 z-[3] transition-all ${
|
|
472
|
+
showChrome ? "opacity-100" : showFaintOutline ? "opacity-30" : "opacity-0 pointer-events-none"
|
|
473
|
+
}`}
|
|
474
|
+
style={{ bottom: 0, transform: "translateY(100%)", padding: "4px 12px 0" }}
|
|
475
|
+
>
|
|
476
|
+
<button
|
|
477
|
+
onClick={handleAddBlockBelow}
|
|
478
|
+
aria-label="Add block below existing blocks"
|
|
479
|
+
className="w-full py-1.5 text-[11px] font-medium rounded bg-[#e28b00] text-white hover:bg-[#c67a00] transition-all shadow-sm"
|
|
480
|
+
style={{ pointerEvents: showChrome ? "auto" : "none" }}
|
|
481
|
+
>
|
|
482
|
+
+ Add Block
|
|
483
|
+
</button>
|
|
484
|
+
</div>
|
|
485
|
+
)}
|
|
486
|
+
</div>
|
|
487
|
+
);
|
|
488
|
+
}
|