@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,472 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
4
|
+
import { useBuilderStore } from "../../../lib/builder/store";
|
|
5
|
+
import type { PageSectionV2 } from "../../../lib/sanity/types";
|
|
6
|
+
import { isPageSectionV2 } from "../../../lib/sanity/types";
|
|
7
|
+
import { getEffectiveColumnsV2, buildColumnV2Overrides } from "../settings-panel/responsive-helpers";
|
|
8
|
+
import { moveColumn as cascadeMoveColumn } from "../../../lib/builder/cascade";
|
|
9
|
+
import type { DeviceViewport } from "../../../lib/builder/types";
|
|
10
|
+
|
|
11
|
+
// ============================================
|
|
12
|
+
// Types
|
|
13
|
+
// ============================================
|
|
14
|
+
|
|
15
|
+
export interface DropTarget {
|
|
16
|
+
type: "swap" | "gap" | "insert";
|
|
17
|
+
sectionKey: string;
|
|
18
|
+
/** swap: the column to swap with */
|
|
19
|
+
columnKey?: string;
|
|
20
|
+
/** gap: position and span of the empty gap */
|
|
21
|
+
gapRow?: number;
|
|
22
|
+
gapCol?: number;
|
|
23
|
+
gapSpan?: number;
|
|
24
|
+
/** insert: grid position where the column will be inserted */
|
|
25
|
+
insertRow?: number;
|
|
26
|
+
insertCol?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface InsertBetween {
|
|
30
|
+
leftKey: string;
|
|
31
|
+
rightKey: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface UseColumnDragReturn {
|
|
35
|
+
isDragging: boolean;
|
|
36
|
+
draggedColumnKey: string | null;
|
|
37
|
+
draggedSectionKey: string | null;
|
|
38
|
+
dropTarget: DropTarget | null;
|
|
39
|
+
insertBetween: InsertBetween | null;
|
|
40
|
+
overlayPosition: { x: number; y: number } | null;
|
|
41
|
+
startDrag: (e: React.MouseEvent, sectionKey: string, columnKey: string) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ============================================
|
|
45
|
+
// Responsive Helper Functions (private)
|
|
46
|
+
// ============================================
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compute swap on effective columns and write as responsive overrides.
|
|
50
|
+
* Follows the same pattern as doMoveColumnV2 in DndWrapper.tsx.
|
|
51
|
+
*/
|
|
52
|
+
function executeResponsiveSwap(
|
|
53
|
+
sectionKey: string,
|
|
54
|
+
draggedKey: string,
|
|
55
|
+
targetKey: string,
|
|
56
|
+
viewport: DeviceViewport,
|
|
57
|
+
updateSectionV2Responsive: (sectionKey: string, responsive: PageSectionV2["responsive"]) => void
|
|
58
|
+
): void {
|
|
59
|
+
const rows = useBuilderStore.getState().rows;
|
|
60
|
+
const section = rows.find((r) => r._key === sectionKey);
|
|
61
|
+
if (!section || !isPageSectionV2(section)) return;
|
|
62
|
+
const v2Section = section as PageSectionV2;
|
|
63
|
+
|
|
64
|
+
const effectiveCols = getEffectiveColumnsV2(v2Section, viewport);
|
|
65
|
+
const draggedCol = effectiveCols.find((c) => c._key === draggedKey);
|
|
66
|
+
const targetCol = effectiveCols.find((c) => c._key === targetKey);
|
|
67
|
+
if (!draggedCol || !targetCol) return;
|
|
68
|
+
|
|
69
|
+
// Swap positions in effective columns
|
|
70
|
+
const columnOverrides = effectiveCols.map((c) => {
|
|
71
|
+
if (c._key === draggedKey) {
|
|
72
|
+
return {
|
|
73
|
+
_key: c._key,
|
|
74
|
+
grid_column: targetCol.grid_column,
|
|
75
|
+
grid_row: targetCol.grid_row,
|
|
76
|
+
span: targetCol.span,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (c._key === targetKey) {
|
|
80
|
+
return {
|
|
81
|
+
_key: c._key,
|
|
82
|
+
grid_column: draggedCol.grid_column,
|
|
83
|
+
grid_row: draggedCol.grid_row,
|
|
84
|
+
span: draggedCol.span,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
_key: c._key,
|
|
89
|
+
grid_column: c.grid_column,
|
|
90
|
+
grid_row: c.grid_row,
|
|
91
|
+
span: c.span,
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const responsive = buildColumnV2Overrides(v2Section, viewport, columnOverrides);
|
|
96
|
+
updateSectionV2Responsive(sectionKey, responsive);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Compute gap move on effective columns and write as responsive overrides.
|
|
101
|
+
*/
|
|
102
|
+
function executeResponsiveGapMove(
|
|
103
|
+
sectionKey: string,
|
|
104
|
+
columnKey: string,
|
|
105
|
+
targetRow: number,
|
|
106
|
+
targetCol: number,
|
|
107
|
+
targetSpan: number,
|
|
108
|
+
viewport: DeviceViewport,
|
|
109
|
+
updateSectionV2Responsive: (sectionKey: string, responsive: PageSectionV2["responsive"]) => void
|
|
110
|
+
): void {
|
|
111
|
+
const rows = useBuilderStore.getState().rows;
|
|
112
|
+
const section = rows.find((r) => r._key === sectionKey);
|
|
113
|
+
if (!section || !isPageSectionV2(section)) return;
|
|
114
|
+
const v2Section = section as PageSectionV2;
|
|
115
|
+
|
|
116
|
+
const effectiveCols = getEffectiveColumnsV2(v2Section, viewport);
|
|
117
|
+
const columnOverrides = effectiveCols.map((c) => {
|
|
118
|
+
if (c._key === columnKey) {
|
|
119
|
+
return {
|
|
120
|
+
_key: c._key,
|
|
121
|
+
grid_column: targetCol,
|
|
122
|
+
grid_row: targetRow,
|
|
123
|
+
span: targetSpan,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
_key: c._key,
|
|
128
|
+
grid_column: c.grid_column,
|
|
129
|
+
grid_row: c.grid_row,
|
|
130
|
+
span: c.span,
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const responsive = buildColumnV2Overrides(v2Section, viewport, columnOverrides);
|
|
135
|
+
updateSectionV2Responsive(sectionKey, responsive);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Compute insert via cascadeMoveColumn on effective columns and write as overrides.
|
|
140
|
+
*/
|
|
141
|
+
function executeResponsiveInsert(
|
|
142
|
+
sectionKey: string,
|
|
143
|
+
columnKey: string,
|
|
144
|
+
targetRow: number,
|
|
145
|
+
targetCol: number,
|
|
146
|
+
viewport: DeviceViewport,
|
|
147
|
+
updateSectionV2Responsive: (sectionKey: string, responsive: PageSectionV2["responsive"]) => void
|
|
148
|
+
): void {
|
|
149
|
+
const rows = useBuilderStore.getState().rows;
|
|
150
|
+
const section = rows.find((r) => r._key === sectionKey);
|
|
151
|
+
if (!section || !isPageSectionV2(section)) return;
|
|
152
|
+
const v2Section = section as PageSectionV2;
|
|
153
|
+
|
|
154
|
+
const effectiveCols = getEffectiveColumnsV2(v2Section, viewport);
|
|
155
|
+
const cascadeResult = cascadeMoveColumn(effectiveCols, columnKey, targetRow, targetCol, v2Section.settings.grid_columns);
|
|
156
|
+
|
|
157
|
+
const columnOverrides = cascadeResult.map((c) => ({
|
|
158
|
+
_key: c._key,
|
|
159
|
+
grid_column: c.grid_column,
|
|
160
|
+
grid_row: c.grid_row,
|
|
161
|
+
span: c.span,
|
|
162
|
+
}));
|
|
163
|
+
|
|
164
|
+
const responsive = buildColumnV2Overrides(v2Section, viewport, columnOverrides);
|
|
165
|
+
updateSectionV2Responsive(sectionKey, responsive);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ============================================
|
|
169
|
+
// useColumnDrag Hook
|
|
170
|
+
// ============================================
|
|
171
|
+
|
|
172
|
+
export function useColumnDrag(): UseColumnDragReturn {
|
|
173
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
174
|
+
const [draggedColumnKey, setDraggedColumnKey] = useState<string | null>(null);
|
|
175
|
+
const [draggedSectionKey, setDraggedSectionKey] = useState<string | null>(null);
|
|
176
|
+
const [dropTarget, setDropTarget] = useState<DropTarget | null>(null);
|
|
177
|
+
const [insertBetween, setInsertBetween] = useState<InsertBetween | null>(null);
|
|
178
|
+
const [overlayPosition, setOverlayPosition] = useState<{ x: number; y: number } | null>(null);
|
|
179
|
+
|
|
180
|
+
// --- Mutable ref for drag state + store actions (stale closure prevention) ---
|
|
181
|
+
const dragRef = useRef({
|
|
182
|
+
sectionKey: "",
|
|
183
|
+
columnKey: "",
|
|
184
|
+
active: false,
|
|
185
|
+
pending: false, // mousedown captured, waiting for 8px threshold
|
|
186
|
+
startX: 0,
|
|
187
|
+
startY: 0,
|
|
188
|
+
draggedEl: null as HTMLElement | null, // pointer-events guard
|
|
189
|
+
// Store actions — updated every render via useEffect
|
|
190
|
+
swapColumnV2: null as ((s: string, d: string, t: string) => void) | null,
|
|
191
|
+
moveColumnToGapV2: null as ((s: string, c: string, r: number, col: number, sp: number) => void) | null,
|
|
192
|
+
moveColumnV2: null as ((s: string, c: string, r: number, col: number) => void) | null,
|
|
193
|
+
updateSectionV2Responsive: null as ((s: string, r: PageSectionV2["responsive"]) => void) | null,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Keep store actions fresh in the ref
|
|
197
|
+
const swapColumnV2 = useBuilderStore((s) => s.swapColumnV2);
|
|
198
|
+
const moveColumnToGapV2 = useBuilderStore((s) => s.moveColumnToGapV2);
|
|
199
|
+
const moveColumnV2 = useBuilderStore((s) => s.moveColumnV2);
|
|
200
|
+
const updateSectionV2Responsive = useBuilderStore((s) => s.updateSectionV2Responsive);
|
|
201
|
+
|
|
202
|
+
// Update ref every render — no deps array
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
dragRef.current.swapColumnV2 = swapColumnV2;
|
|
205
|
+
dragRef.current.moveColumnToGapV2 = moveColumnToGapV2;
|
|
206
|
+
dragRef.current.moveColumnV2 = moveColumnV2;
|
|
207
|
+
dragRef.current.updateSectionV2Responsive = updateSectionV2Responsive;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// --- Stable mousemove handler (empty deps — delegates to dragRef) ---
|
|
211
|
+
const stableMouseMove = useCallback((e: MouseEvent) => {
|
|
212
|
+
// Activation distance: 8px dead zone before drag starts
|
|
213
|
+
if (dragRef.current.pending && !dragRef.current.active) {
|
|
214
|
+
const dist = Math.hypot(
|
|
215
|
+
e.clientX - dragRef.current.startX,
|
|
216
|
+
e.clientY - dragRef.current.startY
|
|
217
|
+
);
|
|
218
|
+
if (dist < 8) return; // Still within dead zone
|
|
219
|
+
|
|
220
|
+
// Threshold met — activate drag
|
|
221
|
+
dragRef.current.pending = false;
|
|
222
|
+
dragRef.current.active = true;
|
|
223
|
+
|
|
224
|
+
const { sectionKey, columnKey } = dragRef.current;
|
|
225
|
+
setDraggedColumnKey(columnKey);
|
|
226
|
+
setDraggedSectionKey(sectionKey);
|
|
227
|
+
setOverlayPosition({ x: e.clientX, y: e.clientY });
|
|
228
|
+
setIsDragging(true);
|
|
229
|
+
|
|
230
|
+
document.body.style.cursor = "grabbing";
|
|
231
|
+
document.body.style.userSelect = "none";
|
|
232
|
+
|
|
233
|
+
// Pointer-events guard: prevent dragged column from capturing elementsFromPoint
|
|
234
|
+
const draggedEl = document.querySelector(
|
|
235
|
+
`[data-col-v2-droptarget][data-section-key="${sectionKey}"][data-column-key="${columnKey}"]`
|
|
236
|
+
) as HTMLElement | null;
|
|
237
|
+
if (draggedEl) {
|
|
238
|
+
dragRef.current.draggedEl = draggedEl;
|
|
239
|
+
draggedEl.style.pointerEvents = "none";
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!dragRef.current.active) return;
|
|
244
|
+
|
|
245
|
+
// 1. Update overlay position (viewport coordinates — no zoom compensation)
|
|
246
|
+
setOverlayPosition({ x: e.clientX, y: e.clientY });
|
|
247
|
+
|
|
248
|
+
const { sectionKey, columnKey } = dragRef.current;
|
|
249
|
+
|
|
250
|
+
// 2. Hit-test via elementsFromPoint
|
|
251
|
+
const elements = document.elementsFromPoint(e.clientX, e.clientY);
|
|
252
|
+
let newTarget: DropTarget | null = null;
|
|
253
|
+
let newInsert: InsertBetween | null = null;
|
|
254
|
+
|
|
255
|
+
for (const el of elements) {
|
|
256
|
+
const htmlEl = el as HTMLElement;
|
|
257
|
+
|
|
258
|
+
// Priority 1: Insert zone
|
|
259
|
+
if (htmlEl.dataset.colV2Insert !== undefined) {
|
|
260
|
+
const targetSection = htmlEl.dataset.sectionKey;
|
|
261
|
+
if (targetSection !== sectionKey) continue;
|
|
262
|
+
newTarget = {
|
|
263
|
+
type: "insert",
|
|
264
|
+
sectionKey: targetSection,
|
|
265
|
+
insertRow: parseInt(htmlEl.dataset.insertRow!, 10),
|
|
266
|
+
insertCol: parseInt(htmlEl.dataset.insertCol!, 10),
|
|
267
|
+
};
|
|
268
|
+
const leftKey = htmlEl.dataset.insertLeftKey;
|
|
269
|
+
const rightKey = htmlEl.dataset.insertRightKey;
|
|
270
|
+
if (leftKey && rightKey) {
|
|
271
|
+
newInsert = { leftKey, rightKey };
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Priority 2: Gap
|
|
277
|
+
if (htmlEl.dataset.colV2Gap !== undefined) {
|
|
278
|
+
const targetSection = htmlEl.dataset.sectionKey;
|
|
279
|
+
if (targetSection !== sectionKey) continue;
|
|
280
|
+
newTarget = {
|
|
281
|
+
type: "gap",
|
|
282
|
+
sectionKey: targetSection,
|
|
283
|
+
gapRow: parseInt(htmlEl.dataset.gapRow!, 10),
|
|
284
|
+
gapCol: parseInt(htmlEl.dataset.gapCol!, 10),
|
|
285
|
+
gapSpan: parseInt(htmlEl.dataset.gapSpan!, 10),
|
|
286
|
+
};
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Priority 3: Column (swap)
|
|
291
|
+
if (htmlEl.dataset.colV2Droptarget !== undefined) {
|
|
292
|
+
const targetSection = htmlEl.dataset.sectionKey;
|
|
293
|
+
const targetColumn = htmlEl.dataset.columnKey;
|
|
294
|
+
if (targetSection !== sectionKey) continue;
|
|
295
|
+
if (targetColumn === columnKey) continue; // skip self
|
|
296
|
+
newTarget = {
|
|
297
|
+
type: "swap",
|
|
298
|
+
sectionKey: targetSection,
|
|
299
|
+
columnKey: targetColumn,
|
|
300
|
+
};
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
setDropTarget(newTarget);
|
|
306
|
+
setInsertBetween(newTarget?.type === "insert" ? newInsert : null);
|
|
307
|
+
}, []);
|
|
308
|
+
|
|
309
|
+
// --- Stable mouseup handler ---
|
|
310
|
+
const stableMouseUp = useCallback((e: MouseEvent) => {
|
|
311
|
+
document.removeEventListener("mousemove", stableMouseMove);
|
|
312
|
+
document.removeEventListener("mouseup", stableMouseUp);
|
|
313
|
+
|
|
314
|
+
// Restore pointer-events on dragged element
|
|
315
|
+
if (dragRef.current.draggedEl) {
|
|
316
|
+
dragRef.current.draggedEl.style.pointerEvents = "";
|
|
317
|
+
dragRef.current.draggedEl = null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// If mouseup before activation threshold — clean up without executing any drop
|
|
321
|
+
if (dragRef.current.pending && !dragRef.current.active) {
|
|
322
|
+
dragRef.current.pending = false;
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
document.body.style.cursor = "";
|
|
327
|
+
document.body.style.userSelect = "";
|
|
328
|
+
|
|
329
|
+
const { sectionKey, columnKey } = dragRef.current;
|
|
330
|
+
dragRef.current.active = false;
|
|
331
|
+
|
|
332
|
+
// Final hit-test at mouseup position
|
|
333
|
+
const elements = document.elementsFromPoint(e.clientX, e.clientY);
|
|
334
|
+
let finalTarget: DropTarget | null = null;
|
|
335
|
+
|
|
336
|
+
for (const el of elements) {
|
|
337
|
+
const htmlEl = el as HTMLElement;
|
|
338
|
+
|
|
339
|
+
// Priority 1: Insert zone
|
|
340
|
+
if (htmlEl.dataset.colV2Insert !== undefined) {
|
|
341
|
+
const ts = htmlEl.dataset.sectionKey;
|
|
342
|
+
if (ts !== sectionKey) continue;
|
|
343
|
+
finalTarget = {
|
|
344
|
+
type: "insert",
|
|
345
|
+
sectionKey: ts,
|
|
346
|
+
insertRow: parseInt(htmlEl.dataset.insertRow!, 10),
|
|
347
|
+
insertCol: parseInt(htmlEl.dataset.insertCol!, 10),
|
|
348
|
+
};
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Priority 2: Gap
|
|
353
|
+
if (htmlEl.dataset.colV2Gap !== undefined) {
|
|
354
|
+
const ts = htmlEl.dataset.sectionKey;
|
|
355
|
+
if (ts !== sectionKey) continue;
|
|
356
|
+
finalTarget = {
|
|
357
|
+
type: "gap",
|
|
358
|
+
sectionKey: ts,
|
|
359
|
+
gapRow: parseInt(htmlEl.dataset.gapRow!, 10),
|
|
360
|
+
gapCol: parseInt(htmlEl.dataset.gapCol!, 10),
|
|
361
|
+
gapSpan: parseInt(htmlEl.dataset.gapSpan!, 10),
|
|
362
|
+
};
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Priority 3: Column (swap)
|
|
367
|
+
if (htmlEl.dataset.colV2Droptarget !== undefined) {
|
|
368
|
+
const ts = htmlEl.dataset.sectionKey;
|
|
369
|
+
const tc = htmlEl.dataset.columnKey;
|
|
370
|
+
if (ts !== sectionKey || tc === columnKey) continue;
|
|
371
|
+
finalTarget = { type: "swap", sectionKey: ts, columnKey: tc };
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Execute the drop action
|
|
377
|
+
if (finalTarget) {
|
|
378
|
+
const activeViewport = useBuilderStore.getState().activeViewport;
|
|
379
|
+
const isResponsive = activeViewport !== "desktop";
|
|
380
|
+
|
|
381
|
+
if (finalTarget.type === "swap" && finalTarget.columnKey) {
|
|
382
|
+
if (!isResponsive) {
|
|
383
|
+
dragRef.current.swapColumnV2?.(sectionKey, columnKey, finalTarget.columnKey);
|
|
384
|
+
} else {
|
|
385
|
+
executeResponsiveSwap(
|
|
386
|
+
sectionKey, columnKey, finalTarget.columnKey, activeViewport,
|
|
387
|
+
dragRef.current.updateSectionV2Responsive!
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
} else if (finalTarget.type === "gap") {
|
|
391
|
+
if (!isResponsive) {
|
|
392
|
+
dragRef.current.moveColumnToGapV2?.(
|
|
393
|
+
sectionKey, columnKey,
|
|
394
|
+
finalTarget.gapRow!, finalTarget.gapCol!, finalTarget.gapSpan!
|
|
395
|
+
);
|
|
396
|
+
} else {
|
|
397
|
+
executeResponsiveGapMove(
|
|
398
|
+
sectionKey, columnKey,
|
|
399
|
+
finalTarget.gapRow!, finalTarget.gapCol!, finalTarget.gapSpan!,
|
|
400
|
+
activeViewport,
|
|
401
|
+
dragRef.current.updateSectionV2Responsive!
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
} else if (finalTarget.type === "insert") {
|
|
405
|
+
if (!isResponsive) {
|
|
406
|
+
dragRef.current.moveColumnV2?.(
|
|
407
|
+
sectionKey, columnKey,
|
|
408
|
+
finalTarget.insertRow!, finalTarget.insertCol!
|
|
409
|
+
);
|
|
410
|
+
} else {
|
|
411
|
+
executeResponsiveInsert(
|
|
412
|
+
sectionKey, columnKey,
|
|
413
|
+
finalTarget.insertRow!, finalTarget.insertCol!,
|
|
414
|
+
activeViewport,
|
|
415
|
+
dragRef.current.updateSectionV2Responsive!
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Reset all React state
|
|
422
|
+
setIsDragging(false);
|
|
423
|
+
setDraggedColumnKey(null);
|
|
424
|
+
setDraggedSectionKey(null);
|
|
425
|
+
setDropTarget(null);
|
|
426
|
+
setInsertBetween(null);
|
|
427
|
+
setOverlayPosition(null);
|
|
428
|
+
}, [stableMouseMove]);
|
|
429
|
+
|
|
430
|
+
// --- startDrag ---
|
|
431
|
+
const startDrag = useCallback((e: React.MouseEvent, sectionKey: string, columnKey: string) => {
|
|
432
|
+
e.preventDefault();
|
|
433
|
+
e.stopPropagation();
|
|
434
|
+
|
|
435
|
+
dragRef.current.sectionKey = sectionKey;
|
|
436
|
+
dragRef.current.columnKey = columnKey;
|
|
437
|
+
dragRef.current.active = false;
|
|
438
|
+
dragRef.current.pending = true;
|
|
439
|
+
dragRef.current.startX = e.clientX;
|
|
440
|
+
dragRef.current.startY = e.clientY;
|
|
441
|
+
|
|
442
|
+
// Don't set React state yet — wait for 8px activation threshold in mousemove
|
|
443
|
+
|
|
444
|
+
document.addEventListener("mousemove", stableMouseMove);
|
|
445
|
+
document.addEventListener("mouseup", stableMouseUp);
|
|
446
|
+
}, [stableMouseMove, stableMouseUp]);
|
|
447
|
+
|
|
448
|
+
// Cleanup on unmount (safety — if component unmounts mid-drag)
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
return () => {
|
|
451
|
+
document.removeEventListener("mousemove", stableMouseMove);
|
|
452
|
+
document.removeEventListener("mouseup", stableMouseUp);
|
|
453
|
+
document.body.style.cursor = "";
|
|
454
|
+
document.body.style.userSelect = "";
|
|
455
|
+
if (dragRef.current.draggedEl) {
|
|
456
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
457
|
+
dragRef.current.draggedEl.style.pointerEvents = "";
|
|
458
|
+
dragRef.current.draggedEl = null;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
}, [stableMouseMove, stableMouseUp]);
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
isDragging,
|
|
465
|
+
draggedColumnKey,
|
|
466
|
+
draggedSectionKey,
|
|
467
|
+
dropTarget,
|
|
468
|
+
insertBetween,
|
|
469
|
+
overlayPosition,
|
|
470
|
+
startDrag,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
4
|
+
import {
|
|
5
|
+
resizeColumn as cascadeResizeColumn,
|
|
6
|
+
resizeColumnLeft as cascadeResizeLeft,
|
|
7
|
+
type ResizeLeftResult,
|
|
8
|
+
} from "../../../lib/builder/cascade";
|
|
9
|
+
import { toCascadeColumns } from "../../../lib/builder/cascade-helpers";
|
|
10
|
+
import type { CascadeColumn } from "../../../lib/builder/cascade-helpers";
|
|
11
|
+
import type { PageSectionV2 } from "../../../lib/sanity/types";
|
|
12
|
+
import type { DeviceViewport } from "../../../lib/builder/types";
|
|
13
|
+
import { buildColumnV2Overrides } from "../settings-panel/responsive-helpers";
|
|
14
|
+
|
|
15
|
+
// ============================================
|
|
16
|
+
// useColumnResize — Extracted from SectionV2Canvas
|
|
17
|
+
//
|
|
18
|
+
// Handles both left and right edge resizing for V2 grid columns.
|
|
19
|
+
// Uses a shared startResize() that parameterizes by direction,
|
|
20
|
+
// eliminating the two near-identical useCallback closures.
|
|
21
|
+
// ============================================
|
|
22
|
+
|
|
23
|
+
interface UseColumnResizeParams {
|
|
24
|
+
section: PageSectionV2;
|
|
25
|
+
gridColumns: number;
|
|
26
|
+
colGap: number;
|
|
27
|
+
canvasZoom: number;
|
|
28
|
+
effectiveCols: CascadeColumn[];
|
|
29
|
+
isResponsive: boolean;
|
|
30
|
+
activeViewport: DeviceViewport;
|
|
31
|
+
// Store actions
|
|
32
|
+
resizeColumnV2: (sectionKey: string, columnKey: string, newSpan: number) => void;
|
|
33
|
+
resizeColumnV2Left: (sectionKey: string, columnKey: string, newGridColumn: number) => void;
|
|
34
|
+
updateSectionV2Responsive: (sectionKey: string, responsive: Record<string, Record<string, unknown>>) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface SnappingInfo {
|
|
38
|
+
columnKey: string;
|
|
39
|
+
edge: "left" | "right";
|
|
40
|
+
willOverflow: boolean;
|
|
41
|
+
/** Key of the left neighbor being compressed during left-edge resize */
|
|
42
|
+
compressedNeighborKey?: string | null;
|
|
43
|
+
/** True when the left neighbor has reached minimum span (1) — visual warning */
|
|
44
|
+
neighborAtMinimum?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface UseColumnResizeReturn {
|
|
48
|
+
resizePreview: CascadeColumn[] | null;
|
|
49
|
+
snappingInfo: SnappingInfo | null;
|
|
50
|
+
handleResizeRight: (columnKey: string, startX: number, startSpan: number, containerEl: HTMLElement) => void;
|
|
51
|
+
handleResizeLeft: (columnKey: string, startX: number, startGridCol: number, startSpan: number, containerEl: HTMLElement) => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Compute pixel width of one grid column unit */
|
|
55
|
+
function computeColUnitWidth(containerWidth: number, gridColumns: number, colGap: number): number {
|
|
56
|
+
const totalGaps = (gridColumns - 1) * colGap;
|
|
57
|
+
return (containerWidth - totalGaps) / gridColumns;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function useColumnResize({
|
|
61
|
+
section,
|
|
62
|
+
gridColumns,
|
|
63
|
+
colGap,
|
|
64
|
+
canvasZoom,
|
|
65
|
+
effectiveCols,
|
|
66
|
+
isResponsive,
|
|
67
|
+
activeViewport,
|
|
68
|
+
resizeColumnV2,
|
|
69
|
+
resizeColumnV2Left,
|
|
70
|
+
updateSectionV2Responsive,
|
|
71
|
+
}: UseColumnResizeParams): UseColumnResizeReturn {
|
|
72
|
+
const [resizePreview, setResizePreview] = useState<CascadeColumn[] | null>(null);
|
|
73
|
+
const [snappingInfo, setSnappingInfo] = useState<SnappingInfo | null>(null);
|
|
74
|
+
|
|
75
|
+
// Ref to access the latest preview in mouseup handlers (closure escape)
|
|
76
|
+
const latestPreviewRef = useRef<CascadeColumn[] | null>(null);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
latestPreviewRef.current = resizePreview;
|
|
79
|
+
}, [resizePreview]);
|
|
80
|
+
|
|
81
|
+
/** Commit the resize result to the store (shared by both edges) */
|
|
82
|
+
const commitResize = useCallback(
|
|
83
|
+
(columnKey: string, startGridCol: number, startSpan: number, edge: "left" | "right") => {
|
|
84
|
+
const preview = latestPreviewRef.current;
|
|
85
|
+
if (!preview) return;
|
|
86
|
+
|
|
87
|
+
const finalCol = preview.find((c) => c._key === columnKey);
|
|
88
|
+
if (!finalCol) return;
|
|
89
|
+
|
|
90
|
+
// Check if anything actually changed
|
|
91
|
+
const changed =
|
|
92
|
+
edge === "right"
|
|
93
|
+
? finalCol.span !== startSpan
|
|
94
|
+
: finalCol.grid_column !== startGridCol || finalCol.span !== startSpan;
|
|
95
|
+
|
|
96
|
+
if (!changed) return;
|
|
97
|
+
|
|
98
|
+
if (isResponsive) {
|
|
99
|
+
// Tablet/Phone: write ALL cascade-affected columns as responsive overrides
|
|
100
|
+
const columnOverrides = preview.map((c) => ({
|
|
101
|
+
_key: c._key,
|
|
102
|
+
grid_column: c.grid_column,
|
|
103
|
+
grid_row: c.grid_row,
|
|
104
|
+
span: c.span,
|
|
105
|
+
}));
|
|
106
|
+
const responsive = buildColumnV2Overrides(section, activeViewport, columnOverrides);
|
|
107
|
+
if (responsive) {
|
|
108
|
+
updateSectionV2Responsive(section._key, responsive as Record<string, Record<string, unknown>>);
|
|
109
|
+
}
|
|
110
|
+
} else if (edge === "right") {
|
|
111
|
+
resizeColumnV2(section._key, columnKey, finalCol.span);
|
|
112
|
+
} else {
|
|
113
|
+
resizeColumnV2Left(section._key, columnKey, finalCol.grid_column);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
[isResponsive, section, activeViewport, resizeColumnV2, resizeColumnV2Left, updateSectionV2Responsive]
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
/** Shared cleanup for mouseup */
|
|
120
|
+
const cleanupResize = useCallback(() => {
|
|
121
|
+
document.body.style.cursor = "";
|
|
122
|
+
document.body.style.userSelect = "";
|
|
123
|
+
setResizePreview(null);
|
|
124
|
+
setSnappingInfo(null);
|
|
125
|
+
}, []);
|
|
126
|
+
|
|
127
|
+
// ---- Resize right handler (drag on right edge) ----
|
|
128
|
+
const handleResizeRight = useCallback(
|
|
129
|
+
(columnKey: string, startX: number, startSpan: number, containerEl: HTMLElement) => {
|
|
130
|
+
const cWidth = containerEl.getBoundingClientRect().width;
|
|
131
|
+
const colUnitWidth = computeColUnitWidth(cWidth, gridColumns, colGap);
|
|
132
|
+
const baseCols = isResponsive ? effectiveCols : toCascadeColumns(section.columns);
|
|
133
|
+
let lastPreviewedSpan = startSpan;
|
|
134
|
+
|
|
135
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
136
|
+
const deltaX = (e.clientX - startX) / canvasZoom;
|
|
137
|
+
const deltaCols = Math.round(deltaX / (colUnitWidth + colGap));
|
|
138
|
+
const newSpan = Math.max(1, Math.min(gridColumns, startSpan + deltaCols));
|
|
139
|
+
|
|
140
|
+
if (newSpan === lastPreviewedSpan) return;
|
|
141
|
+
lastPreviewedSpan = newSpan;
|
|
142
|
+
|
|
143
|
+
const previewCascade = cascadeResizeColumn(baseCols, columnKey, newSpan, gridColumns);
|
|
144
|
+
setResizePreview(previewCascade);
|
|
145
|
+
|
|
146
|
+
const originalMaxRow = baseCols.length > 0 ? Math.max(...baseCols.map((c) => c.grid_row)) : 1;
|
|
147
|
+
const newMaxRow = previewCascade.length > 0 ? Math.max(...previewCascade.map((c) => c.grid_row)) : 1;
|
|
148
|
+
const willOverflow = newMaxRow > originalMaxRow;
|
|
149
|
+
setSnappingInfo(willOverflow ? { columnKey, edge: "right", willOverflow: true } : null);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const handleMouseUp = () => {
|
|
153
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
154
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
155
|
+
commitResize(columnKey, 0, startSpan, "right");
|
|
156
|
+
cleanupResize();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
document.body.style.cursor = "col-resize";
|
|
160
|
+
document.body.style.userSelect = "none";
|
|
161
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
162
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
163
|
+
},
|
|
164
|
+
[gridColumns, colGap, canvasZoom, section, isResponsive, effectiveCols, commitResize, cleanupResize]
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// ---- Resize left handler (drag on left edge) ----
|
|
168
|
+
const handleResizeLeft = useCallback(
|
|
169
|
+
(columnKey: string, startX: number, startGridCol: number, startSpan: number, containerEl: HTMLElement) => {
|
|
170
|
+
const cWidth = containerEl.getBoundingClientRect().width;
|
|
171
|
+
const colUnitWidth = computeColUnitWidth(cWidth, gridColumns, colGap);
|
|
172
|
+
const baseCols = isResponsive ? effectiveCols : toCascadeColumns(section.columns);
|
|
173
|
+
let lastPreviewedGridCol = startGridCol;
|
|
174
|
+
|
|
175
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
176
|
+
const deltaX = (e.clientX - startX) / canvasZoom;
|
|
177
|
+
const deltaCols = Math.round(deltaX / (colUnitWidth + colGap));
|
|
178
|
+
const newGridColumn = Math.max(1, startGridCol + deltaCols);
|
|
179
|
+
if (newGridColumn >= startGridCol + startSpan) return;
|
|
180
|
+
|
|
181
|
+
if (newGridColumn === lastPreviewedGridCol) return;
|
|
182
|
+
lastPreviewedGridCol = newGridColumn;
|
|
183
|
+
|
|
184
|
+
const previewResult = cascadeResizeLeft(baseCols, columnKey, newGridColumn, gridColumns);
|
|
185
|
+
if (previewResult) {
|
|
186
|
+
setResizePreview(previewResult.columns);
|
|
187
|
+
// Surface neighbor compression state for visual feedback
|
|
188
|
+
if (previewResult.compressedNeighborKey) {
|
|
189
|
+
setSnappingInfo({
|
|
190
|
+
columnKey,
|
|
191
|
+
edge: "left",
|
|
192
|
+
willOverflow: false,
|
|
193
|
+
compressedNeighborKey: previewResult.compressedNeighborKey,
|
|
194
|
+
neighborAtMinimum: previewResult.neighborAtMinimum,
|
|
195
|
+
});
|
|
196
|
+
} else {
|
|
197
|
+
setSnappingInfo(null);
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
// Completely blocked (should be rare now — only if column not found)
|
|
201
|
+
setSnappingInfo({ columnKey, edge: "left", willOverflow: false });
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const handleMouseUp = () => {
|
|
206
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
207
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
208
|
+
commitResize(columnKey, startGridCol, startSpan, "left");
|
|
209
|
+
cleanupResize();
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
document.body.style.cursor = "col-resize";
|
|
213
|
+
document.body.style.userSelect = "none";
|
|
214
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
215
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
216
|
+
},
|
|
217
|
+
[gridColumns, colGap, canvasZoom, section, isResponsive, effectiveCols, commitResize, cleanupResize]
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
return { resizePreview, snappingInfo, handleResizeRight, handleResizeLeft };
|
|
221
|
+
}
|