@vc-shell/framework 1.1.0-alpha.4 → 1.1.0-alpha.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 (119) hide show
  1. package/core/composables/useMenuService/index.ts +20 -110
  2. package/core/composables/useWidgets/index.ts +2 -1
  3. package/core/plugins/modularity/index.ts +3 -3
  4. package/core/services/menu-service.ts +195 -0
  5. package/core/services/widget-service.ts +20 -0
  6. package/core/types/index.ts +1 -1
  7. package/dist/core/composables/useMenuService/index.d.ts +4 -10
  8. package/dist/core/composables/useMenuService/index.d.ts.map +1 -1
  9. package/dist/core/composables/useWidgets/index.d.ts +2 -1
  10. package/dist/core/composables/useWidgets/index.d.ts.map +1 -1
  11. package/dist/core/plugins/modularity/index.d.ts.map +1 -1
  12. package/dist/core/services/menu-service.d.ts +17 -0
  13. package/dist/core/services/menu-service.d.ts.map +1 -0
  14. package/dist/core/services/widget-service.d.ts +4 -0
  15. package/dist/core/services/widget-service.d.ts.map +1 -1
  16. package/dist/core/types/index.d.ts +1 -1
  17. package/dist/core/types/index.d.ts.map +1 -1
  18. package/dist/framework.js +220 -210
  19. package/dist/{index-CrxFDC2b.js → index-3ySdd-mG.js} +1 -1
  20. package/dist/{index-B1YR_MYV.js → index-B-nvqNbp.js} +1 -1
  21. package/dist/{index-xLYzNPa7.js → index-BQF2-UMe.js} +1 -1
  22. package/dist/{index-BBYyHeYA.js → index-BXlxP2d2.js} +1 -1
  23. package/dist/{index-Cf9Tz1ql.js → index-C7P-aBjd.js} +1 -1
  24. package/dist/{index-8LELHzw9.js → index-CO_2IshF.js} +1 -1
  25. package/dist/{index-BA98L1jI.js → index-CfyFpaKq.js} +1 -1
  26. package/dist/{index-DVljTjbf.js → index-Ci23AX3j.js} +1 -1
  27. package/dist/{index-D1JchciU.js → index-CyuFXG83.js} +1 -1
  28. package/dist/{index-CWKrD2Cd.js → index-D1rpRTKf.js} +1 -1
  29. package/dist/{index-BAeTsi-X.js → index-DLxTAT7x.js} +1 -1
  30. package/dist/{index-BuO5ByG9.js → index-DOVhosAY.js} +1 -1
  31. package/dist/{index-DLtsQ_PJ.js → index-DZAq0B3U.js} +23780 -23339
  32. package/dist/{index-BrUitdDo.js → index-DtkJ7xTB.js} +1 -1
  33. package/dist/{index-9lJxZE5w.js → index-DvGVm1rK.js} +1 -1
  34. package/dist/{index-RwX3kiZh.js → index-EDF1MDtU.js} +1 -1
  35. package/dist/{index-CJ5I7vTn.js → index-LjqdX6jw.js} +1 -1
  36. package/dist/index.css +1 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/injection-keys.d.ts +2 -0
  39. package/dist/injection-keys.d.ts.map +1 -1
  40. package/dist/shared/components/draggable-dashboard/DraggableDashboard.vue.d.ts +2 -0
  41. package/dist/shared/components/draggable-dashboard/DraggableDashboard.vue.d.ts.map +1 -1
  42. package/dist/shared/components/draggable-dashboard/composables/useCellSizeCalculator.d.ts +25 -0
  43. package/dist/shared/components/draggable-dashboard/composables/useCellSizeCalculator.d.ts.map +1 -0
  44. package/dist/shared/components/draggable-dashboard/composables/useCollisionDetection.d.ts +27 -0
  45. package/dist/shared/components/draggable-dashboard/composables/useCollisionDetection.d.ts.map +1 -0
  46. package/dist/shared/components/draggable-dashboard/composables/useDashboardDragAndDrop.d.ts +22 -0
  47. package/dist/shared/components/draggable-dashboard/composables/useDashboardDragAndDrop.d.ts.map +1 -1
  48. package/dist/shared/components/draggable-dashboard/composables/useDashboardGrid.d.ts +12 -4
  49. package/dist/shared/components/draggable-dashboard/composables/useDashboardGrid.d.ts.map +1 -1
  50. package/dist/shared/components/draggable-dashboard/composables/useDragClone.d.ts +15 -0
  51. package/dist/shared/components/draggable-dashboard/composables/useDragClone.d.ts.map +1 -0
  52. package/dist/shared/components/draggable-dashboard/composables/useEventCoordinates.d.ts +33 -0
  53. package/dist/shared/components/draggable-dashboard/composables/useEventCoordinates.d.ts.map +1 -0
  54. package/dist/shared/components/draggable-dashboard/composables/useGridPosition.d.ts +57 -0
  55. package/dist/shared/components/draggable-dashboard/composables/useGridPosition.d.ts.map +1 -0
  56. package/dist/shared/components/draggable-dashboard/composables/useGridSystem.d.ts +22 -0
  57. package/dist/shared/components/draggable-dashboard/composables/useGridSystem.d.ts.map +1 -0
  58. package/dist/shared/components/draggable-dashboard/composables/useLayoutPersistence.d.ts +19 -0
  59. package/dist/shared/components/draggable-dashboard/composables/useLayoutPersistence.d.ts.map +1 -0
  60. package/dist/shared/components/draggable-dashboard/composables/useResizeObserver.d.ts +18 -0
  61. package/dist/shared/components/draggable-dashboard/composables/useResizeObserver.d.ts.map +1 -0
  62. package/dist/shared/components/draggable-dashboard/composables/useWidgetLayout.d.ts +14 -0
  63. package/dist/shared/components/draggable-dashboard/composables/useWidgetLayout.d.ts.map +1 -0
  64. package/dist/shared/components/draggable-dashboard/composables/useWidgetStyles.d.ts +21 -0
  65. package/dist/shared/components/draggable-dashboard/composables/useWidgetStyles.d.ts.map +1 -0
  66. package/dist/shared/components/draggable-dashboard/types.d.ts +5 -1
  67. package/dist/shared/components/draggable-dashboard/types.d.ts.map +1 -1
  68. package/dist/tsconfig.tsbuildinfo +1 -1
  69. package/dist/ui/components/atoms/vc-icon/icons/FulfillmentCentersIcon.vue.d.ts +18 -0
  70. package/dist/ui/components/atoms/vc-icon/icons/FulfillmentCentersIcon.vue.d.ts.map +1 -0
  71. package/dist/ui/components/atoms/vc-icon/icons/OffersIcon.vue.d.ts +18 -0
  72. package/dist/ui/components/atoms/vc-icon/icons/OffersIcon.vue.d.ts.map +1 -0
  73. package/dist/ui/components/atoms/vc-icon/icons/OrdersIcon.vue.d.ts +18 -0
  74. package/dist/ui/components/atoms/vc-icon/icons/OrdersIcon.vue.d.ts.map +1 -0
  75. package/dist/ui/components/atoms/vc-icon/icons/PeopleIcon.vue.d.ts +18 -0
  76. package/dist/ui/components/atoms/vc-icon/icons/PeopleIcon.vue.d.ts.map +1 -0
  77. package/dist/ui/components/atoms/vc-icon/icons/ProductsIcon.vue.d.ts +18 -0
  78. package/dist/ui/components/atoms/vc-icon/icons/ProductsIcon.vue.d.ts.map +1 -0
  79. package/dist/ui/components/atoms/vc-icon/icons/ProfileIcon.vue.d.ts +18 -0
  80. package/dist/ui/components/atoms/vc-icon/icons/ProfileIcon.vue.d.ts.map +1 -0
  81. package/dist/ui/components/atoms/vc-icon/icons/index.d.ts +6 -0
  82. package/dist/ui/components/atoms/vc-icon/icons/index.d.ts.map +1 -1
  83. package/dist/ui/components/atoms/vc-icon/vc-icon.vue.d.ts.map +1 -1
  84. package/dist/ui/components/organisms/vc-app/_internal/vc-app-bar/components/app-bar-button/app-bar-button.vue.d.ts +1 -0
  85. package/dist/ui/components/organisms/vc-app/_internal/vc-app-bar/components/app-bar-button/app-bar-button.vue.d.ts.map +1 -1
  86. package/dist/ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/vc-app-menu-item.vue.d.ts +2 -1
  87. package/dist/ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/vc-app-menu-item.vue.d.ts.map +1 -1
  88. package/dist/ui/components/organisms/vc-app/vc-app.vue.d.ts.map +1 -1
  89. package/dist/ui/components/organisms/vc-table/vc-table.vue.d.ts.map +1 -1
  90. package/package.json +4 -4
  91. package/shared/components/draggable-dashboard/DraggableDashboard.vue +114 -148
  92. package/shared/components/draggable-dashboard/composables/useCellSizeCalculator.ts +121 -0
  93. package/shared/components/draggable-dashboard/composables/useCollisionDetection.ts +219 -0
  94. package/shared/components/draggable-dashboard/composables/useDashboardDragAndDrop.ts +126 -331
  95. package/shared/components/draggable-dashboard/composables/useDashboardGrid.ts +74 -220
  96. package/shared/components/draggable-dashboard/composables/useDragClone.ts +97 -0
  97. package/shared/components/draggable-dashboard/composables/useEventCoordinates.ts +91 -0
  98. package/shared/components/draggable-dashboard/composables/useGridPosition.ts +150 -0
  99. package/shared/components/draggable-dashboard/composables/useGridSystem.ts +169 -0
  100. package/shared/components/draggable-dashboard/composables/useLayoutPersistence.ts +89 -0
  101. package/shared/components/draggable-dashboard/composables/useResizeObserver.ts +105 -0
  102. package/shared/components/draggable-dashboard/composables/useWidgetLayout.ts +264 -0
  103. package/shared/components/draggable-dashboard/composables/useWidgetStyles.ts +120 -0
  104. package/shared/components/draggable-dashboard/types.ts +6 -1
  105. package/shared/components/notification-dropdown/notification-dropdown.vue +1 -0
  106. package/ui/components/atoms/vc-icon/icons/FulfillmentCentersIcon.vue +27 -0
  107. package/ui/components/atoms/vc-icon/icons/OffersIcon.vue +23 -0
  108. package/ui/components/atoms/vc-icon/icons/OrdersIcon.vue +19 -0
  109. package/ui/components/atoms/vc-icon/icons/PeopleIcon.vue +21 -0
  110. package/ui/components/atoms/vc-icon/icons/ProductsIcon.vue +23 -0
  111. package/ui/components/atoms/vc-icon/icons/ProfileIcon.vue +18 -0
  112. package/ui/components/atoms/vc-icon/icons/index.ts +6 -0
  113. package/ui/components/atoms/vc-icon/vc-icon.vue +101 -82
  114. package/ui/components/organisms/vc-app/_internal/vc-app-bar/components/app-bar-button/app-bar-button.vue +10 -3
  115. package/ui/components/organisms/vc-app/_internal/vc-app-bar/vc-app-bar.vue +3 -4
  116. package/ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/vc-app-menu-item.vue +2 -2
  117. package/ui/components/organisms/vc-app/_internal/vc-app-menu/vc-app-menu.vue +1 -0
  118. package/ui/components/organisms/vc-app/vc-app.vue +2 -3
  119. package/ui/components/organisms/vc-table/vc-table.vue +4 -1
