@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 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
- - **7 Content Blocks** — Text, Image, Image Grid, Video, Spacer, Button, Project Grid
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 || !isPageSectionV2(item)) return null;
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 v2Section = item as PageSectionV2;
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
- const gridColumns = v2Section.settings.grid_columns || 12;
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: "rgba(71, 148, 226, 0.08)",
80
+ background: `rgba(${accentRgb}, 0.08)`,
48
81
  backdropFilter: "blur(8px)",
49
82
  opacity: 0.85,
50
83
  borderRadius: 8,
51
- border: `2px solid ${BUILDER_BLUE}`,
52
- boxShadow: "0 8px 32px rgba(71, 148, 226, 0.3)",
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: "1px solid rgba(71, 148, 226, 0.2)",
96
+ borderBottom: `1px solid rgba(${accentRgb}, 0.2)`,
63
97
  }}
64
98
  >
65
- <svg width="10" height="10" viewBox="0 0 10 10" fill={BUILDER_BLUE}>
66
- <circle cx="3" cy="3" r="1" />
67
- <circle cx="7" cy="3" r="1" />
68
- <circle cx="3" cy="7" r="1" />
69
- <circle cx="7" cy="7" r="1" />
70
- </svg>
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}/{gridColumns}
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
- data-insert-row={pt.row}
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
- // Only show insertion lines if the drag originates from THIS section
73
- const showInsertionLines = isColDragActive && draggingSectionKey === section._key;
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
- data-gap-row={gap.grid_row}
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>