@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
|
@@ -2,12 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useCallback, useRef, useEffect } from "react";
|
|
4
4
|
import { useBuilderStore } from "../../../lib/builder/store";
|
|
5
|
-
import type { PageSectionV2 } from "../../../lib/sanity/types";
|
|
6
|
-
import { isPageSectionV2 } from "../../../lib/sanity/types";
|
|
5
|
+
import type { PageSectionV2, CoverSection, ContentItem } from "../../../lib/sanity/types";
|
|
6
|
+
import { isPageSectionV2, isCoverSection, isColumnarSection } from "../../../lib/sanity/types";
|
|
7
7
|
import { getEffectiveColumnsV2, buildColumnV2Overrides } from "../settings-panel/responsive-helpers";
|
|
8
8
|
import { moveColumn as cascadeMoveColumn } from "../../../lib/builder/cascade";
|
|
9
9
|
import type { DeviceViewport } from "../../../lib/builder/types";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* View a Cover section as a PageSectionV2 for DnD purposes.
|
|
13
|
+
*
|
|
14
|
+
* Cover and V2 share the same column shape, grid_columns, and
|
|
15
|
+
* `responsive[vp].columns` shape — the fields read/written by column
|
|
16
|
+
* DnD. This shim lets us reuse getEffectiveColumnsV2/buildColumnV2Overrides
|
|
17
|
+
* without duplicating logic. Only `columns`, `settings.grid_columns`, and
|
|
18
|
+
* `responsive[vp].columns` are exposed; Cover-specific fields
|
|
19
|
+
* (`cover_rows`, Cover-specific `settings`) are hidden from the V2 helpers.
|
|
20
|
+
*
|
|
21
|
+
* The write-back path (`updateSectionV2Responsive` → `updateSectionAtPath`)
|
|
22
|
+
* merges column overrides back into the cover, preserving Cover-specific
|
|
23
|
+
* fields. See `store-helpers.ts :: updateSectionAtPath` cover branch.
|
|
24
|
+
*/
|
|
25
|
+
function coverAsV2(cover: CoverSection): PageSectionV2 {
|
|
26
|
+
return {
|
|
27
|
+
_type: "pageSectionV2",
|
|
28
|
+
_key: cover._key,
|
|
29
|
+
section_type: "empty-v2",
|
|
30
|
+
columns: cover.columns,
|
|
31
|
+
settings: {
|
|
32
|
+
preset: "custom",
|
|
33
|
+
grid_columns: cover.settings.grid_columns,
|
|
34
|
+
col_gap: cover.settings.col_gap,
|
|
35
|
+
row_gap: cover.settings.row_gap,
|
|
36
|
+
},
|
|
37
|
+
responsive:
|
|
38
|
+
cover.responsive?.tablet?.columns || cover.responsive?.phone?.columns
|
|
39
|
+
? {
|
|
40
|
+
...(cover.responsive.tablet?.columns
|
|
41
|
+
? { tablet: { columns: cover.responsive.tablet.columns } }
|
|
42
|
+
: {}),
|
|
43
|
+
...(cover.responsive.phone?.columns
|
|
44
|
+
? { phone: { columns: cover.responsive.phone.columns } }
|
|
45
|
+
: {}),
|
|
46
|
+
}
|
|
47
|
+
: undefined,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Find the section by key and return a V2-shaped view of it (PageSectionV2
|
|
53
|
+
* directly, or Cover adapted via `coverAsV2`). Returns null if the section
|
|
54
|
+
* doesn't exist or is an unsupported type (parallax, custom instance).
|
|
55
|
+
*/
|
|
56
|
+
function findColumnarSection(
|
|
57
|
+
rows: ContentItem[],
|
|
58
|
+
sectionKey: string,
|
|
59
|
+
): PageSectionV2 | null {
|
|
60
|
+
const section = rows.find((r) => r._key === sectionKey);
|
|
61
|
+
if (!section) return null;
|
|
62
|
+
if (isPageSectionV2(section)) return section as PageSectionV2;
|
|
63
|
+
if (isCoverSection(section)) return coverAsV2(section as CoverSection);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
11
67
|
// ============================================
|
|
12
68
|
// Types
|
|
13
69
|
// ============================================
|
|
@@ -24,6 +80,22 @@ export interface DropTarget {
|
|
|
24
80
|
/** insert: grid position where the column will be inserted */
|
|
25
81
|
insertRow?: number;
|
|
26
82
|
insertCol?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Whether the drop is to a DIFFERENT section than the source.
|
|
85
|
+
* - false → same-section drag (existing behavior).
|
|
86
|
+
* - true → cross-section move (must land on V2 or Cover; swap
|
|
87
|
+
* across sections is not supported and is reported as
|
|
88
|
+
* isValid=false below).
|
|
89
|
+
*/
|
|
90
|
+
isCrossSection: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Whether the drop is legal. Set to false when:
|
|
93
|
+
* - target section is not a columnar section (parallax, custom-instance)
|
|
94
|
+
* - cross-section swap is attempted (only gap/insert cross sections)
|
|
95
|
+
* When false, the drop action is NOT executed on mouseup and the
|
|
96
|
+
* overlay renders in "invalid" (red) state.
|
|
97
|
+
*/
|
|
98
|
+
isValid: boolean;
|
|
27
99
|
}
|
|
28
100
|
|
|
29
101
|
export interface InsertBetween {
|
|
@@ -41,6 +113,103 @@ export interface UseColumnDragReturn {
|
|
|
41
113
|
startDrag: (e: React.MouseEvent, sectionKey: string, columnKey: string) => void;
|
|
42
114
|
}
|
|
43
115
|
|
|
116
|
+
// ============================================
|
|
117
|
+
// Hit-test (shared by mousemove and mouseup)
|
|
118
|
+
// ============================================
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Given a pointer event, resolve the topmost drop target under the cursor.
|
|
122
|
+
*
|
|
123
|
+
* - Priority order: Insert zone > Gap > Column (swap)
|
|
124
|
+
* - Cross-section drops are ALLOWED for Insert and Gap, but only if the
|
|
125
|
+
* target section is columnar (V2 or Cover). Non-columnar targets
|
|
126
|
+
* (parallax slide columns, custom instances) produce a target with
|
|
127
|
+
* `isValid: false` so the caller can show a red overlay.
|
|
128
|
+
* - Cross-section Swap is explicitly rejected (`isValid: false`). Swapping
|
|
129
|
+
* across sections is ambiguous UX; the user can drag into a gap/insert
|
|
130
|
+
* instead.
|
|
131
|
+
*/
|
|
132
|
+
function hitTestDrop(
|
|
133
|
+
e: MouseEvent,
|
|
134
|
+
sourceSectionKey: string,
|
|
135
|
+
sourceColumnKey: string,
|
|
136
|
+
): { target: DropTarget | null; insert: InsertBetween | null } {
|
|
137
|
+
const rows = useBuilderStore.getState().rows;
|
|
138
|
+
const isTargetColumnar = (sectionKey: string): boolean => {
|
|
139
|
+
const section = rows.find((r) => r._key === sectionKey);
|
|
140
|
+
return section ? isColumnarSection(section) : false;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const elements = document.elementsFromPoint(e.clientX, e.clientY);
|
|
144
|
+
for (const el of elements) {
|
|
145
|
+
const htmlEl = el as HTMLElement;
|
|
146
|
+
|
|
147
|
+
// Priority 1: Insert zone (between two columns, or at start/end of a row)
|
|
148
|
+
if (htmlEl.dataset.colV2Insert !== undefined) {
|
|
149
|
+
const ts = htmlEl.dataset.sectionKey!;
|
|
150
|
+
const isCross = ts !== sourceSectionKey;
|
|
151
|
+
const isValid = isTargetColumnar(ts);
|
|
152
|
+
const target: DropTarget = {
|
|
153
|
+
type: "insert",
|
|
154
|
+
sectionKey: ts,
|
|
155
|
+
insertRow: parseInt(htmlEl.dataset.insertRow!, 10),
|
|
156
|
+
insertCol: parseInt(htmlEl.dataset.insertCol!, 10),
|
|
157
|
+
isCrossSection: isCross,
|
|
158
|
+
isValid,
|
|
159
|
+
};
|
|
160
|
+
const leftKey = htmlEl.dataset.insertLeftKey;
|
|
161
|
+
const rightKey = htmlEl.dataset.insertRightKey;
|
|
162
|
+
const insert: InsertBetween | null =
|
|
163
|
+
leftKey && rightKey ? { leftKey, rightKey } : null;
|
|
164
|
+
return { target, insert };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Priority 2: Gap (empty grid cell)
|
|
168
|
+
if (htmlEl.dataset.colV2Gap !== undefined) {
|
|
169
|
+
const ts = htmlEl.dataset.sectionKey!;
|
|
170
|
+
const isCross = ts !== sourceSectionKey;
|
|
171
|
+
const isValid = isTargetColumnar(ts);
|
|
172
|
+
return {
|
|
173
|
+
target: {
|
|
174
|
+
type: "gap",
|
|
175
|
+
sectionKey: ts,
|
|
176
|
+
gapRow: parseInt(htmlEl.dataset.gapRow!, 10),
|
|
177
|
+
gapCol: parseInt(htmlEl.dataset.gapCol!, 10),
|
|
178
|
+
gapSpan: parseInt(htmlEl.dataset.gapSpan!, 10),
|
|
179
|
+
isCrossSection: isCross,
|
|
180
|
+
isValid,
|
|
181
|
+
},
|
|
182
|
+
insert: null,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Priority 3: Column (swap). Valid both same-section AND cross-section
|
|
187
|
+
// (as long as target is a columnar section — V2 or Cover). Cross-section
|
|
188
|
+
// swap routes to `swapColumnsBetweenSections` at drop time; same-section
|
|
189
|
+
// swap routes to `swapColumnV2`.
|
|
190
|
+
if (htmlEl.dataset.colV2Droptarget !== undefined) {
|
|
191
|
+
const ts = htmlEl.dataset.sectionKey!;
|
|
192
|
+
const tc = htmlEl.dataset.columnKey!;
|
|
193
|
+
// Skip if hovering over self (within same section)
|
|
194
|
+
if (ts === sourceSectionKey && tc === sourceColumnKey) continue;
|
|
195
|
+
const isCross = ts !== sourceSectionKey;
|
|
196
|
+
const isValid = isTargetColumnar(ts);
|
|
197
|
+
return {
|
|
198
|
+
target: {
|
|
199
|
+
type: "swap",
|
|
200
|
+
sectionKey: ts,
|
|
201
|
+
columnKey: tc,
|
|
202
|
+
isCrossSection: isCross,
|
|
203
|
+
isValid,
|
|
204
|
+
},
|
|
205
|
+
insert: null,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { target: null, insert: null };
|
|
211
|
+
}
|
|
212
|
+
|
|
44
213
|
// ============================================
|
|
45
214
|
// Responsive Helper Functions (private)
|
|
46
215
|
// ============================================
|
|
@@ -57,9 +226,8 @@ function executeResponsiveSwap(
|
|
|
57
226
|
updateSectionV2Responsive: (sectionKey: string, responsive: PageSectionV2["responsive"]) => void
|
|
58
227
|
): void {
|
|
59
228
|
const rows = useBuilderStore.getState().rows;
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
62
|
-
const v2Section = section as PageSectionV2;
|
|
229
|
+
const v2Section = findColumnarSection(rows, sectionKey);
|
|
230
|
+
if (!v2Section) return;
|
|
63
231
|
|
|
64
232
|
const effectiveCols = getEffectiveColumnsV2(v2Section, viewport);
|
|
65
233
|
const draggedCol = effectiveCols.find((c) => c._key === draggedKey);
|
|
@@ -109,9 +277,8 @@ function executeResponsiveGapMove(
|
|
|
109
277
|
updateSectionV2Responsive: (sectionKey: string, responsive: PageSectionV2["responsive"]) => void
|
|
110
278
|
): void {
|
|
111
279
|
const rows = useBuilderStore.getState().rows;
|
|
112
|
-
const
|
|
113
|
-
if (!
|
|
114
|
-
const v2Section = section as PageSectionV2;
|
|
280
|
+
const v2Section = findColumnarSection(rows, sectionKey);
|
|
281
|
+
if (!v2Section) return;
|
|
115
282
|
|
|
116
283
|
const effectiveCols = getEffectiveColumnsV2(v2Section, viewport);
|
|
117
284
|
const columnOverrides = effectiveCols.map((c) => {
|
|
@@ -147,9 +314,8 @@ function executeResponsiveInsert(
|
|
|
147
314
|
updateSectionV2Responsive: (sectionKey: string, responsive: PageSectionV2["responsive"]) => void
|
|
148
315
|
): void {
|
|
149
316
|
const rows = useBuilderStore.getState().rows;
|
|
150
|
-
const
|
|
151
|
-
if (!
|
|
152
|
-
const v2Section = section as PageSectionV2;
|
|
317
|
+
const v2Section = findColumnarSection(rows, sectionKey);
|
|
318
|
+
if (!v2Section) return;
|
|
153
319
|
|
|
154
320
|
const effectiveCols = getEffectiveColumnsV2(v2Section, viewport);
|
|
155
321
|
const cascadeResult = cascadeMoveColumn(effectiveCols, columnKey, targetRow, targetCol, v2Section.settings.grid_columns);
|
|
@@ -199,6 +365,8 @@ export function useColumnDrag(): UseColumnDragReturn {
|
|
|
199
365
|
swapColumnV2: s.swapColumnV2,
|
|
200
366
|
moveColumnToGapV2: s.moveColumnToGapV2,
|
|
201
367
|
moveColumnV2: s.moveColumnV2,
|
|
368
|
+
moveColumnBetweenSections: s.moveColumnBetweenSections,
|
|
369
|
+
swapColumnsBetweenSections: s.swapColumnsBetweenSections,
|
|
202
370
|
updateSectionV2Responsive: s.updateSectionV2Responsive,
|
|
203
371
|
};
|
|
204
372
|
};
|
|
@@ -243,60 +411,12 @@ export function useColumnDrag(): UseColumnDragReturn {
|
|
|
243
411
|
|
|
244
412
|
const { sectionKey, columnKey } = dragRef.current;
|
|
245
413
|
|
|
246
|
-
// 2. Hit-test via
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const htmlEl = el as HTMLElement;
|
|
253
|
-
|
|
254
|
-
// Priority 1: Insert zone
|
|
255
|
-
if (htmlEl.dataset.colV2Insert !== undefined) {
|
|
256
|
-
const targetSection = htmlEl.dataset.sectionKey;
|
|
257
|
-
if (targetSection !== sectionKey) continue;
|
|
258
|
-
newTarget = {
|
|
259
|
-
type: "insert",
|
|
260
|
-
sectionKey: targetSection,
|
|
261
|
-
insertRow: parseInt(htmlEl.dataset.insertRow!, 10),
|
|
262
|
-
insertCol: parseInt(htmlEl.dataset.insertCol!, 10),
|
|
263
|
-
};
|
|
264
|
-
const leftKey = htmlEl.dataset.insertLeftKey;
|
|
265
|
-
const rightKey = htmlEl.dataset.insertRightKey;
|
|
266
|
-
if (leftKey && rightKey) {
|
|
267
|
-
newInsert = { leftKey, rightKey };
|
|
268
|
-
}
|
|
269
|
-
break;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Priority 2: Gap
|
|
273
|
-
if (htmlEl.dataset.colV2Gap !== undefined) {
|
|
274
|
-
const targetSection = htmlEl.dataset.sectionKey;
|
|
275
|
-
if (targetSection !== sectionKey) continue;
|
|
276
|
-
newTarget = {
|
|
277
|
-
type: "gap",
|
|
278
|
-
sectionKey: targetSection,
|
|
279
|
-
gapRow: parseInt(htmlEl.dataset.gapRow!, 10),
|
|
280
|
-
gapCol: parseInt(htmlEl.dataset.gapCol!, 10),
|
|
281
|
-
gapSpan: parseInt(htmlEl.dataset.gapSpan!, 10),
|
|
282
|
-
};
|
|
283
|
-
break;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Priority 3: Column (swap)
|
|
287
|
-
if (htmlEl.dataset.colV2Droptarget !== undefined) {
|
|
288
|
-
const targetSection = htmlEl.dataset.sectionKey;
|
|
289
|
-
const targetColumn = htmlEl.dataset.columnKey;
|
|
290
|
-
if (targetSection !== sectionKey) continue;
|
|
291
|
-
if (targetColumn === columnKey) continue; // skip self
|
|
292
|
-
newTarget = {
|
|
293
|
-
type: "swap",
|
|
294
|
-
sectionKey: targetSection,
|
|
295
|
-
columnKey: targetColumn,
|
|
296
|
-
};
|
|
297
|
-
break;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
414
|
+
// 2. Hit-test via shared helper (returns DropTarget with isValid/isCrossSection)
|
|
415
|
+
const { target: newTarget, insert: newInsert } = hitTestDrop(
|
|
416
|
+
e,
|
|
417
|
+
sectionKey,
|
|
418
|
+
columnKey,
|
|
419
|
+
);
|
|
300
420
|
|
|
301
421
|
setDropTarget(newTarget);
|
|
302
422
|
setInsertBetween(newTarget?.type === "insert" ? newInsert : null);
|
|
@@ -325,93 +445,100 @@ export function useColumnDrag(): UseColumnDragReturn {
|
|
|
325
445
|
const { sectionKey, columnKey } = dragRef.current;
|
|
326
446
|
dragRef.current.active = false;
|
|
327
447
|
|
|
328
|
-
// Final hit-test at mouseup position
|
|
329
|
-
const
|
|
330
|
-
let finalTarget: DropTarget | null = null;
|
|
331
|
-
|
|
332
|
-
for (const el of elements) {
|
|
333
|
-
const htmlEl = el as HTMLElement;
|
|
334
|
-
|
|
335
|
-
// Priority 1: Insert zone
|
|
336
|
-
if (htmlEl.dataset.colV2Insert !== undefined) {
|
|
337
|
-
const ts = htmlEl.dataset.sectionKey;
|
|
338
|
-
if (ts !== sectionKey) continue;
|
|
339
|
-
finalTarget = {
|
|
340
|
-
type: "insert",
|
|
341
|
-
sectionKey: ts,
|
|
342
|
-
insertRow: parseInt(htmlEl.dataset.insertRow!, 10),
|
|
343
|
-
insertCol: parseInt(htmlEl.dataset.insertCol!, 10),
|
|
344
|
-
};
|
|
345
|
-
break;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Priority 2: Gap
|
|
349
|
-
if (htmlEl.dataset.colV2Gap !== undefined) {
|
|
350
|
-
const ts = htmlEl.dataset.sectionKey;
|
|
351
|
-
if (ts !== sectionKey) continue;
|
|
352
|
-
finalTarget = {
|
|
353
|
-
type: "gap",
|
|
354
|
-
sectionKey: ts,
|
|
355
|
-
gapRow: parseInt(htmlEl.dataset.gapRow!, 10),
|
|
356
|
-
gapCol: parseInt(htmlEl.dataset.gapCol!, 10),
|
|
357
|
-
gapSpan: parseInt(htmlEl.dataset.gapSpan!, 10),
|
|
358
|
-
};
|
|
359
|
-
break;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Priority 3: Column (swap)
|
|
363
|
-
if (htmlEl.dataset.colV2Droptarget !== undefined) {
|
|
364
|
-
const ts = htmlEl.dataset.sectionKey;
|
|
365
|
-
const tc = htmlEl.dataset.columnKey;
|
|
366
|
-
if (ts !== sectionKey || tc === columnKey) continue;
|
|
367
|
-
finalTarget = { type: "swap", sectionKey: ts, columnKey: tc };
|
|
368
|
-
break;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
448
|
+
// Final hit-test at mouseup position (shared with mousemove)
|
|
449
|
+
const { target: finalTarget } = hitTestDrop(e, sectionKey, columnKey);
|
|
371
450
|
|
|
372
|
-
// Execute the drop action — read actions fresh from store (RC-002)
|
|
373
|
-
|
|
451
|
+
// Execute the drop action — read actions fresh from store (RC-002).
|
|
452
|
+
// Only valid targets execute; invalid (e.g. cross-section swap, drop
|
|
453
|
+
// on parallax) fall through silently — overlay already showed red.
|
|
454
|
+
if (finalTarget && finalTarget.isValid) {
|
|
374
455
|
const storeState = useBuilderStore.getState();
|
|
375
456
|
const activeViewport = storeState.activeViewport;
|
|
376
457
|
const isResponsive = activeViewport !== "desktop";
|
|
377
458
|
const actions = getActions();
|
|
378
459
|
|
|
379
|
-
if (finalTarget.
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
460
|
+
if (finalTarget.isCrossSection) {
|
|
461
|
+
// Cross-section drops supported:
|
|
462
|
+
// - gap / insert → `moveColumnBetweenSections`
|
|
463
|
+
// - swap → `swapColumnsBetweenSections` (each column adopts the
|
|
464
|
+
// other's position/row/span, same semantic as intra-section swap)
|
|
465
|
+
// Responsive cross-section moves are not yet supported in this
|
|
466
|
+
// first iteration — treat as desktop move (span/col clamped by
|
|
467
|
+
// helpers). The drop is still written to the Sanity doc.
|
|
468
|
+
if (finalTarget.type === "gap") {
|
|
469
|
+
actions.moveColumnBetweenSections(
|
|
470
|
+
sectionKey,
|
|
471
|
+
columnKey,
|
|
472
|
+
finalTarget.sectionKey,
|
|
473
|
+
finalTarget.gapRow!,
|
|
474
|
+
finalTarget.gapCol!,
|
|
475
|
+
finalTarget.gapSpan!,
|
|
386
476
|
);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
477
|
+
} else if (finalTarget.type === "insert") {
|
|
478
|
+
// For insert drops, preserve the source column's span. The helper
|
|
479
|
+
// clamps to target's grid if needed.
|
|
480
|
+
const sourceCol = useBuilderStore
|
|
481
|
+
.getState()
|
|
482
|
+
.rows.find((r) => r._key === sectionKey);
|
|
483
|
+
const sourceSpan =
|
|
484
|
+
sourceCol && isColumnarSection(sourceCol)
|
|
485
|
+
? sourceCol.columns.find((c) => c._key === columnKey)?.span ?? 12
|
|
486
|
+
: 12;
|
|
487
|
+
actions.moveColumnBetweenSections(
|
|
488
|
+
sectionKey,
|
|
489
|
+
columnKey,
|
|
490
|
+
finalTarget.sectionKey,
|
|
491
|
+
finalTarget.insertRow!,
|
|
492
|
+
finalTarget.insertCol!,
|
|
493
|
+
sourceSpan,
|
|
393
494
|
);
|
|
394
|
-
} else {
|
|
395
|
-
|
|
396
|
-
sectionKey,
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
495
|
+
} else if (finalTarget.type === "swap" && finalTarget.columnKey) {
|
|
496
|
+
actions.swapColumnsBetweenSections(
|
|
497
|
+
sectionKey,
|
|
498
|
+
columnKey,
|
|
499
|
+
finalTarget.sectionKey,
|
|
500
|
+
finalTarget.columnKey,
|
|
400
501
|
);
|
|
401
502
|
}
|
|
402
|
-
} else
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
)
|
|
503
|
+
} else {
|
|
504
|
+
// Same-section — existing behavior preserved exactly
|
|
505
|
+
if (finalTarget.type === "swap" && finalTarget.columnKey) {
|
|
506
|
+
if (!isResponsive) {
|
|
507
|
+
actions.swapColumnV2(sectionKey, columnKey, finalTarget.columnKey);
|
|
508
|
+
} else {
|
|
509
|
+
executeResponsiveSwap(
|
|
510
|
+
sectionKey, columnKey, finalTarget.columnKey, activeViewport,
|
|
511
|
+
actions.updateSectionV2Responsive
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
} else if (finalTarget.type === "gap") {
|
|
515
|
+
if (!isResponsive) {
|
|
516
|
+
actions.moveColumnToGapV2(
|
|
517
|
+
sectionKey, columnKey,
|
|
518
|
+
finalTarget.gapRow!, finalTarget.gapCol!, finalTarget.gapSpan!
|
|
519
|
+
);
|
|
520
|
+
} else {
|
|
521
|
+
executeResponsiveGapMove(
|
|
522
|
+
sectionKey, columnKey,
|
|
523
|
+
finalTarget.gapRow!, finalTarget.gapCol!, finalTarget.gapSpan!,
|
|
524
|
+
activeViewport,
|
|
525
|
+
actions.updateSectionV2Responsive
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
} else if (finalTarget.type === "insert") {
|
|
529
|
+
if (!isResponsive) {
|
|
530
|
+
actions.moveColumnV2(
|
|
531
|
+
sectionKey, columnKey,
|
|
532
|
+
finalTarget.insertRow!, finalTarget.insertCol!
|
|
533
|
+
);
|
|
534
|
+
} else {
|
|
535
|
+
executeResponsiveInsert(
|
|
536
|
+
sectionKey, columnKey,
|
|
537
|
+
finalTarget.insertRow!, finalTarget.insertCol!,
|
|
538
|
+
activeViewport,
|
|
539
|
+
actions.updateSectionV2Responsive
|
|
540
|
+
);
|
|
541
|
+
}
|
|
415
542
|
}
|
|
416
543
|
}
|
|
417
544
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BuilderStore, BuilderState } from "./types";
|
|
1
|
+
import type { BuilderStore, BuilderState, BlockSliceActions } from "./types";
|
|
2
2
|
import type { ContentBlock, ContentItem, PageSectionV2, ParallaxGroup, CoverSection } from "../../lib/sanity/types";
|
|
3
3
|
import { isPageSectionV2, isParallaxGroup, isCoverSection } from "../../lib/sanity/types";
|
|
4
4
|
import { createDefaultBlock } from "./defaults";
|
|
@@ -47,7 +47,7 @@ function applyBlockUpdate(
|
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
export function createBlockActions(set: StoreSet, get: StoreGet) {
|
|
50
|
+
export function createBlockActions(set: StoreSet, get: StoreGet): BlockSliceActions {
|
|
51
51
|
return {
|
|
52
52
|
// ---- Block operations ----
|
|
53
53
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BuilderStore, BuilderState, CanvasTool, DeviceViewport, PageSettings } from "./types";
|
|
1
|
+
import type { BuilderStore, BuilderState, CanvasTool, DeviceViewport, PageSettings, CanvasSliceActions } from "./types";
|
|
2
2
|
import { DEFAULT_PAGE_SETTINGS, DEFAULT_GRID_SETTINGS, DEVICE_WIDTHS } from "./types";
|
|
3
3
|
import type { PageSectionV2 } from "../../lib/sanity/types";
|
|
4
4
|
import { isPageSectionV2 } from "../../lib/sanity/types";
|
|
@@ -11,7 +11,7 @@ type StoreSet = (
|
|
|
11
11
|
) => void;
|
|
12
12
|
type StoreGet = () => BuilderStore;
|
|
13
13
|
|
|
14
|
-
export function createCanvasActions(set: StoreSet, get: StoreGet) {
|
|
14
|
+
export function createCanvasActions(set: StoreSet, get: StoreGet): CanvasSliceActions {
|
|
15
15
|
return {
|
|
16
16
|
// ---- Editor mode ----
|
|
17
17
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Session 176: Cover Sections — Phase 3 (Store).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type { BuilderState } from "./types";
|
|
12
|
+
import type { BuilderState, CoverSliceActions } from "./types";
|
|
13
13
|
import type {
|
|
14
14
|
ContentItem,
|
|
15
15
|
CoverSection,
|
|
@@ -79,7 +79,7 @@ function updateCoverInRows(
|
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
export function createCoverActions(set: StoreSet, get: StoreGet) {
|
|
82
|
+
export function createCoverActions(set: StoreSet, get: StoreGet): CoverSliceActions {
|
|
83
83
|
return {
|
|
84
84
|
addCoverSection: (afterRowKey?: string | null): void => {
|
|
85
85
|
get()._pushSnapshot();
|