@@ -0,0 +1,169 @@
1
+ import { ref, computed } from "vue";
2
+ import type { IDashboardWidget, DashboardWidgetPosition } from "../types";
3
+
4
+ // Константы для работы с сеткой
5
+ export const GRID_COLUMNS = 12;
6
+ export const MIN_GRID_ROWS = 12;
7
+ export const ROWS_BUFFER = 10;
8
+
9
+ /**
10
+ * Hook for working with the dashboard grid
11
+ *
12
+ * Provides basic functions and constants for working with the grid
13
+ *
14
+ * @returns An object with functions and constants for working with the grid
15
+ */
16
+ export function useGridSystem() {
17
+ // Dynamic number of rows in the grid
18
+ const dynamicRows = ref(MIN_GRID_ROWS);
19
+
20
+ /**
21
+ * Updates the number of rows based on the widget positions
22
+ *
23
+ * @param widgets The array of widgets
24
+ * @param layout The map of widget positions
25
+ * @returns The current number of rows
26
+ */
27
+ const updateGridRows = (widgets: IDashboardWidget[], layout: Map<string, DashboardWidgetPosition>): number => {
28
+ if (widgets.length === 0) {
29
+ dynamicRows.value = MIN_GRID_ROWS;
30
+ return dynamicRows.value;
31
+ }
32
+
33
+ // Find the maximum occupied row
34
+ const maxY = Math.max(
35
+ ...Array.from(layout.values()).map((pos, index) => {
36
+ const widget = widgets.find((w) => w.id === Array.from(layout.keys())[index]);
37
+ return widget ? pos.y + widget.size.height : pos.y + 1;
38
+ }),
39
+ 0,
40
+ );
41
+
42
+ // Set the number of rows with a buffer for scrolling
43
+ dynamicRows.value = Math.max(maxY + ROWS_BUFFER, MIN_GRID_ROWS);
44
+ return dynamicRows.value;
45
+ };
46
+
47
+ /**
48
+ * Checks if there is a collision (overlap) of the widget in the specified position
49
+ *
50
+ * @param widget The widget to check
51
+ * @param position The position to check
52
+ * @param widgets The array of all widgets
53
+ * @param layout The map of widget positions
54
+ * @param excludeWidgetId The ID of the widget to exclude from the check (optional)
55
+ * @returns true if there is a collision, false otherwise
56
+ */
57
+ const hasCollision = (
58
+ widget: IDashboardWidget,
59
+ position: DashboardWidgetPosition,
60
+ widgets: IDashboardWidget[],
61
+ layout: Map<string, DashboardWidgetPosition>,
62
+ excludeWidgetId?: string,
63
+ ): boolean => {
64
+ // Check if the position is out of the grid
65
+ if (position.x < 0 || position.x + widget.size.width > GRID_COLUMNS || position.y < 0) {
66
+ return true;
67
+ }
68
+
69
+ // Create a map of occupied cells for optimization
70
+ const occupiedCells = new Set<string>();
71
+
72
+ // Fill the map with occupied cells
73
+ widgets.forEach((other) => {
74
+ if (other.id === widget.id || (excludeWidgetId && other.id === excludeWidgetId)) {
75
+ return;
76
+ }
77
+
78
+ const otherPos = layout.get(other.id);
79
+ if (!otherPos) return;
80
+
81
+ for (let x = otherPos.x; x < otherPos.x + other.size.width; x++) {
82
+ for (let y = otherPos.y; y < otherPos.y + other.size.height; y++) {
83
+ occupiedCells.add(`${x},${y}`);
84
+ }
85
+ }
86
+ });
87
+
88
+ // Check for collisions for each cell of the widget
89
+ for (let x = position.x; x < position.x + widget.size.width; x++) {
90
+ for (let y = position.y; y < position.y + widget.size.height; y++) {
91
+ if (occupiedCells.has(`${x},${y}`)) {
92
+ return true;
93
+ }
94
+ }
95
+ }
96
+
97
+ return false;
98
+ };
99
+
100
+ /**
101
+ * Checks if the specified area is free for placement of the widget
102
+ *
103
+ * @param x The X coordinate
104
+ * @param y The Y coordinate
105
+ * @param width The width of the widget
106
+ * @param height The height of the widget
107
+ * @param occupiedCells The set of occupied cells
108
+ * @returns true if the area is free, false otherwise
109
+ */
110
+ const isAreaFree = (x: number, y: number, width: number, height: number, occupiedCells: Set<string>): boolean => {
111
+ // Check if the area is out of the grid
112
+ if (x < 0 || x + width > GRID_COLUMNS || y < 0) {
113
+ return false;
114
+ }
115
+
116
+ // Check each cell in the area
117
+ for (let ix = x; ix < x + width; ix++) {
118
+ for (let iy = y; iy < y + height; iy++) {
119
+ if (occupiedCells.has(`${ix},${iy}`)) {
120
+ return false;
121
+ }
122
+ }
123
+ }
124
+ return true;
125
+ };
126
+
127
+ /**
128
+ * Creates a map of occupied cells based on the current layout
129
+ *
130
+ * @param widgets The array of widgets
131
+ * @param layout The map of widget positions
132
+ * @returns The set of occupied cells in the format "x,y"
133
+ */
134
+ const createOccupiedCellsMap = (
135
+ widgets: IDashboardWidget[],
136
+ layout: Map<string, DashboardWidgetPosition>,
137
+ excludeWidgetId?: string,
138
+ ): Set<string> => {
139
+ const occupiedCells = new Set<string>();
140
+
141
+ widgets.forEach((widget) => {
142
+ if (excludeWidgetId && widget.id === excludeWidgetId) {
143
+ return;
144
+ }
145
+
146
+ const pos = layout.get(widget.id);
147
+ if (!pos) return;
148
+
149
+ for (let x = pos.x; x < pos.x + widget.size.width; x++) {
150
+ for (let y = pos.y; y < pos.y + widget.size.height; y++) {
151
+ occupiedCells.add(`${x},${y}`);
152
+ }
153
+ }
154
+ });
155
+
156
+ return occupiedCells;
157
+ };
158
+
159
+ return {
160
+ GRID_COLUMNS,
161
+ MIN_GRID_ROWS,
162
+ ROWS_BUFFER,
163
+ dynamicRows,
164
+ updateGridRows,
165
+ hasCollision,
166
+ isAreaFree,
167
+ createOccupiedCellsMap,
168
+ };
169
+ }
@@ -0,0 +1,89 @@
1
+ import { useLocalStorage } from "@vueuse/core";
2
+ import type { DashboardWidgetPosition } from "../types";
3
+
4
+ /**
5
+ * Hook for managing the saving and loading of the dashboard layout
6
+ *
7
+ * Provides functions for saving and loading the dashboard layout from localStorage
8
+ *
9
+ * @param storageKey The key for local storage
10
+ * @param updateCallback The function for updating the position of the widget
11
+ * @returns An object with functions for working with the saved layout
12
+ */
13
+ export function useLayoutPersistence(
14
+ storageKey: string,
15
+ updateCallback: (widgetId: string, position: DashboardWidgetPosition) => void,
16
+ ) {
17
+ // Use useLocalStorage for working with localStorage
18
+ const savedLayout = useLocalStorage<Record<string, DashboardWidgetPosition>>(storageKey, {});
19
+
20
+ /**
21
+ * Saves the current layout to localStorage
22
+ *
23
+ * @param layout The current layout to save
24
+ */
25
+ const saveLayout = (layout: Map<string, DashboardWidgetPosition>): void => {
26
+ try {
27
+ // Convert Map to an object for serialization
28
+ const layoutData: Record<string, DashboardWidgetPosition> = {};
29
+
30
+ layout.forEach((position, widgetId) => {
31
+ layoutData[widgetId] = position;
32
+ });
33
+
34
+ // Save to localStorage through the reactive variable
35
+ savedLayout.value = layoutData;
36
+ } catch (error) {
37
+ console.error("Failed to save dashboard layout to localStorage:", error);
38
+ }
39
+ };
40
+
41
+ /**
42
+ * Loads the saved layout from localStorage
43
+ *
44
+ * @param widgets The array of widgets to check for existence
45
+ * @returns true if the layout was successfully loaded, false otherwise
46
+ */
47
+ const loadLayout = <T extends { id: string }>(widgets: T[]): boolean => {
48
+ try {
49
+ // Check for the existence of the saved layout
50
+ if (!savedLayout.value || Object.keys(savedLayout.value).length === 0) {
51
+ return false;
52
+ }
53
+
54
+ // Apply positions to existing widgets
55
+ Object.entries(savedLayout.value).forEach(([widgetId, position]) => {
56
+ const widget = widgets.find((w) => w.id === widgetId);
57
+ if (widget) {
58
+ updateCallback(widgetId, position);
59
+ }
60
+ });
61
+
62
+ return true;
63
+ } catch (error) {
64
+ console.error("Failed to load dashboard layout from localStorage:", error);
65
+ return false;
66
+ }
67
+ };
68
+
69
+ /**
70
+ * Clears the saved layout from localStorage
71
+ */
72
+ const clearSavedLayout = (): void => {
73
+ savedLayout.value = {};
74
+ };
75
+
76
+ /**
77
+ * Checks if there is a saved layout in localStorage
78
+ */
79
+ const hasSavedLayout = (): boolean => {
80
+ return Object.keys(savedLayout.value).length > 0;
81
+ };
82
+
83
+ return {
84
+ saveLayout,
85
+ loadLayout,
86
+ clearSavedLayout,
87
+ hasSavedLayout,
88
+ };
89
+ }
@@ -0,0 +1,105 @@
1
+ import { ref, onUnmounted } from "vue";
2
+
3
+ /**
4
+ * Hook for managing ResizeObserver
5
+ *
6
+ * Provides functionality for tracking changes in the sizes of elements
7
+ *
8
+ * @param callback The function called when the sizes change
9
+ * @param options Options
10
+ * @returns An object with methods for working with ResizeObserver
11
+ */
12
+ export function useResizeObserver(
13
+ callback: (entries: ResizeObserverEntry[]) => void,
14
+ options?: {
15
+ debounceMs?: number;
16
+ },
17
+ ) {
18
+ // Instance of ResizeObserver
19
+ const observer = ref<ResizeObserver | null>(null);
20
+
21
+ // Timer identifier for debounce
22
+ const debounceTimer = ref<number | null>(null);
23
+
24
+ // Determine if the browser supports ResizeObserver
25
+ const isSupported = typeof window !== "undefined" && "ResizeObserver" in window;
26
+
27
+ /**
28
+ * The handler of the event of changing the sizes with debounce
29
+ *
30
+ * @param entries The array of ResizeObserverEntry
31
+ */
32
+ const handleResize = (entries: ResizeObserverEntry[]) => {
33
+ if (options?.debounceMs) {
34
+ // Clear the previous timer if it exists
35
+ if (debounceTimer.value !== null) {
36
+ window.clearTimeout(debounceTimer.value);
37
+ debounceTimer.value = null;
38
+ }
39
+
40
+ // Set a new timer
41
+ debounceTimer.value = window.setTimeout(() => {
42
+ callback(entries);
43
+ }, options.debounceMs);
44
+ } else {
45
+ // Call immediately without debounce
46
+ callback(entries);
47
+ }
48
+ };
49
+
50
+ /**
51
+ * Initializes ResizeObserver for the specified element
52
+ *
53
+ * @param element The element to monitor the sizes
54
+ */
55
+ const observe = (element: Element) => {
56
+ if (!isSupported) return;
57
+
58
+ // Create a new ResizeObserver if it doesn't exist
59
+ if (!observer.value) {
60
+ observer.value = new ResizeObserver(handleResize);
61
+ }
62
+
63
+ // Start monitoring the changes in the sizes
64
+ observer.value.observe(element);
65
+ };
66
+
67
+ /**
68
+ * Stops monitoring the changes in the sizes for the specified element
69
+ *
70
+ * @param element The element for which to stop monitoring
71
+ */
72
+ const unobserve = (element: Element) => {
73
+ if (!isSupported || !observer.value) return;
74
+
75
+ observer.value.unobserve(element);
76
+ };
77
+
78
+ /**
79
+ * Disconnects ResizeObserver and clears all resources
80
+ */
81
+ const disconnect = () => {
82
+ if (!isSupported || !observer.value) return;
83
+
84
+ observer.value.disconnect();
85
+ observer.value = null;
86
+
87
+ // Clear the debounce timer
88
+ if (debounceTimer.value !== null) {
89
+ window.clearTimeout(debounceTimer.value);
90
+ debounceTimer.value = null;
91
+ }
92
+ };
93
+
94
+ // Automatic cleanup when the component is unmounted
95
+ onUnmounted(() => {
96
+ disconnect();
97
+ });
98
+
99
+ return {
100
+ observe,
101
+ unobserve,
102
+ disconnect,
103
+ isSupported,
104
+ };
105
+ }
@@ -0,0 +1,264 @@
1
+ import { ref } from "vue";
2
+ import type { IDashboardWidget, DashboardWidgetPosition } from "../types";
3
+ import { useGridSystem } from "./useGridSystem";
4
+
5
+ // Constants for finding the optimal position
6
+ const SEARCH_DIRECTIONS = [
7
+ { dx: 0, dy: 0 }, // Current position
8
+ { dx: 0, dy: 1 }, // Down
9
+ { dx: 1, dy: 0 }, // Right
10
+ { dx: 0, dy: -1 }, // Up
11
+ { dx: -1, dy: 0 }, // Left
12
+ { dx: 1, dy: 1 }, // Right-down
13
+ { dx: -1, dy: 1 }, // Left-down
14
+ { dx: 1, dy: -1 }, // Right-up
15
+ { dx: -1, dy: -1 }, // Left-up
16
+ ];
17
+
18
+ /**
19
+ * Hook for managing the placement of widgets on the dashboard
20
+ *
21
+ * @param updatePositionCallback The function for updating the position of the widget
22
+ * @returns An object with functions for working with the placement of widgets
23
+ */
24
+ export function useWidgetLayout(updatePositionCallback: (widgetId: string, position: DashboardWidgetPosition) => void) {
25
+ const grid = useGridSystem();
26
+
27
+ /**
28
+ * Finds a free position for the widget
29
+ *
30
+ * @param widget The widget to place
31
+ * @param widgets The array of all widgets
32
+ * @param layout The map of widget positions
33
+ * @returns The free position for the widget
34
+ */
35
+ const findFreePosition = (
36
+ widget: IDashboardWidget,
37
+ widgets: IDashboardWidget[],
38
+ layout: Map<string, DashboardWidgetPosition>,
39
+ ): DashboardWidgetPosition => {
40
+ const currentPos = layout.get(widget.id);
41
+
42
+ // If the current position is free, use it
43
+ if (currentPos && !grid.hasCollision(widget, currentPos, widgets, layout, widget.id)) {
44
+ return currentPos;
45
+ }
46
+
47
+ // Start searching from the top left corner or the current position
48
+ const startX = currentPos?.x || 0;
49
+ const startY = currentPos?.y || 0;
50
+
51
+ // Spiral search for a free position
52
+ let layer = 0;
53
+ const maxLayers = Math.max(grid.GRID_COLUMNS, grid.dynamicRows.value);
54
+
55
+ while (layer < maxLayers) {
56
+ // Check all directions in the current layer
57
+ for (const { dx, dy } of SEARCH_DIRECTIONS) {
58
+ const x = startX + dx * layer;
59
+ const y = startY + dy * layer;
60
+
61
+ const position = { x, y };
62
+
63
+ // Check the position
64
+ if (!grid.hasCollision(widget, position, widgets, layout, widget.id)) {
65
+ return position;
66
+ }
67
+ }
68
+
69
+ layer++;
70
+ }
71
+
72
+ // If we didn't find a position, add to the end
73
+ return {
74
+ x: 0,
75
+ y: grid.dynamicRows.value,
76
+ };
77
+ };
78
+
79
+ /**
80
+ * Finds the optimal position for the widget considering the occupied cells
81
+ *
82
+ * @param widget The widget to place
83
+ * @param occupiedCells The set of occupied cells
84
+ * @param maxRows The maximum number of rows
85
+ * @returns The optimal position for the widget
86
+ */
87
+ const findOptimalPosition = (
88
+ widget: IDashboardWidget,
89
+ occupiedCells: Set<string>,
90
+ maxRows: number,
91
+ ): DashboardWidgetPosition => {
92
+ // Search for a position with the minimum Y, then with the minimum X
93
+ let bestPos: DashboardWidgetPosition = { x: 0, y: 0 };
94
+ let minY = Number.MAX_SAFE_INTEGER;
95
+
96
+ // Sliding window over the entire grid to find free space
97
+ for (let y = 0; y < maxRows; y++) {
98
+ for (let x = 0; x <= grid.GRID_COLUMNS - widget.size.width; x++) {
99
+ if (grid.isAreaFree(x, y, widget.size.width, widget.size.height, occupiedCells)) {
100
+ // Found free space
101
+ if (y < minY) {
102
+ minY = y;
103
+ bestPos = { x, y };
104
+ } else if (y === minY && x < bestPos.x) {
105
+ // If on the same row, choose the left one
106
+ bestPos = { x, y };
107
+ }
108
+
109
+ // If we found a place in the first row, return it immediately
110
+ if (y === 0) {
111
+ return bestPos;
112
+ }
113
+ }
114
+ }
115
+
116
+ // If we found something, and moved to the next row, stop searching
117
+ if (minY !== Number.MAX_SAFE_INTEGER && y > minY) {
118
+ break;
119
+ }
120
+ }
121
+
122
+ // If we didn't find a place, return the extreme position
123
+ if (minY === Number.MAX_SAFE_INTEGER) {
124
+ return { x: 0, y: maxRows };
125
+ }
126
+
127
+ return bestPos;
128
+ };
129
+
130
+ /**
131
+ * Places widgets in rows, minimizing empty spaces
132
+ *
133
+ * @param widgetsToArrange The array of widgets to place
134
+ * @param allWidgets The array of all widgets
135
+ * @param layout The map of widget positions
136
+ */
137
+ const arrangeWidgetsInRows = (
138
+ widgetsToArrange: IDashboardWidget[],
139
+ allWidgets: IDashboardWidget[],
140
+ layout: Map<string, DashboardWidgetPosition>,
141
+ ): void => {
142
+ if (widgetsToArrange.length === 0) return;
143
+
144
+ const sortedWidgets = [...widgetsToArrange].sort((a, b) => {
145
+ // Sort by size (bigger first)
146
+ const aSize = a.size.width * a.size.height;
147
+ const bSize = b.size.width * b.size.height;
148
+ return bSize - aSize;
149
+ });
150
+
151
+ // Current position for placement
152
+ let currentX = 0;
153
+ let currentY = 0;
154
+ let rowHeight = 0;
155
+
156
+ sortedWidgets.forEach((widget) => {
157
+ // Проверяем, помещается ли виджет в текущую строку
158
+ if (currentX + widget.size.width > grid.GRID_COLUMNS) {
159
+ // If it doesn't fit, move to the next row
160
+ currentX = 0;
161
+ currentY += rowHeight;
162
+ rowHeight = 0;
163
+ }
164
+
165
+ // Place the widget on the current position
166
+ const position = { x: currentX, y: currentY };
167
+ updatePositionCallback(widget.id, position);
168
+
169
+ // Update the current position
170
+ currentX += widget.size.width;
171
+
172
+ // Update the maximum height for the current row
173
+ rowHeight = Math.max(rowHeight, widget.size.height);
174
+ });
175
+ };
176
+
177
+ /**
178
+ * Initializes widgets with their built-in positions
179
+ *
180
+ * @param widgets The array of widgets
181
+ * @param layout The map of widget positions
182
+ * @returns true if at least one widget had a built-in position
183
+ */
184
+ const initializeWithBuiltInPositions = (
185
+ widgets: IDashboardWidget[],
186
+ layout: Map<string, DashboardWidgetPosition>,
187
+ ): boolean => {
188
+ if (widgets.length === 0) return false;
189
+
190
+ // Separate widgets into those that have built-in positions and those that don't
191
+ const widgetsWithPositions = widgets.filter((widget) => widget.position);
192
+ const widgetsWithoutPositions = widgets.filter((widget) => !widget.position);
193
+
194
+ // If there are no widgets with built-in positions, exit
195
+ if (widgetsWithPositions.length === 0) return false;
196
+
197
+ // Sort by rows and columns for more predictable placement
198
+ const sortedWidgets = [...widgetsWithPositions].sort((a, b) => {
199
+ if (!a.position || !b.position) return 0;
200
+ // First sort by y (rows)
201
+ if (a.position.y !== b.position.y) return a.position.y - b.position.y;
202
+ // Then by x (columns)
203
+ return a.position.x - b.position.x;
204
+ });
205
+
206
+ // Clear the current layout
207
+ layout.clear();
208
+
209
+ // Place widgets with given positions
210
+ sortedWidgets.forEach((widget) => {
211
+ if (widget.position) {
212
+ updatePositionCallback(widget.id, widget.position);
213
+ }
214
+ });
215
+
216
+ // Check and fix collisions for widgets with given positions
217
+ let hasCollisions;
218
+ do {
219
+ hasCollisions = false;
220
+ for (const widget of sortedWidgets) {
221
+ if (!widget.position) continue;
222
+
223
+ const position = layout.get(widget.id);
224
+ if (position && grid.hasCollision(widget, position, widgets, layout, widget.id)) {
225
+ hasCollisions = true;
226
+ const freePosition = findFreePosition(widget, widgets, layout);
227
+ updatePositionCallback(widget.id, freePosition);
228
+ }
229
+ }
230
+ } while (hasCollisions);
231
+
232
+ // Create a map of occupied cells for optimal placement of the remaining widgets
233
+ const occupiedCells = grid.createOccupiedCellsMap(widgets, layout);
234
+
235
+ // Sort the remaining widgets by size (bigger first)
236
+ const sortedRemaining = [...widgetsWithoutPositions].sort((a, b) => {
237
+ const aSize = a.size.width * a.size.height;
238
+ const bSize = b.size.width * b.size.height;
239
+ return bSize - aSize; // Bigger first
240
+ });
241
+
242
+ // Place the remaining widgets using the optimal algorithm
243
+ for (const widget of sortedRemaining) {
244
+ const position = findOptimalPosition(widget, occupiedCells, grid.dynamicRows.value + grid.ROWS_BUFFER);
245
+ updatePositionCallback(widget.id, position);
246
+
247
+ // Update the map of occupied cells
248
+ for (let x = position.x; x < position.x + widget.size.width; x++) {
249
+ for (let y = position.y; y < position.y + widget.size.height; y++) {
250
+ occupiedCells.add(`${x},${y}`);
251
+ }
252
+ }
253
+ }
254
+
255
+ return true;
256
+ };
257
+
258
+ return {
259
+ findFreePosition,
260
+ findOptimalPosition,
261
+ arrangeWidgetsInRows,
262
+ initializeWithBuiltInPositions,
263
+ };
264
+ }