@dragonworks/ngx-dashboard 20.0.4 → 20.0.6

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.
Files changed (78) hide show
  1. package/ng-package.json +7 -0
  2. package/package.json +34 -45
  3. package/src/lib/__tests__/dashboard-component-widget-state-integration.spec.ts +537 -0
  4. package/src/lib/cell/__tests__/cell-resize.component.spec.ts +442 -0
  5. package/src/lib/cell/__tests__/cell.component.spec.ts +541 -0
  6. package/src/lib/cell/cell-context-menu.component.ts +138 -0
  7. package/src/lib/cell/cell-context-menu.service.ts +36 -0
  8. package/src/lib/cell/cell.component.html +37 -0
  9. package/src/lib/cell/cell.component.scss +198 -0
  10. package/src/lib/cell/cell.component.ts +375 -0
  11. package/src/lib/dashboard/dashboard.component.html +18 -0
  12. package/src/lib/dashboard/dashboard.component.scss +17 -0
  13. package/src/lib/dashboard/dashboard.component.ts +187 -0
  14. package/src/lib/dashboard-editor/dashboard-editor.component.html +57 -0
  15. package/src/lib/dashboard-editor/dashboard-editor.component.scss +87 -0
  16. package/src/lib/dashboard-editor/dashboard-editor.component.ts +219 -0
  17. package/src/lib/dashboard-viewer/__tests__/dashboard-viewer.component.spec.ts +258 -0
  18. package/src/lib/dashboard-viewer/dashboard-viewer.component.html +20 -0
  19. package/src/lib/dashboard-viewer/dashboard-viewer.component.scss +50 -0
  20. package/src/lib/dashboard-viewer/dashboard-viewer.component.ts +70 -0
  21. package/src/lib/drop-zone/__tests__/drop-zone.component.spec.ts +465 -0
  22. package/src/lib/drop-zone/drop-zone.component.html +20 -0
  23. package/src/lib/drop-zone/drop-zone.component.scss +67 -0
  24. package/src/lib/drop-zone/drop-zone.component.ts +122 -0
  25. package/src/lib/internal-widgets/unknown-widget/unknown-widget.component.ts +72 -0
  26. package/src/lib/models/cell-data.ts +13 -0
  27. package/src/lib/models/cell-dialog.ts +7 -0
  28. package/src/lib/models/cell-id.ts +85 -0
  29. package/src/lib/models/cell-position.ts +15 -0
  30. package/src/lib/models/dashboard-data.dto.ts +44 -0
  31. package/src/lib/models/dashboard-data.utils.ts +49 -0
  32. package/src/lib/models/drag-data.ts +6 -0
  33. package/src/lib/models/index.ts +11 -0
  34. package/src/lib/models/reserved-space.ts +24 -0
  35. package/src/lib/models/widget-factory.ts +33 -0
  36. package/src/lib/models/widget-id.ts +70 -0
  37. package/src/lib/models/widget.ts +21 -0
  38. package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.component.ts +127 -0
  39. package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.provider.ts +15 -0
  40. package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.tokens.ts +20 -0
  41. package/src/lib/providers/cell-settings-dialog/default-cell-settings-dialog.provider.ts +32 -0
  42. package/src/lib/providers/cell-settings-dialog/index.ts +3 -0
  43. package/src/lib/providers/index.ts +1 -0
  44. package/src/lib/services/__tests__/dashboard-bridge.service.spec.ts +220 -0
  45. package/src/lib/services/__tests__/dashboard-viewport.service.spec.ts +362 -0
  46. package/src/lib/services/dashboard-bridge.service.ts +155 -0
  47. package/src/lib/services/dashboard-viewport.service.ts +148 -0
  48. package/src/lib/services/dashboard.service.ts +62 -0
  49. package/src/lib/store/__tests__/dashboard-store-collision-detection.spec.ts +756 -0
  50. package/src/lib/store/__tests__/dashboard-store-computed-properties.spec.ts +974 -0
  51. package/src/lib/store/__tests__/dashboard-store-drag-drop.spec.ts +279 -0
  52. package/src/lib/store/__tests__/dashboard-store-export-import.spec.ts +780 -0
  53. package/src/lib/store/__tests__/dashboard-store-grid-config.spec.ts +128 -0
  54. package/src/lib/store/__tests__/dashboard-store-query-methods.spec.ts +229 -0
  55. package/src/lib/store/__tests__/dashboard-store-resize-operations.spec.ts +652 -0
  56. package/src/lib/store/__tests__/dashboard-store-widget-management.spec.ts +461 -0
  57. package/src/lib/store/__tests__/dashboard-store-widget-state-preservation.spec.ts +369 -0
  58. package/src/lib/store/dashboard-store.ts +239 -0
  59. package/src/lib/store/features/drag-drop.feature.ts +140 -0
  60. package/src/lib/store/features/grid-config.feature.ts +43 -0
  61. package/src/lib/store/features/resize.feature.ts +140 -0
  62. package/src/lib/store/features/utils/collision.utils.ts +89 -0
  63. package/src/lib/store/features/utils/grid-query-internal.utils.ts +37 -0
  64. package/src/lib/store/features/utils/resize.utils.ts +165 -0
  65. package/src/lib/store/features/widget-management.feature.ts +158 -0
  66. package/src/lib/styles/_dashboard-grid-vars.scss +11 -0
  67. package/src/lib/widget-list/__tests__/widget-list-bridge-integration.spec.ts +137 -0
  68. package/src/lib/widget-list/widget-list.component.html +22 -0
  69. package/src/lib/widget-list/widget-list.component.scss +154 -0
  70. package/src/lib/widget-list/widget-list.component.ts +106 -0
  71. package/src/public-api.ts +21 -0
  72. package/src/test-setup.ts +10 -0
  73. package/tsconfig.lib.json +15 -0
  74. package/tsconfig.lib.prod.json +11 -0
  75. package/tsconfig.spec.json +14 -0
  76. package/fesm2022/dragonworks-ngx-dashboard.mjs +0 -2192
  77. package/fesm2022/dragonworks-ngx-dashboard.mjs.map +0 -1
  78. package/index.d.ts +0 -678
