@morphika/andami 0.4.1 → 0.5.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/README.md +2 -1
- package/components/builder/ColumnDragContext.tsx +5 -0
- package/components/builder/ColumnDragOverlay.tsx +59 -17
- package/components/builder/InsertionLines.tsx +9 -1
- package/components/builder/SectionV2Canvas.tsx +13 -3
- package/components/builder/hooks/useColumnDrag.ts +269 -142
- package/lib/builder/store-blocks.ts +2 -2
- package/lib/builder/store-canvas.ts +2 -2
- package/lib/builder/store-cover.ts +2 -2
- package/lib/builder/store-helpers.ts +345 -1
- package/lib/builder/store-sections.ts +62 -2
- package/lib/builder/types-slices.ts +414 -0
- package/lib/builder/types.ts +77 -225
- package/lib/sanity/types.ts +17 -0
- package/lib/version.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,8 @@ A reusable Visual Page Builder framework for Next.js. Build custom websites with
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **Visual Page Builder** — Infinite canvas editor with device previews (desktop, tablet, phone)
|
|
10
|
-
- **
|
|
10
|
+
- **6 Content Blocks** — Text, Image, Image Grid, Video, Spacer, Button (added via "+ Add Block" inside columns)
|
|
11
|
+
- **2 Section-level Blocks** — Project Grid (masonry) and Project Carousel (horizontal "keep browsing" at end of project pages). Added via "+ Add Section"
|
|
11
12
|
- **Cover Sections** — Full-viewport hero sections with proportional rows, background media, and drag-to-resize
|
|
12
13
|
- **V2 Grid System** — 12-column CSS grid with push cascade engine and responsive overrides
|
|
13
14
|
- **Custom Sections** — Create reusable sections with per-instance setting overrides
|
|
@@ -32,6 +32,11 @@ export function ColumnDragProvider({ children }: ColumnDragProviderProps) {
|
|
|
32
32
|
sectionKey={columnDrag.draggedSectionKey}
|
|
33
33
|
columnKey={columnDrag.draggedColumnKey}
|
|
34
34
|
position={columnDrag.overlayPosition}
|
|
35
|
+
// If hovering over a target, use its validity; if no target
|
|
36
|
+
// (empty space), stay in default (valid) state.
|
|
37
|
+
isValidDrop={
|
|
38
|
+
columnDrag.dropTarget ? columnDrag.dropTarget.isValid : true
|
|
39
|
+
}
|
|
35
40
|
/>
|
|
36
41
|
)}
|
|
37
42
|
</ColumnDragContext.Provider>
|
|
@@ -3,31 +3,63 @@
|
|
|
3
3
|
import { memo } from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
5
|
import { useBuilderStore } from "../../lib/builder/store";
|
|
6
|
-
import type { PageSectionV2 } from "../../lib/sanity/types";
|
|
7
|
-
import { isPageSectionV2 } from "../../lib/sanity/types";
|
|
6
|
+
import type { PageSectionV2, CoverSection, SectionColumn } from "../../lib/sanity/types";
|
|
7
|
+
import { isPageSectionV2, isCoverSection } from "../../lib/sanity/types";
|
|
8
8
|
import { BUILDER_BLUE } from "../../lib/builder/constants";
|
|
9
9
|
|
|
10
|
+
/** Color used when the current drop target is invalid (cross-section
|
|
11
|
+
* swap, or a target that is not V2/Cover). Red 500 with compatible
|
|
12
|
+
* translucent tints for the overlay chrome. */
|
|
13
|
+
const INVALID_RED = "#ef4444";
|
|
14
|
+
const INVALID_RED_RGB = "239, 68, 68";
|
|
15
|
+
|
|
10
16
|
interface ColumnDragOverlayProps {
|
|
11
17
|
sectionKey: string;
|
|
12
18
|
columnKey: string;
|
|
13
19
|
position: { x: number; y: number };
|
|
20
|
+
/**
|
|
21
|
+
* Whether the current drop target under the cursor is a valid drop.
|
|
22
|
+
* - true (or no target): render in blue (default).
|
|
23
|
+
* - false: render in red to signal "drop here won't execute".
|
|
24
|
+
*/
|
|
25
|
+
isValidDrop?: boolean;
|
|
14
26
|
}
|
|
15
27
|
|
|
16
28
|
const ColumnDragOverlay = memo(function ColumnDragOverlay({
|
|
17
29
|
sectionKey,
|
|
18
30
|
columnKey,
|
|
19
31
|
position,
|
|
32
|
+
isValidDrop = true,
|
|
20
33
|
}: ColumnDragOverlayProps) {
|
|
21
34
|
const rows = useBuilderStore((s) => s.rows);
|
|
22
35
|
const item = rows.find((r) => r._key === sectionKey);
|
|
23
|
-
if (!item
|
|
36
|
+
if (!item) return null;
|
|
37
|
+
|
|
38
|
+
// Accept both PageSectionV2 and CoverSection — both expose `columns:
|
|
39
|
+
// SectionColumn[]` and `settings.grid_columns`. Other section types
|
|
40
|
+
// (parallax, custom instance) are not column-draggable targets.
|
|
41
|
+
let columns: SectionColumn[] | undefined;
|
|
42
|
+
let gridColumns = 12;
|
|
43
|
+
if (isPageSectionV2(item)) {
|
|
44
|
+
const v2 = item as PageSectionV2;
|
|
45
|
+
columns = v2.columns;
|
|
46
|
+
gridColumns = v2.settings?.grid_columns || 12;
|
|
47
|
+
} else if (isCoverSection(item)) {
|
|
48
|
+
const cover = item as CoverSection;
|
|
49
|
+
columns = cover.columns;
|
|
50
|
+
gridColumns = cover.settings?.grid_columns || 12;
|
|
51
|
+
} else {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
24
54
|
|
|
25
|
-
const
|
|
26
|
-
const col = v2Section.columns?.find((c) => c._key === columnKey);
|
|
55
|
+
const col = columns?.find((c) => c._key === columnKey);
|
|
27
56
|
if (!col) return null;
|
|
28
57
|
|
|
29
58
|
const blockCount = (col.blocks || []).length;
|
|
30
|
-
|
|
59
|
+
|
|
60
|
+
// Pick accent based on drop validity.
|
|
61
|
+
const accentColor = isValidDrop ? BUILDER_BLUE : INVALID_RED;
|
|
62
|
+
const accentRgb = isValidDrop ? "71, 148, 226" : INVALID_RED_RGB;
|
|
31
63
|
|
|
32
64
|
const overlay = (
|
|
33
65
|
<div
|
|
@@ -38,18 +70,20 @@ const ColumnDragOverlay = memo(function ColumnDragOverlay({
|
|
|
38
70
|
transform: "translate(-50%, -50%)",
|
|
39
71
|
pointerEvents: "none",
|
|
40
72
|
zIndex: 99999,
|
|
73
|
+
transition: "filter 120ms ease",
|
|
41
74
|
}}
|
|
42
75
|
>
|
|
43
76
|
<div
|
|
44
77
|
style={{
|
|
45
78
|
width: 180,
|
|
46
79
|
minHeight: 80,
|
|
47
|
-
background:
|
|
80
|
+
background: `rgba(${accentRgb}, 0.08)`,
|
|
48
81
|
backdropFilter: "blur(8px)",
|
|
49
82
|
opacity: 0.85,
|
|
50
83
|
borderRadius: 8,
|
|
51
|
-
border: `2px solid ${
|
|
52
|
-
boxShadow:
|
|
84
|
+
border: `2px solid ${accentColor}`,
|
|
85
|
+
boxShadow: `0 8px 32px rgba(${accentRgb}, 0.3)`,
|
|
86
|
+
transition: "border-color 120ms ease, background 120ms ease, box-shadow 120ms ease",
|
|
53
87
|
}}
|
|
54
88
|
>
|
|
55
89
|
{/* Column header badge */}
|
|
@@ -59,17 +93,25 @@ const ColumnDragOverlay = memo(function ColumnDragOverlay({
|
|
|
59
93
|
alignItems: "center",
|
|
60
94
|
gap: 8,
|
|
61
95
|
padding: "8px 12px",
|
|
62
|
-
borderBottom:
|
|
96
|
+
borderBottom: `1px solid rgba(${accentRgb}, 0.2)`,
|
|
63
97
|
}}
|
|
64
98
|
>
|
|
65
|
-
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
99
|
+
{isValidDrop ? (
|
|
100
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill={accentColor}>
|
|
101
|
+
<circle cx="3" cy="3" r="1" />
|
|
102
|
+
<circle cx="7" cy="3" r="1" />
|
|
103
|
+
<circle cx="3" cy="7" r="1" />
|
|
104
|
+
<circle cx="7" cy="7" r="1" />
|
|
105
|
+
</svg>
|
|
106
|
+
) : (
|
|
107
|
+
// "Blocked" icon when invalid — slashed circle
|
|
108
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="none">
|
|
109
|
+
<circle cx="8" cy="8" r="6.5" stroke={accentColor} strokeWidth="1.5" />
|
|
110
|
+
<line x1="3.5" y1="12.5" x2="12.5" y2="3.5" stroke={accentColor} strokeWidth="1.5" />
|
|
111
|
+
</svg>
|
|
112
|
+
)}
|
|
71
113
|
<span style={{ fontSize: 12, color: "white", fontWeight: 500 }}>
|
|
72
|
-
Column {col.span}
|
|
114
|
+
{isValidDrop ? `Column ${col.span}/${gridColumns}` : "Cannot drop here"}
|
|
73
115
|
</span>
|
|
74
116
|
</div>
|
|
75
117
|
{/* Block count indicators */}
|
|
@@ -30,6 +30,11 @@ interface InsertionLinesProps {
|
|
|
30
30
|
colGap: number;
|
|
31
31
|
sectionKey: string;
|
|
32
32
|
dropTarget: DropTarget | null;
|
|
33
|
+
/** Offset added to grid_row in emitted `data-insert-row` attributes —
|
|
34
|
+
* required when this SectionV2Canvas is a virtual per-row view inside
|
|
35
|
+
* a CoverSection (rows are remapped to 1 for rendering; the real
|
|
36
|
+
* grid_row must be recovered for the DnD hit-test). Default 0. */
|
|
37
|
+
gridRowOffset?: number;
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
export const InsertionLines = memo(function InsertionLines({
|
|
@@ -40,6 +45,7 @@ export const InsertionLines = memo(function InsertionLines({
|
|
|
40
45
|
colGap,
|
|
41
46
|
sectionKey,
|
|
42
47
|
dropTarget,
|
|
48
|
+
gridRowOffset = 0,
|
|
43
49
|
}: InsertionLinesProps) {
|
|
44
50
|
// Compute insertion points between adjacent columns (no gap between them)
|
|
45
51
|
// Exclude the currently dragged column from consideration
|
|
@@ -124,7 +130,9 @@ export const InsertionLines = memo(function InsertionLines({
|
|
|
124
130
|
<div
|
|
125
131
|
data-col-v2-insert=""
|
|
126
132
|
data-section-key={sectionKey}
|
|
127
|
-
|
|
133
|
+
// Absolute grid_row (including Cover row offset) — see
|
|
134
|
+
// SectionV2Canvas's data-gap-row comment for rationale.
|
|
135
|
+
data-insert-row={pt.row + gridRowOffset}
|
|
128
136
|
data-insert-col={pt.gridColumn}
|
|
129
137
|
data-insert-left-key={pt.leftColKey}
|
|
130
138
|
data-insert-right-key={pt.rightColKey}
|
|
@@ -69,8 +69,13 @@ export default function SectionV2Canvas({
|
|
|
69
69
|
startDrag,
|
|
70
70
|
} = useColumnDragContext();
|
|
71
71
|
|
|
72
|
-
//
|
|
73
|
-
|
|
72
|
+
// Show insertion lines + highlighted gaps:
|
|
73
|
+
// - While dragging, in the section the drag ORIGINATED from (source feedback)
|
|
74
|
+
// - While dragging, in ANY section the cursor is currently over (target feedback
|
|
75
|
+
// for cross-section drops — the user needs a visible drop zone in the target).
|
|
76
|
+
const showInsertionLines =
|
|
77
|
+
isColDragActive &&
|
|
78
|
+
(draggingSectionKey === section._key || isSectionHovered);
|
|
74
79
|
|
|
75
80
|
// When drag ends (isColDragActive becomes false), reset hover state so it
|
|
76
81
|
// doesn't stay stuck true if the pointer ended outside the grid.
|
|
@@ -267,7 +272,11 @@ export default function SectionV2Canvas({
|
|
|
267
272
|
key={`gap-${gap.grid_row}-${gap.grid_column}`}
|
|
268
273
|
data-col-v2-gap=""
|
|
269
274
|
data-section-key={section._key}
|
|
270
|
-
|
|
275
|
+
// Absolute grid_row (including Cover row offset) — used by the
|
|
276
|
+
// DnD hit-test. Without the offset, Cover drops in row 2/3 would
|
|
277
|
+
// collapse to row 1. The `onClick` below applies the same offset
|
|
278
|
+
// to keep "+ Add Column" consistent with drag drops.
|
|
279
|
+
data-gap-row={gap.grid_row + gridRowOffset}
|
|
271
280
|
data-gap-col={gap.grid_column}
|
|
272
281
|
data-gap-span={gap.span}
|
|
273
282
|
onClick={(e) => {
|
|
@@ -304,6 +313,7 @@ export default function SectionV2Canvas({
|
|
|
304
313
|
colGap={colGap}
|
|
305
314
|
sectionKey={section._key}
|
|
306
315
|
dropTarget={dropTarget}
|
|
316
|
+
gridRowOffset={gridRowOffset}
|
|
307
317
|
/>
|
|
308
318
|
)}
|
|
309
319
|
</div>
|