@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
|
@@ -16,10 +16,16 @@ import type {
|
|
|
16
16
|
SectionColumn,
|
|
17
17
|
SectionV2Preset,
|
|
18
18
|
CoverSection,
|
|
19
|
+
CoverSectionResponsiveOverride,
|
|
19
20
|
} from "../../lib/sanity/types";
|
|
20
21
|
import { isPageSectionV2, isParallaxGroup, isCoverSection } from "../../lib/sanity/types";
|
|
21
22
|
import { columnsFromPreset, detectPreset } from "./cascade";
|
|
22
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
resizeColumnLeft as cascadeResizeLeft,
|
|
25
|
+
moveColumn as cascadeMoveColumn,
|
|
26
|
+
addColumn as cascadeAddColumn,
|
|
27
|
+
type ResizeLeftResult,
|
|
28
|
+
} from "./cascade";
|
|
23
29
|
import { applyBlocksToColumns, toCascadeColumns, type CascadeColumn } from "./cascade-helpers";
|
|
24
30
|
import { generateKey } from "./utils";
|
|
25
31
|
import { createDefaultParallaxSlide } from "./defaults";
|
|
@@ -480,6 +486,25 @@ export function updateSectionAtPath(
|
|
|
480
486
|
return rows.map((item, i) => {
|
|
481
487
|
if (i !== path.index || !isCoverSection(item)) return item;
|
|
482
488
|
const cover = item as CoverSection;
|
|
489
|
+
|
|
490
|
+
// Passthrough of `responsive.tablet/phone.columns` only — V2 operations
|
|
491
|
+
// (column DnD, resize) read and write this field, and its shape
|
|
492
|
+
// (`ColumnOverride[]`) is identical between V2 and Cover. We do NOT
|
|
493
|
+
// expose Cover-specific fields (`cover_rows`, Cover `settings`) to the
|
|
494
|
+
// updater via virtualSection.responsive — those stay on the cover
|
|
495
|
+
// unchanged. This keeps the virtualSection's shape a true V2.
|
|
496
|
+
const virtualResponsive: PageSectionV2["responsive"] =
|
|
497
|
+
cover.responsive?.tablet?.columns || cover.responsive?.phone?.columns
|
|
498
|
+
? {
|
|
499
|
+
...(cover.responsive.tablet?.columns
|
|
500
|
+
? { tablet: { columns: cover.responsive.tablet.columns } }
|
|
501
|
+
: {}),
|
|
502
|
+
...(cover.responsive.phone?.columns
|
|
503
|
+
? { phone: { columns: cover.responsive.phone.columns } }
|
|
504
|
+
: {}),
|
|
505
|
+
}
|
|
506
|
+
: undefined;
|
|
507
|
+
|
|
483
508
|
const virtualSection: PageSectionV2 = {
|
|
484
509
|
_type: "pageSectionV2",
|
|
485
510
|
_key: cover._key,
|
|
@@ -497,11 +522,34 @@ export function updateSectionAtPath(
|
|
|
497
522
|
enter_animation: cover.settings.enter_animation,
|
|
498
523
|
stagger: cover.settings.stagger,
|
|
499
524
|
},
|
|
525
|
+
responsive: virtualResponsive,
|
|
500
526
|
};
|
|
501
527
|
const updated = updater(virtualSection);
|
|
528
|
+
|
|
529
|
+
// Merge updated `responsive.columns` back into cover.responsive,
|
|
530
|
+
// preserving `cover_rows` and Cover-specific `settings` per viewport.
|
|
531
|
+
const mergedResponsive: CoverSection["responsive"] = {};
|
|
532
|
+
for (const vp of ["tablet", "phone"] as const) {
|
|
533
|
+
const existing: CoverSectionResponsiveOverride = {
|
|
534
|
+
...(cover.responsive?.[vp] || {}),
|
|
535
|
+
};
|
|
536
|
+
const updatedCols = updated.responsive?.[vp]?.columns;
|
|
537
|
+
if (updatedCols === undefined) {
|
|
538
|
+
delete existing.columns;
|
|
539
|
+
} else {
|
|
540
|
+
existing.columns = updatedCols;
|
|
541
|
+
}
|
|
542
|
+
if (existing.columns || existing.cover_rows || existing.settings) {
|
|
543
|
+
mergedResponsive[vp] = existing;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
502
547
|
return {
|
|
503
548
|
...cover,
|
|
504
549
|
columns: updated.columns,
|
|
550
|
+
responsive: Object.keys(mergedResponsive).length
|
|
551
|
+
? mergedResponsive
|
|
552
|
+
: undefined,
|
|
505
553
|
settings: {
|
|
506
554
|
...cover.settings,
|
|
507
555
|
grid_columns: updated.settings.grid_columns,
|
|
@@ -582,3 +630,299 @@ export function addParallaxGroupInState(
|
|
|
582
630
|
|
|
583
631
|
return { rows: newRows, newGroupKey: newGroup._key };
|
|
584
632
|
}
|
|
633
|
+
|
|
634
|
+
// ============================================
|
|
635
|
+
// moveColumnBetweenSectionsInState (Session 183 — cross-section column DnD)
|
|
636
|
+
// ============================================
|
|
637
|
+
|
|
638
|
+
export interface MoveColumnBetweenSectionsResult {
|
|
639
|
+
rows: ContentItem[];
|
|
640
|
+
/** Final span after clamping to target grid. */
|
|
641
|
+
finalSpan: number;
|
|
642
|
+
/** Final grid_column after clamping. */
|
|
643
|
+
finalGridColumn: number;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Move a column from one columnar section to another (V2 ↔ Cover).
|
|
648
|
+
*
|
|
649
|
+
* Blocks travel with the column. Responsive overrides for the moved
|
|
650
|
+
* column are STRIPPED from the source section — responsive overrides
|
|
651
|
+
* are per-section `_key` lookups and don't carry semantic meaning in
|
|
652
|
+
* the new home. Tablet/phone layouts for the moved column revert to
|
|
653
|
+
* desktop until the user re-adds overrides in the target.
|
|
654
|
+
*
|
|
655
|
+
* Span and grid_column are clamped to target's grid_columns.
|
|
656
|
+
*
|
|
657
|
+
* Returns null if:
|
|
658
|
+
* - source or target key doesn't resolve to a columnar section
|
|
659
|
+
* - source and target are the same section (caller should use
|
|
660
|
+
* moveColumnV2InState instead)
|
|
661
|
+
* - the column key doesn't exist in the source
|
|
662
|
+
*/
|
|
663
|
+
export function moveColumnBetweenSectionsInState(
|
|
664
|
+
rows: ContentItem[],
|
|
665
|
+
sourceSectionKey: string,
|
|
666
|
+
columnKey: string,
|
|
667
|
+
targetSectionKey: string,
|
|
668
|
+
targetRow: number,
|
|
669
|
+
targetColumn: number,
|
|
670
|
+
targetSpan: number,
|
|
671
|
+
): MoveColumnBetweenSectionsResult | null {
|
|
672
|
+
if (sourceSectionKey === targetSectionKey) return null;
|
|
673
|
+
|
|
674
|
+
const sourcePath = findSectionPath(rows, sourceSectionKey);
|
|
675
|
+
const targetPath = findSectionPath(rows, targetSectionKey);
|
|
676
|
+
if (!sourcePath || !targetPath) return null;
|
|
677
|
+
|
|
678
|
+
// Read the source column (full SectionColumn with blocks)
|
|
679
|
+
const sourceVirtual = getSectionFromPath(rows, sourcePath);
|
|
680
|
+
if (!sourceVirtual) return null;
|
|
681
|
+
const movedColumn = sourceVirtual.columns.find((c) => c._key === columnKey);
|
|
682
|
+
if (!movedColumn) return null;
|
|
683
|
+
|
|
684
|
+
// Read target for grid_columns
|
|
685
|
+
const targetVirtual = getSectionFromPath(rows, targetPath);
|
|
686
|
+
if (!targetVirtual) return null;
|
|
687
|
+
const targetGridCols = targetVirtual.settings.grid_columns || 12;
|
|
688
|
+
|
|
689
|
+
// Clamp span + grid column to target grid
|
|
690
|
+
const clampedSpan = Math.max(1, Math.min(targetSpan, targetGridCols));
|
|
691
|
+
const clampedCol = Math.max(
|
|
692
|
+
1,
|
|
693
|
+
Math.min(targetColumn, targetGridCols - clampedSpan + 1),
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
// --- Step 1: Remove column from source, strip its responsive overrides ---
|
|
697
|
+
let newRows = updateSectionAtPath(rows, sourcePath, (section) => {
|
|
698
|
+
const filteredColumns = section.columns.filter((c) => c._key !== columnKey);
|
|
699
|
+
|
|
700
|
+
// Strip responsive overrides referencing the moved column
|
|
701
|
+
let newResponsive = section.responsive;
|
|
702
|
+
if (newResponsive) {
|
|
703
|
+
const cleanResponsive: PageSectionV2["responsive"] = {};
|
|
704
|
+
for (const vp of ["tablet", "phone"] as const) {
|
|
705
|
+
const vpData = newResponsive[vp];
|
|
706
|
+
if (!vpData) continue;
|
|
707
|
+
const newVpData: NonNullable<PageSectionV2["responsive"]>[typeof vp] = {
|
|
708
|
+
...vpData,
|
|
709
|
+
};
|
|
710
|
+
if (newVpData.columns) {
|
|
711
|
+
const filtered = newVpData.columns.filter((o) => o._key !== columnKey);
|
|
712
|
+
newVpData.columns = filtered.length > 0 ? filtered : undefined;
|
|
713
|
+
}
|
|
714
|
+
// Keep viewport entry only if it still has something
|
|
715
|
+
if (newVpData.columns || newVpData.settings) {
|
|
716
|
+
cleanResponsive[vp] = newVpData;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
newResponsive =
|
|
720
|
+
Object.keys(cleanResponsive).length > 0 ? cleanResponsive : undefined;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const cascadeCols = toCascadeColumns(filteredColumns);
|
|
724
|
+
const newPreset = detectPreset(cascadeCols, section.settings.grid_columns);
|
|
725
|
+
|
|
726
|
+
return {
|
|
727
|
+
...section,
|
|
728
|
+
columns: filteredColumns,
|
|
729
|
+
responsive: newResponsive,
|
|
730
|
+
settings: { ...section.settings, preset: newPreset },
|
|
731
|
+
};
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// --- Step 2: Insert the column into target with cascade ---
|
|
735
|
+
newRows = updateSectionAtPath(newRows, targetPath, (section) => {
|
|
736
|
+
// Augment the source list so `applyBlocksToColumns` can find the moved
|
|
737
|
+
// column's blocks by key. The position fields here are overwritten by
|
|
738
|
+
// cascade; only the blocks matter for the lookup.
|
|
739
|
+
const augmentedSource: SectionColumn[] = [
|
|
740
|
+
...section.columns,
|
|
741
|
+
{
|
|
742
|
+
...movedColumn,
|
|
743
|
+
grid_row: targetRow,
|
|
744
|
+
grid_column: clampedCol,
|
|
745
|
+
span: clampedSpan,
|
|
746
|
+
},
|
|
747
|
+
];
|
|
748
|
+
|
|
749
|
+
const cascadeResult = cascadeAddColumn(
|
|
750
|
+
toCascadeColumns(section.columns),
|
|
751
|
+
targetRow,
|
|
752
|
+
clampedCol,
|
|
753
|
+
clampedSpan,
|
|
754
|
+
columnKey,
|
|
755
|
+
section.settings.grid_columns,
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
const finalColumns = applyBlocksToColumns(cascadeResult, augmentedSource);
|
|
759
|
+
const newPreset = detectPreset(cascadeResult, section.settings.grid_columns);
|
|
760
|
+
|
|
761
|
+
return {
|
|
762
|
+
...section,
|
|
763
|
+
columns: finalColumns,
|
|
764
|
+
settings: { ...section.settings, preset: newPreset },
|
|
765
|
+
};
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
return {
|
|
769
|
+
rows: newRows,
|
|
770
|
+
finalSpan: clampedSpan,
|
|
771
|
+
finalGridColumn: clampedCol,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// ============================================
|
|
776
|
+
// swapColumnsBetweenSectionsInState (Session 183 — cross-section column swap)
|
|
777
|
+
// ============================================
|
|
778
|
+
|
|
779
|
+
export interface SwapColumnsBetweenSectionsResult {
|
|
780
|
+
rows: ContentItem[];
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Swap two columns across different columnar sections (V2 ↔ Cover).
|
|
785
|
+
*
|
|
786
|
+
* Semantically identical to the intra-section `swapColumnV2InState`:
|
|
787
|
+
* each column adopts the other's position/row/span. The difference is
|
|
788
|
+
* that the columns live in DIFFERENT sections, so each has to travel.
|
|
789
|
+
* Blocks go with their owner column.
|
|
790
|
+
*
|
|
791
|
+
* Clamping: when the two sections have different `grid_columns`, each
|
|
792
|
+
* column's new span and grid_column are clamped to fit the destination
|
|
793
|
+
* grid. Clamping is defensive — if both sections share 12 columns (the
|
|
794
|
+
* common case), no clamping happens.
|
|
795
|
+
*
|
|
796
|
+
* Responsive: overrides for BOTH swapped columns are stripped from both
|
|
797
|
+
* sections' responsive tables. Overrides live per-section by `_key`
|
|
798
|
+
* lookup; transplanting either direction would leave orphaned entries.
|
|
799
|
+
*
|
|
800
|
+
* Returns null if:
|
|
801
|
+
* - source and target are the same section
|
|
802
|
+
* - either key doesn't resolve to a columnar section
|
|
803
|
+
* - either column key doesn't exist in its section
|
|
804
|
+
*/
|
|
805
|
+
export function swapColumnsBetweenSectionsInState(
|
|
806
|
+
rows: ContentItem[],
|
|
807
|
+
sourceSectionKey: string,
|
|
808
|
+
sourceColumnKey: string,
|
|
809
|
+
targetSectionKey: string,
|
|
810
|
+
targetColumnKey: string,
|
|
811
|
+
): SwapColumnsBetweenSectionsResult | null {
|
|
812
|
+
if (sourceSectionKey === targetSectionKey) return null;
|
|
813
|
+
|
|
814
|
+
const sourcePath = findSectionPath(rows, sourceSectionKey);
|
|
815
|
+
const targetPath = findSectionPath(rows, targetSectionKey);
|
|
816
|
+
if (!sourcePath || !targetPath) return null;
|
|
817
|
+
|
|
818
|
+
const sourceVirtual = getSectionFromPath(rows, sourcePath);
|
|
819
|
+
const targetVirtual = getSectionFromPath(rows, targetPath);
|
|
820
|
+
if (!sourceVirtual || !targetVirtual) return null;
|
|
821
|
+
|
|
822
|
+
const sourceCol = sourceVirtual.columns.find((c) => c._key === sourceColumnKey);
|
|
823
|
+
const targetCol = targetVirtual.columns.find((c) => c._key === targetColumnKey);
|
|
824
|
+
if (!sourceCol || !targetCol) return null;
|
|
825
|
+
|
|
826
|
+
const sourceGridCols = sourceVirtual.settings.grid_columns || 12;
|
|
827
|
+
const targetGridCols = targetVirtual.settings.grid_columns || 12;
|
|
828
|
+
|
|
829
|
+
// Clamp source column's new dimensions (adopting target's position) to target grid
|
|
830
|
+
const newSourceSpan = Math.max(1, Math.min(targetCol.span, targetGridCols));
|
|
831
|
+
const newSourceGridCol = Math.max(
|
|
832
|
+
1,
|
|
833
|
+
Math.min(targetCol.grid_column, targetGridCols - newSourceSpan + 1),
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
// Clamp target column's new dimensions (adopting source's position) to source grid
|
|
837
|
+
const newTargetSpan = Math.max(1, Math.min(sourceCol.span, sourceGridCols));
|
|
838
|
+
const newTargetGridCol = Math.max(
|
|
839
|
+
1,
|
|
840
|
+
Math.min(sourceCol.grid_column, sourceGridCols - newTargetSpan + 1),
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
// --- Helper: strip responsive overrides for a given column key ---
|
|
844
|
+
const stripResponsiveForKey = (
|
|
845
|
+
responsive: PageSectionV2["responsive"],
|
|
846
|
+
columnKey: string,
|
|
847
|
+
): PageSectionV2["responsive"] => {
|
|
848
|
+
if (!responsive) return responsive;
|
|
849
|
+
const cleanResponsive: PageSectionV2["responsive"] = {};
|
|
850
|
+
for (const vp of ["tablet", "phone"] as const) {
|
|
851
|
+
const vpData = responsive[vp];
|
|
852
|
+
if (!vpData) continue;
|
|
853
|
+
const newVpData: NonNullable<PageSectionV2["responsive"]>[typeof vp] = {
|
|
854
|
+
...vpData,
|
|
855
|
+
};
|
|
856
|
+
if (newVpData.columns) {
|
|
857
|
+
const filtered = newVpData.columns.filter((o) => o._key !== columnKey);
|
|
858
|
+
newVpData.columns = filtered.length > 0 ? filtered : undefined;
|
|
859
|
+
}
|
|
860
|
+
if (newVpData.columns || newVpData.settings) {
|
|
861
|
+
cleanResponsive[vp] = newVpData;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return Object.keys(cleanResponsive).length > 0 ? cleanResponsive : undefined;
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
// --- Step 1: Replace source column with target column (transplanted) ---
|
|
868
|
+
// Also strip sourceColumnKey's responsive overrides.
|
|
869
|
+
let newRows = updateSectionAtPath(rows, sourcePath, (section) => {
|
|
870
|
+
// Build the transplanted column with target's blocks, adopting
|
|
871
|
+
// source's OLD position/row/span (clamped to source grid).
|
|
872
|
+
const transplanted: SectionColumn = {
|
|
873
|
+
...targetCol,
|
|
874
|
+
grid_row: sourceCol.grid_row,
|
|
875
|
+
grid_column: newTargetGridCol,
|
|
876
|
+
span: newTargetSpan,
|
|
877
|
+
};
|
|
878
|
+
const updatedColumns = section.columns.map((c) =>
|
|
879
|
+
c._key === sourceColumnKey ? transplanted : c,
|
|
880
|
+
);
|
|
881
|
+
|
|
882
|
+
const newResponsive = stripResponsiveForKey(
|
|
883
|
+
section.responsive,
|
|
884
|
+
sourceColumnKey,
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
const cascadeCols = toCascadeColumns(updatedColumns);
|
|
888
|
+
const newPreset = detectPreset(cascadeCols, section.settings.grid_columns);
|
|
889
|
+
|
|
890
|
+
return {
|
|
891
|
+
...section,
|
|
892
|
+
columns: updatedColumns,
|
|
893
|
+
responsive: newResponsive,
|
|
894
|
+
settings: { ...section.settings, preset: newPreset },
|
|
895
|
+
};
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
// --- Step 2: Replace target column with source column (transplanted) ---
|
|
899
|
+
// Also strip targetColumnKey's responsive overrides.
|
|
900
|
+
newRows = updateSectionAtPath(newRows, targetPath, (section) => {
|
|
901
|
+
const transplanted: SectionColumn = {
|
|
902
|
+
...sourceCol,
|
|
903
|
+
grid_row: targetCol.grid_row,
|
|
904
|
+
grid_column: newSourceGridCol,
|
|
905
|
+
span: newSourceSpan,
|
|
906
|
+
};
|
|
907
|
+
const updatedColumns = section.columns.map((c) =>
|
|
908
|
+
c._key === targetColumnKey ? transplanted : c,
|
|
909
|
+
);
|
|
910
|
+
|
|
911
|
+
const newResponsive = stripResponsiveForKey(
|
|
912
|
+
section.responsive,
|
|
913
|
+
targetColumnKey,
|
|
914
|
+
);
|
|
915
|
+
|
|
916
|
+
const cascadeCols = toCascadeColumns(updatedColumns);
|
|
917
|
+
const newPreset = detectPreset(cascadeCols, section.settings.grid_columns);
|
|
918
|
+
|
|
919
|
+
return {
|
|
920
|
+
...section,
|
|
921
|
+
columns: updatedColumns,
|
|
922
|
+
responsive: newResponsive,
|
|
923
|
+
settings: { ...section.settings, preset: newPreset },
|
|
924
|
+
};
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
return { rows: newRows };
|
|
928
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BuilderStore, BuilderState, BlockType } from "./types";
|
|
1
|
+
import type { BuilderStore, BuilderState, BlockType, SectionSliceActions } from "./types";
|
|
2
2
|
import type {
|
|
3
3
|
ContentBlock,
|
|
4
4
|
ContentItem,
|
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
moveColumnV2InState,
|
|
39
39
|
swapColumnV2InState,
|
|
40
40
|
moveColumnToGapV2InState,
|
|
41
|
+
moveColumnBetweenSectionsInState,
|
|
42
|
+
swapColumnsBetweenSectionsInState,
|
|
41
43
|
} from "./store-helpers";
|
|
42
44
|
|
|
43
45
|
type StoreSet = (
|
|
@@ -45,7 +47,7 @@ type StoreSet = (
|
|
|
45
47
|
) => void;
|
|
46
48
|
type StoreGet = () => BuilderStore;
|
|
47
49
|
|
|
48
|
-
export function createSectionActions(set: StoreSet, get: StoreGet) {
|
|
50
|
+
export function createSectionActions(set: StoreSet, get: StoreGet): SectionSliceActions {
|
|
49
51
|
return {
|
|
50
52
|
// ---- Section operations ----
|
|
51
53
|
|
|
@@ -339,6 +341,64 @@ export function createSectionActions(set: StoreSet, get: StoreGet) {
|
|
|
339
341
|
set({ rows: result.rows, isDirty: true });
|
|
340
342
|
},
|
|
341
343
|
|
|
344
|
+
moveColumnBetweenSections: (
|
|
345
|
+
sourceSectionKey: string,
|
|
346
|
+
columnKey: string,
|
|
347
|
+
targetSectionKey: string,
|
|
348
|
+
targetRow: number,
|
|
349
|
+
targetColumn: number,
|
|
350
|
+
targetSpan: number,
|
|
351
|
+
): void => {
|
|
352
|
+
const result = moveColumnBetweenSectionsInState(
|
|
353
|
+
get().rows,
|
|
354
|
+
sourceSectionKey,
|
|
355
|
+
columnKey,
|
|
356
|
+
targetSectionKey,
|
|
357
|
+
targetRow,
|
|
358
|
+
targetColumn,
|
|
359
|
+
targetSpan,
|
|
360
|
+
);
|
|
361
|
+
if (!result) return;
|
|
362
|
+
|
|
363
|
+
get()._pushSnapshot();
|
|
364
|
+
// Clear any selection referencing the moved column in its old section —
|
|
365
|
+
// the column key remains but the "selected section" context has changed.
|
|
366
|
+
set({
|
|
367
|
+
rows: result.rows,
|
|
368
|
+
isDirty: true,
|
|
369
|
+
selectedColumnKey: columnKey,
|
|
370
|
+
selectedRowKey: targetSectionKey,
|
|
371
|
+
selectedBlockKey: null,
|
|
372
|
+
});
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
swapColumnsBetweenSections: (
|
|
376
|
+
sourceSectionKey: string,
|
|
377
|
+
sourceColumnKey: string,
|
|
378
|
+
targetSectionKey: string,
|
|
379
|
+
targetColumnKey: string,
|
|
380
|
+
): void => {
|
|
381
|
+
const result = swapColumnsBetweenSectionsInState(
|
|
382
|
+
get().rows,
|
|
383
|
+
sourceSectionKey,
|
|
384
|
+
sourceColumnKey,
|
|
385
|
+
targetSectionKey,
|
|
386
|
+
targetColumnKey,
|
|
387
|
+
);
|
|
388
|
+
if (!result) return;
|
|
389
|
+
|
|
390
|
+
get()._pushSnapshot();
|
|
391
|
+
// After swap: the dragged column now lives in the target section.
|
|
392
|
+
// Select it there (match mental model: "I just put this column here").
|
|
393
|
+
set({
|
|
394
|
+
rows: result.rows,
|
|
395
|
+
isDirty: true,
|
|
396
|
+
selectedColumnKey: sourceColumnKey,
|
|
397
|
+
selectedRowKey: targetSectionKey,
|
|
398
|
+
selectedBlockKey: null,
|
|
399
|
+
});
|
|
400
|
+
},
|
|
401
|
+
|
|
342
402
|
applyPresetV2: (sectionKey: string, preset: SectionV2Preset): void => {
|
|
343
403
|
if (preset === "custom") return;
|
|
344
404
|
|