@@ -0,0 +1,140 @@
1
+ import {
2
+ signalStoreFeature,
3
+ withMethods,
4
+ withState,
5
+ withComputed,
6
+ patchState,
7
+ } from '@ngrx/signals';
8
+ import { computed } from '@angular/core';
9
+ import {
10
+ CellId,
11
+ CellIdUtils,
12
+ CellData,
13
+ DragData,
14
+ WidgetFactory,
15
+ WidgetId,
16
+ } from '../../models';
17
+ import {
18
+ calculateCollisionInfo,
19
+ calculateHighlightedZones,
20
+ } from './utils/collision.utils';
21
+ import { DashboardService } from '../../services/dashboard.service';
22
+
23
+ export interface DragDropState {
24
+ dragData: DragData | null;
25
+ hoveredDropZone: { row: number; col: number } | null;
26
+ }
27
+
28
+ const initialDragDropState: DragDropState = {
29
+ dragData: null,
30
+ hoveredDropZone: null,
31
+ };
32
+
33
+ export const withDragDrop = () =>
34
+ signalStoreFeature(
35
+ withState<DragDropState>(initialDragDropState),
36
+ withComputed((store) => ({
37
+ // Highlighted zones during drag
38
+ highlightedZones: computed(() =>
39
+ calculateHighlightedZones(store.dragData(), store.hoveredDropZone()),
40
+ ),
41
+ })),
42
+ withComputed((store) => ({
43
+ // Map for quick highlight lookup - reuse highlightedZones computation
44
+ highlightMap: computed(() => {
45
+ const zones = store.highlightedZones();
46
+ const map = new Set<CellId>();
47
+
48
+ for (const z of zones) {
49
+ map.add(CellIdUtils.create(z.row, z.col));
50
+ }
51
+
52
+ return map;
53
+ }),
54
+ })),
55
+ withMethods((store) => ({
56
+ startDrag(dragData: DragData) {
57
+ patchState(store, { dragData });
58
+ },
59
+
60
+ endDrag() {
61
+ patchState(store, {
62
+ dragData: null,
63
+ hoveredDropZone: null,
64
+ });
65
+ },
66
+
67
+ setHoveredDropZone(zone: { row: number; col: number } | null) {
68
+ patchState(store, { hoveredDropZone: zone });
69
+ },
70
+ })),
71
+
72
+ // Second withMethods block for drop handling that can access endDrag
73
+ withMethods((store) => ({
74
+ // Drop handling logic with dependency injection
75
+ _handleDrop(
76
+ dragData: DragData,
77
+ targetPosition: { row: number; col: number },
78
+ dependencies: {
79
+ cells: CellData[];
80
+ rows: number;
81
+ columns: number;
82
+ dashboardService: DashboardService;
83
+ createWidget: (
84
+ row: number,
85
+ col: number,
86
+ factory: WidgetFactory,
87
+ widgetState?: string,
88
+ ) => void;
89
+ updateWidgetPosition: (
90
+ widgetId: WidgetId,
91
+ row: number,
92
+ col: number,
93
+ ) => void;
94
+ },
95
+ ): boolean {
96
+ // 1. Validate placement using existing collision detection
97
+ const collisionInfo = calculateCollisionInfo(
98
+ dragData,
99
+ targetPosition,
100
+ dependencies.cells,
101
+ dependencies.rows,
102
+ dependencies.columns,
103
+ );
104
+
105
+ // 2. End drag state first
106
+ store.endDrag();
107
+
108
+ // 3. Early return if invalid placement
109
+ if (collisionInfo.hasCollisions || collisionInfo.outOfBounds) {
110
+ return false;
111
+ }
112
+
113
+ // 4. Handle widget creation from palette
114
+ if (dragData.kind === 'widget') {
115
+ const factory = dependencies.dashboardService.getFactory(
116
+ dragData.content.widgetTypeid,
117
+ );
118
+ dependencies.createWidget(
119
+ targetPosition.row,
120
+ targetPosition.col,
121
+ factory,
122
+ undefined,
123
+ );
124
+ return true;
125
+ }
126
+
127
+ // 5. Handle cell movement
128
+ if (dragData.kind === 'cell') {
129
+ dependencies.updateWidgetPosition(
130
+ dragData.content.widgetId, // Use widgetId instead of cellId
131
+ targetPosition.row,
132
+ targetPosition.col,
133
+ );
134
+ return true;
135
+ }
136
+
137
+ return false;
138
+ },
139
+ })),
140
+ );
@@ -0,0 +1,43 @@
1
+ import { signalStoreFeature, withMethods, withState, patchState } from '@ngrx/signals';
2
+
3
+ export interface GridConfigState {
4
+ rows: number;
5
+ columns: number;
6
+ gutterSize: string;
7
+ isEditMode: boolean;
8
+ gridCellDimensions: { width: number; height: number };
9
+ }
10
+
11
+ const initialGridConfigState: GridConfigState = {
12
+ rows: 8,
13
+ columns: 16,
14
+ gutterSize: '0.5em',
15
+ isEditMode: false,
16
+ gridCellDimensions: { width: 0, height: 0 },
17
+ };
18
+
19
+ export const withGridConfig = () =>
20
+ signalStoreFeature(
21
+ withState<GridConfigState>(initialGridConfigState),
22
+ withMethods((store) => ({
23
+ setGridConfig(config: {
24
+ rows?: number;
25
+ columns?: number;
26
+ gutterSize?: string;
27
+ }) {
28
+ patchState(store, config);
29
+ },
30
+
31
+ setGridCellDimensions(width: number, height: number) {
32
+ patchState(store, { gridCellDimensions: { width, height } });
33
+ },
34
+
35
+ toggleEditMode() {
36
+ patchState(store, { isEditMode: !store.isEditMode() });
37
+ },
38
+
39
+ setEditMode(isEditMode: boolean) {
40
+ patchState(store, { isEditMode });
41
+ },
42
+ }))
43
+ );
@@ -0,0 +1,140 @@
1
+ import {
2
+ signalStoreFeature,
3
+ withMethods,
4
+ withState,
5
+ patchState,
6
+ } from '@ngrx/signals';
7
+ import { CellId, CellIdUtils, CellData } from '../../models';
8
+ import { calculateResizePreview, type ResizeData } from './utils/resize.utils';
9
+
10
+ export interface ResizeState {
11
+ resizeData: ResizeData | null;
12
+ }
13
+
14
+ const initialResizeState: ResizeState = {
15
+ resizeData: null,
16
+ };
17
+
18
+ // Utility functions for resize preview computations
19
+ export const ResizePreviewUtils = {
20
+ computePreviewCells(
21
+ resizeData: ResizeData | null,
22
+ cells: CellData[],
23
+ ): { row: number; col: number }[] {
24
+ if (!resizeData) return [];
25
+
26
+ const cell = cells.find((cell) =>
27
+ CellIdUtils.equals(cell.cellId, resizeData.cellId),
28
+ );
29
+ if (!cell) return [];
30
+
31
+ const previewCells: { row: number; col: number }[] = [];
32
+ for (let r = 0; r < resizeData.previewRowSpan; r++) {
33
+ for (let c = 0; c < resizeData.previewColSpan; c++) {
34
+ previewCells.push({
35
+ row: cell.row + r,
36
+ col: cell.col + c,
37
+ });
38
+ }
39
+ }
40
+
41
+ return previewCells;
42
+ },
43
+
44
+ computePreviewMap(previewCells: { row: number; col: number }[]): Set<CellId> {
45
+ const map = new Set<CellId>();
46
+ for (const cell of previewCells) {
47
+ map.add(CellIdUtils.create(cell.row, cell.col));
48
+ }
49
+ return map;
50
+ },
51
+ };
52
+
53
+ export const withResize = () =>
54
+ signalStoreFeature(
55
+ withState<ResizeState>(initialResizeState),
56
+ withMethods((store) => ({
57
+ // Resize methods that need cross-feature dependencies
58
+ _startResize(
59
+ cellId: CellId,
60
+ dependencies: {
61
+ cells: CellData[];
62
+ },
63
+ ) {
64
+ const cell = dependencies.cells.find((c) =>
65
+ CellIdUtils.equals(c.cellId, cellId),
66
+ );
67
+ if (!cell) return;
68
+
69
+ patchState(store, {
70
+ resizeData: {
71
+ cellId,
72
+ originalRowSpan: cell.rowSpan,
73
+ originalColSpan: cell.colSpan,
74
+ previewRowSpan: cell.rowSpan,
75
+ previewColSpan: cell.colSpan,
76
+ },
77
+ });
78
+ },
79
+
80
+ _updateResizePreview(
81
+ direction: 'horizontal' | 'vertical',
82
+ delta: number,
83
+ dependencies: {
84
+ cells: CellData[];
85
+ rows: number;
86
+ columns: number;
87
+ },
88
+ ) {
89
+ const resizeData = store.resizeData();
90
+ if (!resizeData) return;
91
+
92
+ const newSpans = calculateResizePreview(
93
+ resizeData,
94
+ direction,
95
+ delta,
96
+ dependencies.cells,
97
+ dependencies.rows,
98
+ dependencies.columns,
99
+ );
100
+
101
+ if (newSpans) {
102
+ patchState(store, {
103
+ resizeData: {
104
+ ...resizeData,
105
+ previewRowSpan: newSpans.rowSpan,
106
+ previewColSpan: newSpans.colSpan,
107
+ },
108
+ });
109
+ }
110
+ },
111
+
112
+ _endResize(
113
+ apply: boolean,
114
+ dependencies: {
115
+ updateWidgetSpan: (
116
+ id: CellId,
117
+ rowSpan: number,
118
+ colSpan: number,
119
+ ) => void;
120
+ },
121
+ ) {
122
+ const resizeData = store.resizeData();
123
+ if (!resizeData) return;
124
+
125
+ if (
126
+ apply &&
127
+ (resizeData.previewRowSpan !== resizeData.originalRowSpan ||
128
+ resizeData.previewColSpan !== resizeData.originalColSpan)
129
+ ) {
130
+ dependencies.updateWidgetSpan(
131
+ resizeData.cellId,
132
+ resizeData.previewRowSpan,
133
+ resizeData.previewColSpan,
134
+ );
135
+ }
136
+
137
+ patchState(store, { resizeData: null });
138
+ },
139
+ })),
140
+ );
@@ -0,0 +1,89 @@
1
+ import { CellId, CellIdUtils, CellData, DragData } from '../../../models';
2
+ import { GridQueryInternalUtils } from './grid-query-internal.utils';
3
+
4
+ export interface CollisionInfo {
5
+ hasCollisions: boolean;
6
+ invalidCells: CellId[];
7
+ outOfBounds: boolean;
8
+ footprint: { row: number; col: number }[];
9
+ }
10
+
11
+ export function calculateCollisionInfo(
12
+ dragData: DragData | null,
13
+ hovered: { row: number; col: number } | null,
14
+ cells: CellData[],
15
+ rows: number,
16
+ columns: number,
17
+ ): CollisionInfo {
18
+ if (!dragData || !hovered) {
19
+ return {
20
+ hasCollisions: false,
21
+ invalidCells: [],
22
+ outOfBounds: false,
23
+ footprint: [],
24
+ };
25
+ }
26
+
27
+ const isCell = dragData.kind === 'cell';
28
+ const rowSpan = isCell ? dragData.content.rowSpan : 1;
29
+ const colSpan = isCell ? dragData.content.colSpan : 1;
30
+
31
+ // Check bounds
32
+ const outOfBounds = GridQueryInternalUtils.isOutOfBounds(
33
+ hovered.row,
34
+ hovered.col,
35
+ rowSpan,
36
+ colSpan,
37
+ rows,
38
+ columns
39
+ );
40
+
41
+ // Generate footprint
42
+ const footprint: { row: number; col: number }[] = [];
43
+ for (let r = 0; r < rowSpan; r++) {
44
+ for (let c = 0; c < colSpan; c++) {
45
+ footprint.push({ row: hovered.row + r, col: hovered.col + c });
46
+ }
47
+ }
48
+
49
+ const excludeWidgetId = isCell ? dragData.content.widgetId : undefined;
50
+
51
+ // Check for actual collisions with other widgets (not self)
52
+ const hasCollisions = footprint.some((pos) =>
53
+ GridQueryInternalUtils.isCellOccupied(cells, pos.row, pos.col, excludeWidgetId)
54
+ );
55
+
56
+ // Generate invalid cell IDs
57
+ const invalidCells: CellId[] = [];
58
+ if (hasCollisions || outOfBounds) {
59
+ for (const pos of footprint) {
60
+ invalidCells.push(CellIdUtils.create(pos.row, pos.col));
61
+ }
62
+ }
63
+
64
+ return {
65
+ hasCollisions,
66
+ invalidCells,
67
+ outOfBounds,
68
+ footprint,
69
+ };
70
+ }
71
+
72
+ export function calculateHighlightedZones(
73
+ dragData: DragData | null,
74
+ hovered: { row: number; col: number } | null,
75
+ ): { row: number; col: number }[] {
76
+ if (!dragData || !hovered) return [];
77
+
78
+ const zones: { row: number; col: number }[] = [];
79
+ const rowSpan = dragData.kind === 'cell' ? dragData.content.rowSpan : 1;
80
+ const colSpan = dragData.kind === 'cell' ? dragData.content.colSpan : 1;
81
+
82
+ for (let r = 0; r < rowSpan; r++) {
83
+ for (let c = 0; c < colSpan; c++) {
84
+ zones.push({ row: hovered.row + r, col: hovered.col + c });
85
+ }
86
+ }
87
+
88
+ return zones;
89
+ }
@@ -0,0 +1,37 @@
1
+ import { CellId, CellIdUtils, CellData, WidgetId } from '../../../models';
2
+
3
+ // Internal utility functions used by collision detection, resize logic, and tests
4
+ export const GridQueryInternalUtils = {
5
+ isCellOccupied(cells: CellData[], row: number, col: number, excludeWidgetId?: WidgetId): boolean {
6
+ return cells.some((cell) => {
7
+ // Skip checking against the widget being dragged (use widgetId for stable identity)
8
+ if (excludeWidgetId && cell.widgetId === excludeWidgetId)
9
+ return false;
10
+
11
+ const endRow = cell.row + cell.rowSpan - 1;
12
+ const endCol = cell.col + cell.colSpan - 1;
13
+
14
+ return (
15
+ cell.row <= row && row <= endRow && cell.col <= col && col <= endCol
16
+ );
17
+ });
18
+ },
19
+
20
+ isOutOfBounds(
21
+ targetRow: number,
22
+ targetCol: number,
23
+ spanRow: number,
24
+ spanCol: number,
25
+ maxRows: number,
26
+ maxColumns: number,
27
+ ): boolean {
28
+ const rowLimit = targetRow + spanRow - 1;
29
+ const colLimit = targetCol + spanCol - 1;
30
+
31
+ return rowLimit > maxRows || colLimit > maxColumns;
32
+ },
33
+
34
+ getCellAt(cells: CellData[], row: number, col: number): CellData | null {
35
+ return cells.find((cell) => cell.row === row && cell.col === col) ?? null;
36
+ },
37
+ };
@@ -0,0 +1,165 @@
1
+ import { CellId, CellIdUtils, CellData } from '../../../models';
2
+
3
+ export function getMaxColSpan(
4
+ cellId: CellId,
5
+ row: number,
6
+ col: number,
7
+ cells: CellData[],
8
+ columns: number,
9
+ ): number {
10
+ const currentCell = cells.find((c) => CellIdUtils.equals(c.cellId, cellId));
11
+ if (!currentCell) return 1;
12
+
13
+ // Start from current position and check each column until we hit a boundary or collision
14
+ let maxSpan = 1;
15
+
16
+ for (let testCol = col + 1; testCol <= columns; testCol++) {
17
+ // Check if this column is free for all rows the widget spans
18
+ let columnIsFree = true;
19
+
20
+ for (let testRow = row; testRow < row + currentCell.rowSpan; testRow++) {
21
+ const occupied = cells.some((cell) => {
22
+ if (CellIdUtils.equals(cell.cellId, cellId)) return false;
23
+
24
+ const wStartCol = cell.col;
25
+ const wEndCol = cell.col + cell.colSpan - 1;
26
+ const wStartRow = cell.row;
27
+ const wEndRow = cell.row + cell.rowSpan - 1;
28
+
29
+ return (
30
+ testCol >= wStartCol &&
31
+ testCol <= wEndCol &&
32
+ testRow >= wStartRow &&
33
+ testRow <= wEndRow
34
+ );
35
+ });
36
+
37
+ if (occupied) {
38
+ columnIsFree = false;
39
+ break;
40
+ }
41
+ }
42
+
43
+ if (!columnIsFree) {
44
+ break; // Hit a collision, stop here
45
+ }
46
+
47
+ maxSpan = testCol - col + 1; // Update max span to include this column
48
+ }
49
+
50
+ return maxSpan;
51
+ }
52
+
53
+ export function getMaxRowSpan(
54
+ cellId: CellId,
55
+ row: number,
56
+ col: number,
57
+ cells: CellData[],
58
+ rows: number,
59
+ ): number {
60
+ const currentCell = cells.find((c) => CellIdUtils.equals(c.cellId, cellId));
61
+ if (!currentCell) return 1;
62
+
63
+ // Start from current position and check each row until we hit a boundary or collision
64
+ let maxSpan = 1;
65
+
66
+ for (let testRow = row + 1; testRow <= rows; testRow++) {
67
+ // Check if this row is free for all columns the widget spans
68
+ let rowIsFree = true;
69
+
70
+ for (let testCol = col; testCol < col + currentCell.colSpan; testCol++) {
71
+ const occupied = cells.some((cell) => {
72
+ if (CellIdUtils.equals(cell.cellId, cellId)) return false;
73
+
74
+ const wStartRow = cell.row;
75
+ const wEndRow = cell.row + cell.rowSpan - 1;
76
+ const wStartCol = cell.col;
77
+ const wEndCol = cell.col + cell.colSpan - 1;
78
+
79
+ return (
80
+ testRow >= wStartRow &&
81
+ testRow <= wEndRow &&
82
+ testCol >= wStartCol &&
83
+ testCol <= wEndCol
84
+ );
85
+ });
86
+
87
+ if (occupied) {
88
+ rowIsFree = false;
89
+ break;
90
+ }
91
+ }
92
+
93
+ if (!rowIsFree) {
94
+ break; // Hit a collision, stop here
95
+ }
96
+
97
+ maxSpan = testRow - row + 1; // Update max span to include this row
98
+ }
99
+
100
+ return maxSpan;
101
+ }
102
+
103
+ export interface ResizeData {
104
+ cellId: CellId;
105
+ originalRowSpan: number;
106
+ originalColSpan: number;
107
+ previewRowSpan: number;
108
+ previewColSpan: number;
109
+ }
110
+
111
+ export function calculateResizePreview(
112
+ resizeData: ResizeData,
113
+ direction: 'horizontal' | 'vertical',
114
+ delta: number,
115
+ cells: CellData[],
116
+ rows: number,
117
+ columns: number,
118
+ ): { rowSpan: number; colSpan: number } | null {
119
+ const cell = cells.find((c) =>
120
+ CellIdUtils.equals(c.cellId, resizeData.cellId),
121
+ );
122
+ if (!cell) return null;
123
+
124
+ if (direction === 'horizontal') {
125
+ // Calculate the desired span based on the delta
126
+ const desiredColSpan = Math.max(1, resizeData.originalColSpan + delta);
127
+
128
+ // Get the maximum allowed span
129
+ const maxColSpan = getMaxColSpan(
130
+ cell.cellId,
131
+ cell.row,
132
+ cell.col,
133
+ cells,
134
+ columns,
135
+ );
136
+
137
+ // Clamp to the maximum
138
+ const newColSpan = Math.min(desiredColSpan, maxColSpan);
139
+
140
+ return {
141
+ rowSpan: resizeData.previewRowSpan,
142
+ colSpan: newColSpan,
143
+ };
144
+ } else {
145
+ // Calculate the desired span based on the delta
146
+ const desiredRowSpan = Math.max(1, resizeData.originalRowSpan + delta);
147
+
148
+ // Get the maximum allowed span
149
+ const maxRowSpan = getMaxRowSpan(
150
+ cell.cellId,
151
+ cell.row,
152
+ cell.col,
153
+ cells,
154
+ rows,
155
+ );
156
+
157
+ // Clamp to the maximum
158
+ const newRowSpan = Math.min(desiredRowSpan, maxRowSpan);
159
+
160
+ return {
161
+ rowSpan: newRowSpan,
162
+ colSpan: resizeData.previewColSpan,
163
+ };
164
+ }
165
+ }