@visns-studio/visns-components 5.2.3 → 5.2.4

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/package.json CHANGED
@@ -78,7 +78,7 @@
78
78
  "react-dom": "^17.0.0 || ^18.0.0"
79
79
  },
80
80
  "name": "@visns-studio/visns-components",
81
- "version": "5.2.3",
81
+ "version": "5.2.4",
82
82
  "description": "Various packages to assist in the development of our Custom Applications.",
83
83
  "main": "src/index.js",
84
84
  "files": [
@@ -1,5 +1,4 @@
1
1
  import '../../styles/global.css';
2
-
3
2
  import React, { useState, useEffect, useRef } from 'react';
4
3
  import { Link, useNavigate } from 'react-router-dom';
5
4
  import { ResponsiveBar } from '@nivo/bar';
@@ -18,29 +17,60 @@ import styles from './styles/GenericDashboard.module.scss';
18
17
 
19
18
  const toSnakeCase = (str) => str.replace(/[\s-]+/g, '_').toLowerCase();
20
19
 
21
- function GenericDashboard({ setting }) {
20
+ function GenericDashboard({ setting, userProfile }) {
22
21
  const swapy = useRef(null);
23
22
  const container = useRef(null);
24
23
  const navigate = useNavigate();
24
+ const [dashboardSetting, setDashboardSetting] = useState(
25
+ userProfile.dashboard_setting || setting
26
+ );
25
27
  const [data, setData] = useState({});
26
28
  const [dropdowns, setDropdowns] = useState({});
27
- const [filters, setFilters] = useState({}); // Track filter values for each widget
29
+ const [filters, setFilters] = useState({});
28
30
  const [modalShow, setModalShow] = useState(false);
29
31
  const [modalContent, setModalContent] = useState(null);
32
+ const [editMode, setEditMode] = useState(false);
33
+
34
+ const getDefaultDate = (defaultValue) => {
35
+ switch (defaultValue) {
36
+ case 'startOfMonth':
37
+ return dayjs().startOf('month').format('YYYY-MM-DD');
38
+ case 'endOfMonth':
39
+ return dayjs().endOf('month').format('YYYY-MM-DD');
40
+ default:
41
+ return '';
42
+ }
43
+ };
44
+
45
+ // Initialize filters based on each widget’s filters
46
+ useEffect(() => {
47
+ const initialFilters = {};
48
+ dashboardSetting.widgets.forEach((widget) => {
49
+ if (widget.filters) {
50
+ initialFilters[widget.id] = widget.filters.reduce(
51
+ (acc, filter) => {
52
+ if (filter.type === 'date' && filter.default) {
53
+ acc[filter.id] = getDefaultDate(filter.default);
54
+ } else if (filter.type === 'multi-dropdown-ajax') {
55
+ acc[filter.id] = [];
56
+ } else if (filter.type === 'years') {
57
+ acc[filter.id] = dayjs().year();
58
+ }
59
+ return acc;
60
+ },
61
+ {}
62
+ );
63
+ }
64
+ });
65
+ setFilters(initialFilters);
66
+ updateDashboardSetting(dashboardSetting);
67
+ }, [dashboardSetting]);
30
68
 
31
69
  const fetchData = async (appliedFilters = {}) => {
32
70
  try {
33
- // Flatten the nested widgets array into a single array
34
- const flattenedWidgets = setting.widgets.reduce(
35
- (acc, row) => [...acc, ...row],
36
- []
37
- );
38
-
39
- // Map through the flattened array of widgets
40
- const promises = flattenedWidgets.map(async (widget) => {
71
+ const promises = dashboardSetting.widgets.map(async (widget) => {
41
72
  const widgetFilters = appliedFilters[widget.id] || {};
42
73
 
43
- // Fetch dropdown data if required
44
74
  if (widget.filters) {
45
75
  for (const filter of widget.filters) {
46
76
  if (filter.url) {
@@ -68,7 +98,6 @@ function GenericDashboard({ setting }) {
68
98
  }
69
99
  }
70
100
 
71
- // Check if widget.api exists before making a request
72
101
  if (widget.api?.url && widget.api?.method) {
73
102
  return CustomFetch(
74
103
  widget.api.url,
@@ -76,15 +105,11 @@ function GenericDashboard({ setting }) {
76
105
  widgetFilters
77
106
  );
78
107
  }
79
-
80
- return null; // Return null if no API request is needed
108
+ return null;
81
109
  });
82
110
 
83
- // Execute all API requests
84
111
  const results = await Promise.all(promises);
85
-
86
- // Build a data object keyed by widget id
87
- const fetchedData = flattenedWidgets.reduce(
112
+ const fetchedData = dashboardSetting.widgets.reduce(
88
113
  (acc, widget, index) => {
89
114
  if (results[index]) {
90
115
  acc[widget.id] = results[index]?.data;
@@ -93,13 +118,16 @@ function GenericDashboard({ setting }) {
93
118
  },
94
119
  {}
95
120
  );
96
-
97
121
  setData(fetchedData);
98
122
  } catch (error) {
99
123
  console.error('Error fetching dashboard data:', error);
100
124
  }
101
125
  };
102
126
 
127
+ useEffect(() => {
128
+ fetchData(filters);
129
+ }, [filters]);
130
+
103
131
  const openModal = (widgetId) => {
104
132
  setModalContent(widgetId);
105
133
  setModalShow(true);
@@ -150,81 +178,64 @@ function GenericDashboard({ setting }) {
150
178
  }
151
179
  };
152
180
 
153
- const getDefaultDate = (defaultValue) => {
154
- switch (defaultValue) {
155
- case 'startOfMonth':
156
- return dayjs().startOf('month').format('YYYY-MM-DD');
157
- case 'endOfMonth':
158
- return dayjs().endOf('month').format('YYYY-MM-DD');
159
- default:
160
- return '';
181
+ const updateDashboardSetting = async (newSetting) => {
182
+ try {
183
+ if (userProfile.id && newSetting) {
184
+ await CustomFetch(`/ajax/users/${userProfile.id}/`, 'PUT', {
185
+ dashboard_setting: newSetting,
186
+ });
187
+ }
188
+ } catch (err) {
189
+ console.info(err);
161
190
  }
162
191
  };
163
192
 
164
- useEffect(() => {
165
- // Initialize filters with default values when settings change
166
- const initializeFilters = () => {
167
- const initialFilters = {};
168
-
169
- setting.widgets.forEach((row) => {
170
- row.forEach((widget) => {
171
- if (widget.filters) {
172
- initialFilters[widget.id] = widget.filters.reduce(
173
- (acc, filter) => {
174
- if (filter.type === 'date' && filter.default) {
175
- acc[filter.id] = getDefaultDate(
176
- filter.default
177
- );
178
- } else if (
179
- filter.type === 'multi-dropdown-ajax'
180
- ) {
181
- acc[filter.id] = [];
182
- } else if (filter.type === 'years') {
183
- acc[filter.id] = dayjs().year();
184
- }
185
- return acc;
186
- },
187
- {}
188
- );
193
+ // Cycle widget size: "1/3" → "half" → "2/3" → "full" (and reverse)
194
+ const changeSize = (widgetId, direction) => {
195
+ setDashboardSetting((prevSetting) => {
196
+ const updatedWidgets = prevSetting.widgets.map((widget) => {
197
+ if (widget.id === widgetId) {
198
+ const currentSize = widget.size || '1/3';
199
+ let newSize = currentSize;
200
+ const order = ['1/3', 'half', '2/3', 'full'];
201
+ const currentIndex = order.indexOf(currentSize);
202
+ if (
203
+ direction === 'increase' &&
204
+ currentIndex < order.length - 1
205
+ ) {
206
+ newSize = order[currentIndex + 1];
207
+ } else if (direction === 'decrease' && currentIndex > 0) {
208
+ newSize = order[currentIndex - 1];
189
209
  }
190
- });
210
+ return { ...widget, size: newSize };
211
+ }
212
+ return widget;
191
213
  });
214
+ const newSetting = { ...prevSetting, widgets: updatedWidgets };
215
+ updateDashboardSetting(newSetting);
216
+ return newSetting;
217
+ });
218
+ };
192
219
 
193
- setFilters(initialFilters);
194
- };
195
-
196
- initializeFilters();
197
- }, [setting]);
198
-
199
- useEffect(() => {
200
- fetchData(filters);
201
- }, [filters]);
202
-
203
- useEffect(() => {
204
- // If container element is loaded
205
- if (container.current) {
206
- swapy.current = createSwapy(container.current);
207
-
208
- // Your event listeners
209
- swapy.current.onSwap((event) => {
210
- console.log('swap', event);
211
- });
220
+ // Map widget size to a CSS class
221
+ const getGridClass = (size) => {
222
+ switch (size) {
223
+ case 'full':
224
+ return styles.grid__full;
225
+ case 'half':
226
+ return styles.grid__half;
227
+ case '2/3':
228
+ return styles.grid__twothird;
229
+ case '1/3':
230
+ default:
231
+ return styles.grid__three;
212
232
  }
213
-
214
- return () => {
215
- // Destroy the swapy instance on component destroy
216
- swapy.current?.destroy();
217
- };
218
- }, []);
233
+ };
219
234
 
220
235
  const renderFilters = (widget) => {
221
236
  if (!widget.filters || widget.filters.length === 0) return null;
222
-
223
237
  return (
224
238
  <div className={styles['filter-container']}>
225
- <div className={styles['filter-header']}>
226
- <h3>Filters</h3>
227
- </div>
228
239
  <div className={styles['filter-fields']}>
229
240
  {widget.filters.map((filter) => {
230
241
  switch (filter.type) {
@@ -257,7 +268,7 @@ function GenericDashboard({ setting }) {
257
268
  filter.id,
258
269
  e.target.value
259
270
  );
260
- e.target.blur(); // Closes the picker
271
+ e.target.blur();
261
272
  }}
262
273
  />
263
274
  </div>
@@ -328,7 +339,7 @@ function GenericDashboard({ setting }) {
328
339
  filter.id,
329
340
  e.target.value
330
341
  );
331
- e.target.blur(); // Closes the picker
342
+ e.target.blur();
332
343
  }}
333
344
  >
334
345
  {Array.from(
@@ -365,8 +376,7 @@ function GenericDashboard({ setting }) {
365
376
  ...prevFilters,
366
377
  [widget.id]: {},
367
378
  }));
368
-
369
- fetchData(); // Reset filters and fetch data
379
+ fetchData();
370
380
  }}
371
381
  >
372
382
  Clear Filters
@@ -379,7 +389,6 @@ function GenericDashboard({ setting }) {
379
389
 
380
390
  const renderWidget = (widget) => {
381
391
  const widgetData = data[widget.id] || [];
382
-
383
392
  if (
384
393
  widgetData.length > 0 ||
385
394
  widgetData?.data?.length > 0 ||
@@ -422,12 +431,10 @@ function GenericDashboard({ setting }) {
422
431
  return (
423
432
  <ResponsivePie data={widgetData} {...widget.props} />
424
433
  );
425
- case 'bar':
434
+ case 'bar': {
426
435
  if (widgetData.keys && widget.props.keys) {
427
436
  widget.props.keys = widgetData.keys;
428
437
  }
429
-
430
- // Build legendMapping and attach legendLabel and a custom tooltip if a legend is provided.
431
438
  let legendMapping = {};
432
439
  if (widget.legend) {
433
440
  legendMapping = widget.legend.reduce((acc, curr) => {
@@ -437,7 +444,6 @@ function GenericDashboard({ setting }) {
437
444
  widget.props.legendLabel = (e) =>
438
445
  legendMapping[e.id] || e.id;
439
446
 
440
- // Define a custom tooltip that uses the legendMapping
441
447
  const CustomTooltip = ({ id, value, color }) => {
442
448
  const label = legendMapping[id] || id;
443
449
  return (
@@ -457,10 +463,8 @@ function GenericDashboard({ setting }) {
457
463
 
458
464
  widget.props.tooltip = CustomTooltip;
459
465
  }
460
-
461
466
  const barData =
462
467
  widgetData.bar?.data || widgetData.data || widgetData;
463
-
464
468
  if (barData?.length > 0) {
465
469
  return (
466
470
  <div style={{ height: widget.height || '600px' }}>
@@ -472,6 +476,7 @@ function GenericDashboard({ setting }) {
472
476
  );
473
477
  }
474
478
  break;
479
+ }
475
480
  case 'line':
476
481
  if (widgetData.length > 0) {
477
482
  return (
@@ -481,6 +486,7 @@ function GenericDashboard({ setting }) {
481
486
  />
482
487
  );
483
488
  }
489
+ break;
484
490
  case 'list':
485
491
  return (
486
492
  <ul className={styles.dashList}>
@@ -520,10 +526,9 @@ function GenericDashboard({ setting }) {
520
526
  )}
521
527
  </ul>
522
528
  );
523
- case 'table':
529
+ case 'table': {
524
530
  const tableData = widgetData.data || widgetData;
525
531
  const tableKeys = widgetData.keys || [];
526
-
527
532
  return (
528
533
  <div className={styles['table-container']}>
529
534
  {tableData.length > 0 ? (
@@ -581,14 +586,13 @@ function GenericDashboard({ setting }) {
581
586
  {tableKeys && tableKeys.length > 0
582
587
  ? tableKeys.map(
583
588
  (type, typeIndex) => {
584
- let total = 0; // Track total for the current form type
589
+ let total = 0;
585
590
  return (
586
591
  <tr
587
592
  key={`form-type-${toSnakeCase(
588
593
  type
589
594
  )}`}
590
595
  >
591
- {/* <td>{type}</td> */}
592
596
  {widget.total
593
597
  .column &&
594
598
  tableData.map(
@@ -608,7 +612,6 @@ function GenericDashboard({ setting }) {
608
612
  widget
609
613
  .axisKeys
610
614
  .xAxis;
611
-
612
615
  const column =
613
616
  row[
614
617
  columnKey
@@ -627,7 +630,6 @@ function GenericDashboard({ setting }) {
627
630
  : 0;
628
631
  total +=
629
632
  count;
630
-
631
633
  return (
632
634
  <td
633
635
  key={`${toSnakeCase(
@@ -680,8 +682,6 @@ function GenericDashboard({ setting }) {
680
682
  </tr>
681
683
  )
682
684
  )}
683
-
684
- {/* Add a total row for supervisors */}
685
685
  {widget.total.column && (
686
686
  <tr>
687
687
  <td>
@@ -707,7 +707,6 @@ function GenericDashboard({ setting }) {
707
707
  column.form_template_label ===
708
708
  type
709
709
  );
710
-
711
710
  return (
712
711
  sum +
713
712
  (column
@@ -721,7 +720,6 @@ function GenericDashboard({ setting }) {
721
720
  },
722
721
  0
723
722
  );
724
-
725
723
  return (
726
724
  <td
727
725
  key={`${toSnakeCase(
@@ -752,6 +750,7 @@ function GenericDashboard({ setting }) {
752
750
  )}
753
751
  </div>
754
752
  );
753
+ }
755
754
  default:
756
755
  return null;
757
756
  }
@@ -760,87 +759,162 @@ function GenericDashboard({ setting }) {
760
759
  }
761
760
  };
762
761
 
763
- const renderRows = () => {
764
- return setting.widgets.map((row, rowIndex) => {
765
- // Use the number of widgets in the row to determine the grid class
766
- const columns = row.length;
767
- const gridClass =
768
- columns === 1
769
- ? styles.grid__full
770
- : columns === 2
771
- ? styles.grid__half
772
- : columns === 3
773
- ? styles.grid__three
774
- : styles.grid__three; // fallback if more than 3 columns
762
+ // Drag-and-drop swap handling. Each widget is given a slot based on its index.
763
+ const handleWidgetSwap = (event) => {
764
+ const parseSlot = (slot) => {
765
+ // Slot format: "widget-slot-INDEX"
766
+ const parts = slot.split('-');
767
+ return parseInt(parts[2], 10);
768
+ };
769
+
770
+ const fromIndex = parseSlot(event.fromSlot);
771
+ const toIndex = parseSlot(event.toSlot);
772
+
773
+ setDashboardSetting((prevSetting) => {
774
+ const newWidgets = [...prevSetting.widgets];
775
+ [newWidgets[fromIndex], newWidgets[toIndex]] = [
776
+ newWidgets[toIndex],
777
+ newWidgets[fromIndex],
778
+ ];
779
+ const newSetting = { ...prevSetting, widgets: newWidgets };
780
+ updateDashboardSetting(newSetting);
781
+ return newSetting;
782
+ });
783
+ };
775
784
 
785
+ useEffect(() => {
786
+ if (editMode && container.current) {
787
+ swapy.current = createSwapy(container.current, {
788
+ manualSwap: true,
789
+ });
790
+ swapy.current.onSwap((event) => {
791
+ console.log('swap event', event);
792
+ handleWidgetSwap(event);
793
+ });
794
+ }
795
+ return () => {
796
+ swapy.current?.destroy();
797
+ swapy.current = null;
798
+ };
799
+ }, [editMode]);
800
+
801
+ const renderWidgets = () => {
802
+ return dashboardSetting.widgets.map((widget, index) => {
776
803
  return (
777
- <div className={styles.grid__row} key={`row-${rowIndex}`}>
778
- {row.map((widget) => (
779
- <div
780
- key={widget.id}
781
- className={`${gridClass} ${
782
- widget.type !== 'list'
783
- ? widget.highlight
784
- ? styles['dashwidget--highlight']
785
- : styles.dashwidget
786
- : ''
787
- }`}
788
- data-swapy-slot={`row-slot-${widget.id}-${rowIndex}`}
789
- >
790
- <div data-swapy-item={`widget-${widget.id}`}>
791
- <div className={styles.widgetTitle}>
792
- <h2>{widget.title}</h2>
793
- </div>
794
- {renderFilters(widget)}
795
- <div>{renderWidget(widget)}</div>
796
- {widget.modal && (
797
- <div className={styles.widgetAction}>
798
- <button
799
- className={`${styles.btn} ${styles.dmore}`}
800
- onClick={() => openModal(widget.id)}
801
- >
802
- View all{' '}
803
- <span>
804
- ({data[widget.id]?.length || 0})
805
- </span>
806
- </button>
807
- </div>
804
+ <div
805
+ key={widget.id}
806
+ className={`${getGridClass(widget.size)} ${
807
+ widget.type !== 'list'
808
+ ? widget.highlight
809
+ ? styles['dashwidget--highlight']
810
+ : styles.dashwidget
811
+ : ''
812
+ } ${
813
+ editMode
814
+ ? widget.highlight
815
+ ? styles.editModeWidgetHighlight
816
+ : styles.editModeWidgetNormal
817
+ : ''
818
+ }`}
819
+ {...(editMode && {
820
+ 'data-swapy-slot': `widget-slot-${index}`,
821
+ })}
822
+ >
823
+ <div
824
+ {...(editMode && {
825
+ 'data-swapy-item': `widget-${widget.id}`,
826
+ })}
827
+ className={`${styles.widgetItem}`}
828
+ >
829
+ <div className={styles.widgetTitle}>
830
+ <h2>{widget.title}</h2>
831
+ </div>
832
+ {renderFilters(widget)}
833
+ <div>{renderWidget(widget)}</div>
834
+ {widget.modal && (
835
+ <div className={styles.widgetAction}>
836
+ <button
837
+ className={`${styles.btn} ${styles.dmore}`}
838
+ onClick={() => openModal(widget.id)}
839
+ >
840
+ View all{' '}
841
+ <span>
842
+ ({data[widget.id]?.length || 0})
843
+ </span>
844
+ </button>
845
+ </div>
846
+ )}
847
+ {editMode && (
848
+ <div className={styles.resizeTools}>
849
+ {(widget.size || '1/3') !== '1/3' && (
850
+ <button
851
+ onClick={() =>
852
+ changeSize(widget.id, 'decrease')
853
+ }
854
+ >
855
+
856
+ </button>
857
+ )}
858
+ {(widget.size || '1/3') !== 'full' && (
859
+ <button
860
+ onClick={() =>
861
+ changeSize(widget.id, 'increase')
862
+ }
863
+ >
864
+ +
865
+ </button>
808
866
  )}
809
867
  </div>
810
- </div>
811
- ))}
868
+ )}
869
+ </div>
812
870
  </div>
813
871
  );
814
872
  });
815
873
  };
816
874
 
817
875
  return (
818
- <div className={styles.grid} ref={container}>
819
- {renderRows()}
820
- <Popup open={modalShow} onClose={closeModal}>
821
- <div className="modalwrap top--modal modalWide">
822
- <div className="modal">
823
- <div className="modal__header">
824
- <h1>{setting.modalTitles?.[modalContent]}</h1>
825
- <button
826
- className="modal__close"
827
- onClick={closeModal}
828
- >
829
- <CircleX strokeWidth={1} size={24} />
830
- </button>
831
- </div>
832
- <div className="modal__content">
833
- {modalContent &&
834
- renderWidget(
835
- setting.widgets.find(
836
- (w) => w.id === modalContent
837
- )
838
- )}
876
+ <>
877
+ <div className={styles.editButtonContainer}>
878
+ <button
879
+ className={styles.btn}
880
+ onClick={() => setEditMode((prev) => !prev)}
881
+ >
882
+ {editMode ? 'Done' : 'Edit'}
883
+ </button>
884
+ </div>
885
+ <div className={styles.grid} ref={container}>
886
+ {renderWidgets()}
887
+ <Popup open={modalShow} onClose={closeModal}>
888
+ <div className="modalwrap top--modal modalWide">
889
+ <div className="modal">
890
+ <div className="modal__header">
891
+ <h1>
892
+ {
893
+ dashboardSetting.modalTitles?.[
894
+ modalContent
895
+ ]
896
+ }
897
+ </h1>
898
+ <button
899
+ className="modal__close"
900
+ onClick={closeModal}
901
+ >
902
+ <CircleX strokeWidth={1} size={24} />
903
+ </button>
904
+ </div>
905
+ <div className="modal__content">
906
+ {modalContent &&
907
+ renderWidget(
908
+ dashboardSetting.widgets.find(
909
+ (w) => w.id === modalContent
910
+ )
911
+ )}
912
+ </div>
839
913
  </div>
840
914
  </div>
841
- </div>
842
- </Popup>
843
- </div>
915
+ </Popup>
916
+ </div>
917
+ </>
844
918
  );
845
919
  }
846
920
 
@@ -1,14 +1,12 @@
1
- .grid__row {
2
- width: 100%;
1
+ /* New grid container */
2
+ .grid {
3
3
  display: flex;
4
- align-items: flex-start;
5
- flex-wrap: nowrap;
6
- height: auto;
4
+ flex-wrap: wrap;
7
5
  gap: 1rem;
8
6
  }
9
7
 
10
8
  .grid__three {
11
- width: 33%;
9
+ flex: 0 0 calc(33.33% - 0.67rem);
12
10
  background: var(--item-color);
13
11
  border-radius: var(--br);
14
12
  border: 1px solid rgba(var(--item-color-rgb), 1.15);
@@ -21,10 +19,10 @@
21
19
  }
22
20
 
23
21
  .grid__half {
24
- width: 50%;
22
+ flex: 0 0 calc(50% - 0.5rem);
25
23
  background: var(--item-color);
26
- border-radius: 6px;
27
- border: 1px solid #dadce0;
24
+ border-radius: var(--br);
25
+ border: 1px solid rgba(var(--item-color-rgb), 1.15);
28
26
  box-sizing: border-box;
29
27
  overflow: visible;
30
28
  position: relative;
@@ -34,23 +32,23 @@
34
32
  }
35
33
 
36
34
  .grid__full {
37
- width: 100%;
35
+ flex: 0 0 100%;
38
36
  background: var(--item-color);
39
37
  border-radius: var(--br);
40
38
  border: 1px solid rgba(var(--item-color-rgb), 1.15);
41
39
  box-sizing: border-box;
42
40
  overflow: visible;
43
41
  position: relative;
44
- padding: 10px;
42
+ padding: 20px;
45
43
  margin: 8px 0;
46
44
  box-shadow: 0 4px 0 rgba(var(--primary-rgb), 0.05);
47
45
  }
48
46
 
49
- .grid__dashfull {
50
- width: 100%;
47
+ .grid__twothird {
48
+ flex: 0 0 calc(66.66% - 0.67rem);
51
49
  background: var(--item-color);
52
- border-radius: 6px;
53
- border: 1px solid #dadce0;
50
+ border-radius: var(--br);
51
+ border: 1px solid rgba(var(--item-color-rgb), 1.15);
54
52
  box-sizing: border-box;
55
53
  overflow: visible;
56
54
  position: relative;
@@ -59,21 +57,21 @@
59
57
  box-shadow: 0 4px 0 rgba(var(--primary-rgb), 0.05);
60
58
  }
61
59
 
60
+ /* The remaining CSS remains largely unchanged */
62
61
  .widgetTitle {
63
62
  width: 100%;
64
63
  height: auto;
65
64
  padding: 0;
66
65
  margin-bottom: 0.85rem;
67
-
68
- h2 {
69
- font-size: 1rem;
70
- margin: 0;
71
- padding: 0;
72
- line-height: 1;
73
- text-transform: capitalize;
74
- text-align: left;
75
- color: var(--primary-color);
76
- }
66
+ }
67
+ .widgetTitle h2 {
68
+ font-size: 1rem;
69
+ margin: 0;
70
+ padding: 0;
71
+ line-height: 1;
72
+ text-transform: capitalize;
73
+ text-align: left;
74
+ color: var(--primary-color);
77
75
  }
78
76
 
79
77
  .dashList {
@@ -430,3 +428,93 @@
430
428
  .table-container th:last-child {
431
429
  border-right: none;
432
430
  }
431
+
432
+ // Indicates that the widget is in edit mode with a glowing dashed border.
433
+ .editModeWidget {
434
+ border: 2px dashed var(--primary-color);
435
+ box-shadow: 0 0 10px var(--primary-color);
436
+ }
437
+
438
+ // Container for the edit mode toggle button.
439
+ .editButtonContainer {
440
+ display: flex;
441
+ justify-content: flex-end;
442
+ margin-bottom: 1rem;
443
+ }
444
+
445
+ .editButtonContainer .btn {
446
+ padding: 0.5rem 1rem;
447
+ background: var(--primary-color);
448
+ color: var(--tertiary-color);
449
+ border: none;
450
+ border-radius: var(--br);
451
+ font-size: 0.875rem;
452
+ font-weight: 600;
453
+ text-transform: uppercase;
454
+ cursor: pointer;
455
+ transition: background-color 0.25s ease;
456
+ }
457
+
458
+ .editButtonContainer .btn:hover {
459
+ background-color: var(--secondary-color);
460
+ }
461
+
462
+ // Normal edit mode style for non-highlighted widgets
463
+ .editModeWidgetNormal {
464
+ border: 2px dashed var(--primary-color);
465
+ box-shadow: 0 0 10px var(--primary-color);
466
+
467
+ // Use the default resize tools positioning for normal widgets.
468
+ .resizeTools {
469
+ top: -5px;
470
+ right: 0px;
471
+ }
472
+ }
473
+
474
+ // Edit mode style for highlighted widgets (using a contrasting color)
475
+ .editModeWidgetHighlight {
476
+ border: 2px dashed var(--secondary-color);
477
+ box-shadow: 0 0 10px var(--secondary-color);
478
+
479
+ // Position resize tools further up to avoid blending with the background.
480
+ .resizeTools {
481
+ top: -5px; // Adjust this value as needed
482
+ right: 0px;
483
+ }
484
+ }
485
+
486
+ // Resizing tool container (common styling)
487
+ .resizeTools {
488
+ position: absolute;
489
+ display: flex;
490
+ gap: 5px;
491
+
492
+ button {
493
+ background: var(--primary-color);
494
+ color: var(--tertiary-color);
495
+ border: none;
496
+ border-radius: 3px;
497
+ padding: 0.25rem 0.5rem;
498
+ cursor: pointer;
499
+ font-size: 0.75rem;
500
+ transition: background-color 0.2s;
501
+
502
+ &:hover {
503
+ background-color: rgba(var(--primary-rgb), 0.8);
504
+ }
505
+ }
506
+ }
507
+
508
+ // Override the resize button for highlighted widgets
509
+ .editModeWidgetHighlight .resizeTools button {
510
+ background: var(--tertiary-color); // Use a contrasting background
511
+ color: var(--primary-color); // Ensure the text is legible
512
+
513
+ &:hover {
514
+ background-color: rgba(var(--tertiary-color-rgb), 0.8);
515
+ }
516
+ }
517
+
518
+ .widgetItem {
519
+ position: relative;
520
+ }