@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,337 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DndContext,
|
|
5
|
+
DragOverlay,
|
|
6
|
+
closestCenter,
|
|
7
|
+
KeyboardSensor,
|
|
8
|
+
PointerSensor,
|
|
9
|
+
useSensor,
|
|
10
|
+
useSensors,
|
|
11
|
+
type DragStartEvent,
|
|
12
|
+
type DragEndEvent,
|
|
13
|
+
} from "@dnd-kit/core";
|
|
14
|
+
import { useState, useCallback, useMemo, useEffect, memo, type ReactNode } from "react";
|
|
15
|
+
import { createPortal } from "react-dom";
|
|
16
|
+
import { useBuilderStore } from "../../lib/builder/store";
|
|
17
|
+
import { ALL_BLOCK_INFO } from "../../lib/builder/types";
|
|
18
|
+
import type { PageSectionV2, ParallaxGroup } from "../../lib/sanity/types";
|
|
19
|
+
import { isPageSectionV2, isParallaxGroup } from "../../lib/sanity/types";
|
|
20
|
+
|
|
21
|
+
// ============================================
|
|
22
|
+
// Scale Modifier for dnd-kit inside CSS-scaled canvas
|
|
23
|
+
// ============================================
|
|
24
|
+
|
|
25
|
+
function createScaleModifier(zoom: number) {
|
|
26
|
+
return ({ transform }: { transform: { x: number; y: number; scaleX: number; scaleY: number } }) => ({
|
|
27
|
+
...transform,
|
|
28
|
+
x: transform.x / zoom,
|
|
29
|
+
y: transform.y / zoom,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ============================================
|
|
34
|
+
// DnD ID Helpers
|
|
35
|
+
// ============================================
|
|
36
|
+
|
|
37
|
+
// IDs encode context: "row:KEY" or "block:ROWKEY:COLKEY:BLOCKKEY"
|
|
38
|
+
export function makeRowId(rowKey: string) {
|
|
39
|
+
return `row:${rowKey}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function makeBlockId(rowKey: string, colKey: string, blockKey: string) {
|
|
43
|
+
return `block:${rowKey}:${colKey}:${blockKey}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function makeColumnDroppableId(rowKey: string, colKey: string) {
|
|
47
|
+
return `col-drop:${rowKey}:${colKey}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseId(id: string) {
|
|
51
|
+
const parts = id.split(":");
|
|
52
|
+
if (parts[0] === "row" && parts.length >= 2) {
|
|
53
|
+
return { type: "row" as const, rowKey: parts[1] };
|
|
54
|
+
}
|
|
55
|
+
if (parts[0] === "block" && parts.length >= 4) {
|
|
56
|
+
return { type: "block" as const, rowKey: parts[1], colKey: parts[2], blockKey: parts[3] };
|
|
57
|
+
}
|
|
58
|
+
if (parts[0] === "col-drop" && parts.length >= 3) {
|
|
59
|
+
return { type: "column" as const, rowKey: parts[1], colKey: parts[2] };
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ============================================
|
|
65
|
+
// Drag Overlay Components
|
|
66
|
+
// ============================================
|
|
67
|
+
|
|
68
|
+
function RowDragOverlay({ rowKey }: { rowKey: string }) {
|
|
69
|
+
const rows = useBuilderStore((s) => s.rows);
|
|
70
|
+
const item = rows.find((r) => r._key === rowKey);
|
|
71
|
+
if (!item) return null;
|
|
72
|
+
|
|
73
|
+
// PageSectionV2
|
|
74
|
+
if (isPageSectionV2(item)) {
|
|
75
|
+
const v2Section = item as PageSectionV2;
|
|
76
|
+
const colCount = v2Section.columns?.length || 0;
|
|
77
|
+
return (
|
|
78
|
+
<div className="rounded border border-[#076bff] bg-[#1a1a1a]/90 px-4 py-3 shadow-lg shadow-[#076bff]/20 backdrop-blur-sm">
|
|
79
|
+
<div className="flex items-center gap-2">
|
|
80
|
+
<span className="text-[#076bff]">⠿</span>
|
|
81
|
+
<span className="text-xs text-white">V2 Section · {colCount} col{colCount !== 1 ? "s" : ""}</span>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ParallaxGroup
|
|
88
|
+
if (isParallaxGroup(item)) {
|
|
89
|
+
const group = item as ParallaxGroup;
|
|
90
|
+
return (
|
|
91
|
+
<div className="rounded border border-[#8b5cf6] bg-[#1a1a1a]/90 px-4 py-3 shadow-lg shadow-[#8b5cf6]/20 backdrop-blur-sm">
|
|
92
|
+
<div className="flex items-center gap-2">
|
|
93
|
+
<span className="text-[#8b5cf6]">⠿</span>
|
|
94
|
+
<span className="text-xs text-white">Parallax Showcase · {group.slides.length} slide{group.slides.length !== 1 ? "s" : ""}</span>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// PageSection
|
|
101
|
+
if ((item as { _type?: string })._type === "pageSection") {
|
|
102
|
+
const section = item as import("../../lib/sanity/types").PageSection;
|
|
103
|
+
const label = section.section_type === "projectGrid"
|
|
104
|
+
? "Project Grid"
|
|
105
|
+
: "Section";
|
|
106
|
+
return (
|
|
107
|
+
<div className="rounded border border-[#93278f] bg-[#1a1a1a]/90 px-4 py-3 shadow-lg shadow-[#93278f]/20 backdrop-blur-sm">
|
|
108
|
+
<div className="flex items-center gap-2">
|
|
109
|
+
<span className="text-[#93278f]">⠿</span>
|
|
110
|
+
<span className="text-xs text-white">{label}</span>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const BlockDragOverlay = memo(function BlockDragOverlay({ blockKey, rowKey }: { blockKey: string; rowKey: string }) {
|
|
120
|
+
const rows = useBuilderStore((s) => s.rows);
|
|
121
|
+
|
|
122
|
+
// Find the content item — could be a top-level row or a parallax slide
|
|
123
|
+
let item = rows.find((r) => r._key === rowKey);
|
|
124
|
+
// If not found at top level, search inside parallax groups for a matching slide
|
|
125
|
+
if (!item) {
|
|
126
|
+
for (const r of rows) {
|
|
127
|
+
if (isParallaxGroup(r)) {
|
|
128
|
+
const group = r as ParallaxGroup;
|
|
129
|
+
const slide = group.slides.find((s) => s._key === rowKey);
|
|
130
|
+
if (slide) {
|
|
131
|
+
// Create a virtual section item for block lookup
|
|
132
|
+
item = { _type: "pageSectionV2", _key: slide._key, section_type: "empty-v2", columns: slide.columns, settings: slide.section_settings } as PageSectionV2;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!item) return null;
|
|
139
|
+
|
|
140
|
+
// Handle V2 sections, PageSections, and virtual sections from parallax slides
|
|
141
|
+
let block: import("../../lib/sanity/types").ContentBlock | undefined;
|
|
142
|
+
if (isPageSectionV2(item)) {
|
|
143
|
+
const v2Section = item as PageSectionV2;
|
|
144
|
+
for (const col of v2Section.columns || []) {
|
|
145
|
+
const found = (col.blocks || []).find((b) => b._key === blockKey);
|
|
146
|
+
if (found) { block = found; break; }
|
|
147
|
+
}
|
|
148
|
+
} else if ((item as { _type?: string })._type === "pageSection") {
|
|
149
|
+
const section = item as import("../../lib/sanity/types").PageSection;
|
|
150
|
+
const sBlock = Array.isArray(section.block) ? section.block[0] : undefined;
|
|
151
|
+
if (sBlock && sBlock._key === blockKey) {
|
|
152
|
+
block = sBlock as import("../../lib/sanity/types").ContentBlock;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!block) return null;
|
|
157
|
+
const info = ALL_BLOCK_INFO.find((b) => b.type === block!._type);
|
|
158
|
+
return (
|
|
159
|
+
<div className="rounded border border-[#e28b00] bg-[#e28b00]/10 px-3 py-2 shadow-lg shadow-[#e28b00]/20 backdrop-blur-sm">
|
|
160
|
+
<div className="flex items-center gap-2">
|
|
161
|
+
<span className="text-xs">{info?.icon || "▪"}</span>
|
|
162
|
+
<span className="text-xs text-white">{info?.label || block._type}</span>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ============================================
|
|
169
|
+
// DndWrapper — handles row reordering and block DnD
|
|
170
|
+
//
|
|
171
|
+
// Column drag is now handled by the custom useColumnDrag
|
|
172
|
+
// hook + ColumnDragContext (Session 111).
|
|
173
|
+
// ============================================
|
|
174
|
+
|
|
175
|
+
interface DndWrapperProps {
|
|
176
|
+
children: ReactNode;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
interface ActiveDrag {
|
|
180
|
+
type: "row" | "block";
|
|
181
|
+
rowKey: string;
|
|
182
|
+
colKey?: string;
|
|
183
|
+
blockKey?: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export default function DndWrapper({ children }: DndWrapperProps) {
|
|
187
|
+
// Use individual selectors — avoid subscribing to the entire store which
|
|
188
|
+
// causes re-renders on every zoom/pan/selection/text change.
|
|
189
|
+
const canvasZoom = useBuilderStore((s) => s.canvasZoom);
|
|
190
|
+
const reorderRows = useBuilderStore((s) => s.reorderRows);
|
|
191
|
+
const reorderBlocks = useBuilderStore((s) => s.reorderBlocks);
|
|
192
|
+
const moveBlock = useBuilderStore((s) => s.moveBlock);
|
|
193
|
+
const [activeDrag, setActiveDrag] = useState<ActiveDrag | null>(null);
|
|
194
|
+
|
|
195
|
+
// Portal target for DragOverlay — must render outside the canvas transform
|
|
196
|
+
// container so overlay positioning uses viewport coordinates directly.
|
|
197
|
+
const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
setPortalTarget(document.body);
|
|
200
|
+
}, []);
|
|
201
|
+
|
|
202
|
+
const scaleModifier = useMemo(() => createScaleModifier(canvasZoom), [canvasZoom]);
|
|
203
|
+
|
|
204
|
+
const sensors = useSensors(
|
|
205
|
+
useSensor(PointerSensor, {
|
|
206
|
+
activationConstraint: {
|
|
207
|
+
distance: 8, // 8px before drag starts (avoids accidental drags)
|
|
208
|
+
},
|
|
209
|
+
}),
|
|
210
|
+
useSensor(KeyboardSensor)
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
214
|
+
const parsed = parseId(event.active.id as string);
|
|
215
|
+
if (!parsed) return;
|
|
216
|
+
|
|
217
|
+
if (parsed.type === "row") {
|
|
218
|
+
setActiveDrag({ type: "row", rowKey: parsed.rowKey });
|
|
219
|
+
} else if (parsed.type === "block") {
|
|
220
|
+
setActiveDrag({
|
|
221
|
+
type: "block",
|
|
222
|
+
rowKey: parsed.rowKey,
|
|
223
|
+
colKey: parsed.colKey,
|
|
224
|
+
blockKey: parsed.blockKey,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}, []);
|
|
228
|
+
|
|
229
|
+
const handleDragEnd = useCallback(
|
|
230
|
+
(event: DragEndEvent) => {
|
|
231
|
+
const { active, over } = event;
|
|
232
|
+
setActiveDrag(null);
|
|
233
|
+
|
|
234
|
+
if (!over || active.id === over.id) return;
|
|
235
|
+
|
|
236
|
+
const activeParsed = parseId(active.id as string);
|
|
237
|
+
const overParsed = parseId(over.id as string);
|
|
238
|
+
|
|
239
|
+
if (!activeParsed || !overParsed) return;
|
|
240
|
+
|
|
241
|
+
// Read rows at drag-end time (not subscribed — avoids re-renders)
|
|
242
|
+
const rows = useBuilderStore.getState().rows;
|
|
243
|
+
|
|
244
|
+
// Row reordering
|
|
245
|
+
if (activeParsed.type === "row" && overParsed.type === "row") {
|
|
246
|
+
const fromIndex = rows.findIndex((r) => r._key === activeParsed.rowKey);
|
|
247
|
+
const toIndex = rows.findIndex((r) => r._key === overParsed.rowKey);
|
|
248
|
+
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
|
|
249
|
+
reorderRows(fromIndex, toIndex);
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Block reordering / cross-column move
|
|
255
|
+
if (activeParsed.type === "block") {
|
|
256
|
+
const fromRowKey = activeParsed.rowKey;
|
|
257
|
+
const fromColKey = activeParsed.colKey!;
|
|
258
|
+
const blockKey = activeParsed.blockKey!;
|
|
259
|
+
|
|
260
|
+
if (overParsed.type === "block") {
|
|
261
|
+
const toRowKey = overParsed.rowKey;
|
|
262
|
+
const toColKey = overParsed.colKey!;
|
|
263
|
+
const toBlockKey = overParsed.blockKey!;
|
|
264
|
+
|
|
265
|
+
// Find target index
|
|
266
|
+
let toCol: { blocks?: { _key: string }[] } | undefined;
|
|
267
|
+
|
|
268
|
+
// Check if target is a V2 section
|
|
269
|
+
const toItem = rows.find((r) => r._key === toRowKey);
|
|
270
|
+
if (toItem && isPageSectionV2(toItem)) {
|
|
271
|
+
const v2Section = toItem as PageSectionV2;
|
|
272
|
+
toCol = v2Section.columns?.find((c) => c._key === toColKey);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const toIndex = (toCol?.blocks || []).findIndex((b) => b._key === toBlockKey);
|
|
276
|
+
|
|
277
|
+
if (fromRowKey === toRowKey && fromColKey === toColKey) {
|
|
278
|
+
// Same column reorder
|
|
279
|
+
let fromCol: { blocks?: { _key: string }[] } | undefined;
|
|
280
|
+
const fromItem = rows.find((r) => r._key === fromRowKey);
|
|
281
|
+
if (fromItem && isPageSectionV2(fromItem)) {
|
|
282
|
+
fromCol = (fromItem as PageSectionV2).columns?.find((c) => c._key === fromColKey);
|
|
283
|
+
}
|
|
284
|
+
const fromIndex = (fromCol?.blocks || []).findIndex((b) => b._key === blockKey);
|
|
285
|
+
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
|
|
286
|
+
reorderBlocks(fromRowKey, fromColKey, fromIndex, toIndex);
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
// Cross-column move
|
|
290
|
+
moveBlock(blockKey, toRowKey, toColKey, toIndex);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
[reorderRows, reorderBlocks, moveBlock]
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<DndContext
|
|
300
|
+
sensors={sensors}
|
|
301
|
+
collisionDetection={closestCenter}
|
|
302
|
+
onDragStart={handleDragStart}
|
|
303
|
+
onDragEnd={handleDragEnd}
|
|
304
|
+
modifiers={[scaleModifier]}
|
|
305
|
+
>
|
|
306
|
+
{children}
|
|
307
|
+
{/* Portal the DragOverlay to document.body so it escapes the canvas
|
|
308
|
+
transform container (translate3d + scale). Without this, the overlay
|
|
309
|
+
inherits the canvas CSS transform and its viewport-based positioning
|
|
310
|
+
is offset by the pan/zoom values — causing the ghost to jump far
|
|
311
|
+
from the cursor on drag start. React Context crosses portals, so
|
|
312
|
+
DragOverlay still communicates with DndContext correctly. */}
|
|
313
|
+
{portalTarget
|
|
314
|
+
? createPortal(
|
|
315
|
+
<DragOverlay dropAnimation={null}>
|
|
316
|
+
{activeDrag?.type === "row" && (
|
|
317
|
+
<RowDragOverlay rowKey={activeDrag.rowKey} />
|
|
318
|
+
)}
|
|
319
|
+
{activeDrag?.type === "block" && activeDrag.blockKey && (
|
|
320
|
+
<BlockDragOverlay blockKey={activeDrag.blockKey} rowKey={activeDrag.rowKey} />
|
|
321
|
+
)}
|
|
322
|
+
</DragOverlay>,
|
|
323
|
+
portalTarget
|
|
324
|
+
)
|
|
325
|
+
: (
|
|
326
|
+
<DragOverlay dropAnimation={null}>
|
|
327
|
+
{activeDrag?.type === "row" && (
|
|
328
|
+
<RowDragOverlay rowKey={activeDrag.rowKey} />
|
|
329
|
+
)}
|
|
330
|
+
{activeDrag?.type === "block" && activeDrag.blockKey && (
|
|
331
|
+
<BlockDragOverlay blockKey={activeDrag.blockKey} rowKey={activeDrag.rowKey} />
|
|
332
|
+
)}
|
|
333
|
+
</DragOverlay>
|
|
334
|
+
)}
|
|
335
|
+
</DndContext>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, memo } from "react";
|
|
4
|
+
import type { CascadeColumn } from "../../lib/builder/cascade-helpers";
|
|
5
|
+
import type { DropTarget } from "./hooks/useColumnDrag";
|
|
6
|
+
import { BUILDER_GREEN } from "../../lib/builder/constants";
|
|
7
|
+
|
|
8
|
+
// ============================================
|
|
9
|
+
// InsertionLines — Visual insertion indicators between adjacent columns
|
|
10
|
+
// ============================================
|
|
11
|
+
// Rendered during custom column drag. Shows thin green lines with a circle
|
|
12
|
+
// indicator between columns that have no gap between them, serving as
|
|
13
|
+
// drop targets for inserting a column between two neighbours.
|
|
14
|
+
//
|
|
15
|
+
// Extracted from SectionV2Canvas (Session G refactor).
|
|
16
|
+
|
|
17
|
+
/** Insertion point between two adjacent columns (no gap between them) */
|
|
18
|
+
interface InsertionPoint {
|
|
19
|
+
row: number;
|
|
20
|
+
gridColumn: number; // The grid column where the insertion line sits (= right column's start)
|
|
21
|
+
leftColKey: string;
|
|
22
|
+
rightColKey: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface InsertionLinesProps {
|
|
26
|
+
displayCols: (CascadeColumn & { _key: string })[];
|
|
27
|
+
draggingColumnKey: string | null;
|
|
28
|
+
containerWidth: number;
|
|
29
|
+
gridColumns: number;
|
|
30
|
+
colGap: number;
|
|
31
|
+
sectionKey: string;
|
|
32
|
+
dropTarget: DropTarget | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const InsertionLines = memo(function InsertionLines({
|
|
36
|
+
displayCols,
|
|
37
|
+
draggingColumnKey,
|
|
38
|
+
containerWidth,
|
|
39
|
+
gridColumns,
|
|
40
|
+
colGap,
|
|
41
|
+
sectionKey,
|
|
42
|
+
dropTarget,
|
|
43
|
+
}: InsertionLinesProps) {
|
|
44
|
+
// Compute insertion points between adjacent columns (no gap between them)
|
|
45
|
+
// Exclude the currently dragged column from consideration
|
|
46
|
+
const insertionPoints = useMemo((): InsertionPoint[] => {
|
|
47
|
+
const points: InsertionPoint[] = [];
|
|
48
|
+
// Group display columns by row
|
|
49
|
+
const rowMap = new Map<number, (CascadeColumn & { _key: string })[]>();
|
|
50
|
+
for (const dc of displayCols) {
|
|
51
|
+
// Skip the dragged column — it's "lifted" from the grid
|
|
52
|
+
if (dc._key === draggingColumnKey) continue;
|
|
53
|
+
const row = dc.grid_row;
|
|
54
|
+
if (!rowMap.has(row)) rowMap.set(row, []);
|
|
55
|
+
rowMap.get(row)!.push(dc);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const [row, cols] of rowMap) {
|
|
59
|
+
const sorted = [...cols].sort((a, b) => a.grid_column - b.grid_column);
|
|
60
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
61
|
+
const colA = sorted[i];
|
|
62
|
+
const colB = sorted[i + 1];
|
|
63
|
+
const aEnd = colA.grid_column + colA.span; // 1 past end
|
|
64
|
+
// Adjacent = no gap between them
|
|
65
|
+
if (aEnd === colB.grid_column) {
|
|
66
|
+
points.push({
|
|
67
|
+
row,
|
|
68
|
+
gridColumn: colB.grid_column,
|
|
69
|
+
leftColKey: colA._key,
|
|
70
|
+
rightColKey: colB._key,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return points;
|
|
77
|
+
}, [displayCols, draggingColumnKey]);
|
|
78
|
+
|
|
79
|
+
// Compute pixel positions for insertion lines
|
|
80
|
+
const insertionLinePositions = useMemo(() => {
|
|
81
|
+
if (containerWidth === 0) return [];
|
|
82
|
+
|
|
83
|
+
// Each 1fr unit width: (containerWidth - totalGaps) / gridColumns
|
|
84
|
+
const totalGaps = (gridColumns - 1) * colGap;
|
|
85
|
+
const unitWidth = (containerWidth - totalGaps) / gridColumns;
|
|
86
|
+
|
|
87
|
+
return insertionPoints.map((pt) => {
|
|
88
|
+
// The left pixel of the grid column `gc` is:
|
|
89
|
+
// (gc - 1) * unitWidth + (gc - 1) * colGap = (gc - 1) * (unitWidth + colGap)
|
|
90
|
+
const leftPxOfRightCol = (pt.gridColumn - 1) * (unitWidth + colGap);
|
|
91
|
+
// The insertion line sits in the middle of the gap between the two columns
|
|
92
|
+
const linePx = leftPxOfRightCol - colGap / 2;
|
|
93
|
+
return { ...pt, pixelLeft: linePx };
|
|
94
|
+
});
|
|
95
|
+
}, [containerWidth, insertionPoints, gridColumns, colGap]);
|
|
96
|
+
|
|
97
|
+
if (insertionLinePositions.length === 0) return null;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<>
|
|
101
|
+
{insertionLinePositions.map((pt) => {
|
|
102
|
+
const isActive =
|
|
103
|
+
dropTarget?.type === "insert" &&
|
|
104
|
+
dropTarget.sectionKey === sectionKey &&
|
|
105
|
+
dropTarget.insertRow === pt.row &&
|
|
106
|
+
dropTarget.insertCol === pt.gridColumn;
|
|
107
|
+
// Hit-test zone: at least the gap width + 10px padding, but never less than 24px.
|
|
108
|
+
// The 24px minimum ensures the target is usable even with very small (or zero) column gaps.
|
|
109
|
+
const hitWidth = Math.max(colGap + 10, 24);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div
|
|
113
|
+
key={`insert-${pt.row}-${pt.gridColumn}`}
|
|
114
|
+
style={{
|
|
115
|
+
gridColumn: "1 / -1",
|
|
116
|
+
gridRow: pt.row,
|
|
117
|
+
position: "relative",
|
|
118
|
+
pointerEvents: "none",
|
|
119
|
+
zIndex: 20,
|
|
120
|
+
alignSelf: "stretch",
|
|
121
|
+
minHeight: 0,
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
<div
|
|
125
|
+
data-col-v2-insert=""
|
|
126
|
+
data-section-key={sectionKey}
|
|
127
|
+
data-insert-row={pt.row}
|
|
128
|
+
data-insert-col={pt.gridColumn}
|
|
129
|
+
data-insert-left-key={pt.leftColKey}
|
|
130
|
+
data-insert-right-key={pt.rightColKey}
|
|
131
|
+
style={{
|
|
132
|
+
position: "absolute",
|
|
133
|
+
left: pt.pixelLeft - hitWidth / 2,
|
|
134
|
+
top: 0,
|
|
135
|
+
bottom: 0,
|
|
136
|
+
width: hitWidth,
|
|
137
|
+
pointerEvents: "auto",
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
{/* Visual insertion line — green when active */}
|
|
141
|
+
<div
|
|
142
|
+
style={{
|
|
143
|
+
position: "absolute",
|
|
144
|
+
left: "50%",
|
|
145
|
+
top: 4,
|
|
146
|
+
bottom: 4,
|
|
147
|
+
width: isActive ? 4 : 0,
|
|
148
|
+
transform: "translateX(-50%)",
|
|
149
|
+
background: BUILDER_GREEN,
|
|
150
|
+
borderRadius: 2,
|
|
151
|
+
transition: "width 100ms ease-out, opacity 100ms ease-out",
|
|
152
|
+
opacity: isActive ? 1 : 0,
|
|
153
|
+
}}
|
|
154
|
+
/>
|
|
155
|
+
{/* Circle indicator at center */}
|
|
156
|
+
{isActive && (
|
|
157
|
+
<div
|
|
158
|
+
style={{
|
|
159
|
+
position: "absolute",
|
|
160
|
+
left: "50%",
|
|
161
|
+
top: "50%",
|
|
162
|
+
transform: "translate(-50%, -50%)",
|
|
163
|
+
width: 14,
|
|
164
|
+
height: 14,
|
|
165
|
+
borderRadius: "50%",
|
|
166
|
+
background: BUILDER_GREEN,
|
|
167
|
+
border: "2px solid white",
|
|
168
|
+
boxShadow: "0 1px 4px rgba(34, 197, 94, 0.4)",
|
|
169
|
+
display: "flex",
|
|
170
|
+
alignItems: "center",
|
|
171
|
+
justifyContent: "center",
|
|
172
|
+
fontSize: 10,
|
|
173
|
+
color: "white",
|
|
174
|
+
fontWeight: "bold",
|
|
175
|
+
}}
|
|
176
|
+
>
|
|
177
|
+
+
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
})}
|
|
184
|
+
</>
|
|
185
|
+
);
|
|
186
|
+
});
|