@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.
@@ -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 section = rows.find((r) => r._key === sectionKey);
61
- if (!section || !isPageSectionV2(section)) return;
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 section = rows.find((r) => r._key === sectionKey);
113
- if (!section || !isPageSectionV2(section)) return;
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 section = rows.find((r) => r._key === sectionKey);
151
- if (!section || !isPageSectionV2(section)) return;
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 elementsFromPoint
247
- const elements = document.elementsFromPoint(e.clientX, e.clientY);
248
- let newTarget: DropTarget | null = null;
249
- let newInsert: InsertBetween | null = null;
250
-
251
- for (const el of elements) {
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 elements = document.elementsFromPoint(e.clientX, e.clientY);
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
- if (finalTarget) {
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.type === "swap" && finalTarget.columnKey) {
380
- if (!isResponsive) {
381
- actions.swapColumnV2(sectionKey, columnKey, finalTarget.columnKey);
382
- } else {
383
- executeResponsiveSwap(
384
- sectionKey, columnKey, finalTarget.columnKey, activeViewport,
385
- actions.updateSectionV2Responsive
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
- } else if (finalTarget.type === "gap") {
389
- if (!isResponsive) {
390
- actions.moveColumnToGapV2(
391
- sectionKey, columnKey,
392
- finalTarget.gapRow!, finalTarget.gapCol!, finalTarget.gapSpan!
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
- executeResponsiveGapMove(
396
- sectionKey, columnKey,
397
- finalTarget.gapRow!, finalTarget.gapCol!, finalTarget.gapSpan!,
398
- activeViewport,
399
- actions.updateSectionV2Responsive
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 if (finalTarget.type === "insert") {
403
- if (!isResponsive) {
404
- actions.moveColumnV2(
405
- sectionKey, columnKey,
406
- finalTarget.insertRow!, finalTarget.insertCol!
407
- );
408
- } else {
409
- executeResponsiveInsert(
410
- sectionKey, columnKey,
411
- finalTarget.insertRow!, finalTarget.insertCol!,
412
- activeViewport,
413
- actions.updateSectionV2Responsive
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();