@visns-studio/visns-components 5.6.6 → 5.6.7

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 CHANGED
@@ -59,6 +59,7 @@ VISNS Components is a React-based UI component library that provides a set of re
59
59
  - **Notification Pages** - User notification center
60
60
  - **Sorting Pages** - Content ordering interfaces
61
61
  - **Report Pages** - Custom report building and execution
62
+ - **Audit Logs** - System activity tracking and history
62
63
 
63
64
  ## Installation
64
65
 
@@ -224,6 +225,34 @@ A powerful data table component with sorting, filtering, and pagination.
224
225
  | setConfig | Callback for configuration changes | Function | - |
225
226
  | style | Custom styles | Object | - |
226
227
 
228
+ #### Auto-Refresh Functionality
229
+
230
+ The DataGrid component includes an auto-refresh feature that automatically reloads data at specified intervals. This is useful for displaying real-time or frequently updated data.
231
+
232
+ To enable auto-refresh:
233
+
234
+ ```jsx
235
+ <DataGrid
236
+ ajaxSetting={{
237
+ url: '/api/data',
238
+ method: 'GET',
239
+ params: {},
240
+ autoRefresh: 30, // Refresh every 30 seconds
241
+ // or
242
+ autoRefresh: true, // Use default 30-second interval
243
+ }}
244
+ // Other props...
245
+ />
246
+ ```
247
+
248
+ When auto-refresh is configured:
249
+
250
+ - A checkbox appears in the DataGrid header to enable/disable auto-refresh
251
+ - Auto-refresh is enabled by default when configured
252
+ - The interval can be customized (in seconds) by setting a numeric value
253
+ - Setting `autoRefresh: true` uses the default 30-second interval
254
+ - The component silently refreshes data without showing notifications
255
+
227
256
  ### Form Components
228
257
 
229
258
  #### Form
@@ -336,7 +365,7 @@ File upload component with drag and drop support.
336
365
 
337
366
  #### CustomFetch
338
367
 
339
- Data fetching utility with error handling.
368
+ Data fetching utility with error handling and payload size validation.
340
369
 
341
370
  ```jsx
342
371
  // Using async/await
@@ -358,6 +387,15 @@ CustomFetch(
358
387
  );
359
388
  ```
360
389
 
390
+ The CustomFetch component includes these features:
391
+
392
+ - **Payload Size Validation**: Automatically checks if the request payload exceeds the maximum allowed size (default: 4.5MB, configurable via `VITE_MAX_PAYLOAD_SIZE_MB` environment variable)
393
+ - **Toast Notifications**: Displays error messages using toast notifications
394
+ - **Promise Tracking**: Integrates with react-promise-tracker for loading indicators
395
+ - **HTML Error Parsing**: Properly renders HTML content in error messages
396
+ - **CSRF Protection**: Redirects to login page on CSRF token mismatch
397
+ - **Authentication Handling**: Silently handles unauthenticated errors without showing toast notifications
398
+
361
399
  #### Download
362
400
 
363
401
  File download utility.
@@ -404,6 +442,53 @@ The QrCode component fetches QR code data from the specified URL and displays it
404
442
  }
405
443
  ```
406
444
 
445
+ #### AuditLogs and AuditLog
446
+
447
+ The AuditLogs component displays a table of system activity logs, showing who made changes, what was changed, and when the changes occurred.
448
+
449
+ ```jsx
450
+ <AuditLogs layout="full" />
451
+ ```
452
+
453
+ The AuditLog component displays detailed information about a specific audit log entry.
454
+
455
+ ```jsx
456
+ <AuditLog userProfile={userProfile} />
457
+ ```
458
+
459
+ The AuditLogs component features:
460
+
461
+ - User tracking - Shows which user performed the action
462
+ - Event tracking - Displays the type of action (create, update, delete)
463
+ - Value comparison - Shows old and new values for changed fields
464
+ - IP address logging - Records the IP address of the user
465
+ - Timestamp recording - Shows when the action occurred
466
+ - Formatted display - Presents JSON data in a readable format
467
+
468
+ The component expects audit log data from the API with the following structure:
469
+
470
+ ```json
471
+ {
472
+ "id": 123,
473
+ "user": {
474
+ "id": 1,
475
+ "name": "John Doe"
476
+ },
477
+ "old_values": {
478
+ "field1": "old value",
479
+ "field2": "old value"
480
+ },
481
+ "new_values": {
482
+ "field1": "new value",
483
+ "field2": "new value"
484
+ },
485
+ "formatted_event": "Updating",
486
+ "ip_address": "192.168.1.1",
487
+ "url": "/api/resource/123",
488
+ "created_at": "2023-01-01T12:00:00Z"
489
+ }
490
+ ```
491
+
407
492
  ## Two-Factor Authentication (2FA)
408
493
 
409
494
  ## Client Portal
package/package.json CHANGED
@@ -84,7 +84,7 @@
84
84
  "react-dom": "^17.0.0 || ^18.0.0"
85
85
  },
86
86
  "name": "@visns-studio/visns-components",
87
- "version": "5.6.6",
87
+ "version": "5.6.7",
88
88
  "description": "Various packages to assist in the development of our Custom Applications.",
89
89
  "main": "src/index.js",
90
90
  "files": [
@@ -17,7 +17,6 @@ import numeral from 'numeral';
17
17
  import Popup from 'reactjs-popup';
18
18
  import 'reactjs-popup/dist/index.css';
19
19
  import parse from 'html-react-parser';
20
- import 'react-toggle/style.css';
21
20
  import { saveAs } from 'file-saver';
22
21
  import NumberFilter from '@visns-studio/visns-datagrid-enterprise/NumberFilter';
23
22
  import SelectFilter from '@visns-studio/visns-datagrid-enterprise/SelectFilter';
@@ -386,10 +385,51 @@ const DataGrid = forwardRef(
386
385
  const [localInputValues, setLocalInputValues] = useState({});
387
386
  const localInputValuesRef = useRef({}); // Add this ref to track values
388
387
 
388
+ /** Auto Refresh States */
389
+ const [autoRefreshEnabled, setAutoRefreshEnabled] = useState(false);
390
+ const [refreshInterval, setRefreshInterval] = useState(30); // Default 30 seconds interval
391
+ const refreshTimerRef = useRef(null);
392
+ const autoRefreshInitialized = useRef(false); // Track if we've initialized auto-refresh
393
+
394
+ /** Responsive Modal States */
395
+ const [windowWidth, setWindowWidth] = useState(window.innerWidth);
396
+
389
397
  useEffect(() => {
390
398
  localInputValuesRef.current = localInputValues;
391
399
  }, [localInputValues]);
392
400
 
401
+ // Effect to handle window resize for responsive modals
402
+ useEffect(() => {
403
+ const handleResize = () => {
404
+ setWindowWidth(window.innerWidth);
405
+ };
406
+
407
+ window.addEventListener('resize', handleResize);
408
+
409
+ // Clean up event listener on unmount
410
+ return () => {
411
+ window.removeEventListener('resize', handleResize);
412
+ };
413
+ }, []);
414
+
415
+ // Effect to handle autoRefresh setting
416
+ useEffect(() => {
417
+ if (ajaxSetting && ajaxSetting.autoRefresh !== undefined) {
418
+ // Enable auto-refresh by default when the setting is present
419
+ if (!autoRefreshInitialized.current) {
420
+ setAutoRefreshEnabled(true);
421
+ autoRefreshInitialized.current = true;
422
+ }
423
+
424
+ // Set the refresh interval based on the provided value
425
+ if (typeof ajaxSetting.autoRefresh === 'number') {
426
+ setRefreshInterval(ajaxSetting.autoRefresh);
427
+ } else if (ajaxSetting.autoRefresh === true) {
428
+ setRefreshInterval(30); // Default 30 seconds
429
+ }
430
+ }
431
+ }, [ajaxSetting]);
432
+
393
433
  /** Table Functions */
394
434
  useImperativeHandle(ref, () => ({
395
435
  reload: () => {
@@ -754,8 +794,6 @@ const DataGrid = forwardRef(
754
794
  // Extracting nested value using predefined keys from 's.relation'
755
795
  const nestedValue = getNestedValue(d, s.relation);
756
796
 
757
- console.info(d, s.relation);
758
-
759
797
  // Check if nestedValue is defined and is an array before mapping
760
798
  if (!nestedValue || !Array.isArray(nestedValue)) {
761
799
  console.warn(
@@ -1243,11 +1281,33 @@ const DataGrid = forwardRef(
1243
1281
  className={styles.searchInput}
1244
1282
  />
1245
1283
  </div>
1246
- {!isCreateDisabled && (
1247
- <div className={styles.actionButtonsWrapper}>
1248
- {renderCreateButton()}
1249
- </div>
1250
- )}
1284
+ <div className={styles.actionButtonsWrapper}>
1285
+ {!isCreateDisabled && renderCreateButton()}
1286
+ </div>
1287
+ </div>
1288
+ );
1289
+ };
1290
+
1291
+ const renderAutoRefreshControls = () => {
1292
+ // Only show auto-refresh if the setting is present in ajaxSetting
1293
+ if (!ajaxSetting || ajaxSetting.autoRefresh === undefined) {
1294
+ return null;
1295
+ }
1296
+
1297
+ return (
1298
+ <div className={styles.autoRefreshContainer}>
1299
+ <label className={styles.autoRefreshLabel}>
1300
+ <input
1301
+ type="checkbox"
1302
+ checked={autoRefreshEnabled}
1303
+ onChange={() =>
1304
+ setAutoRefreshEnabled(!autoRefreshEnabled)
1305
+ }
1306
+ className={styles.autoRefreshCheckbox}
1307
+ />
1308
+ Auto Refresh{' '}
1309
+ {refreshInterval && `(${refreshInterval}s)`}
1310
+ </label>
1251
1311
  </div>
1252
1312
  );
1253
1313
  };
@@ -1259,7 +1319,7 @@ const DataGrid = forwardRef(
1259
1319
  if (linkUrl) {
1260
1320
  const baseUrl = window.location.origin;
1261
1321
  const fullUrl = `${baseUrl}${linkUrl}${
1262
- rowProps.data[form.primaryKey]
1322
+ rowProps.data[cellProps?.link?.key || form.primaryKey]
1263
1323
  }`;
1264
1324
 
1265
1325
  menuProps.autoDismiss = true;
@@ -3146,6 +3206,34 @@ const DataGrid = forwardRef(
3146
3206
  };
3147
3207
  }, []);
3148
3208
 
3209
+ // Auto-refresh effect
3210
+ useEffect(() => {
3211
+ // Clear any existing timer
3212
+ if (refreshTimerRef.current) {
3213
+ clearInterval(refreshTimerRef.current);
3214
+ refreshTimerRef.current = null;
3215
+ }
3216
+
3217
+ // Set up new timer if auto-refresh is enabled and the setting is present in ajaxSetting
3218
+ if (
3219
+ autoRefreshEnabled &&
3220
+ ajaxSetting &&
3221
+ ajaxSetting.autoRefresh !== undefined
3222
+ ) {
3223
+ refreshTimerRef.current = setInterval(() => {
3224
+ handleReload();
3225
+ // No toast notification on refresh
3226
+ }, refreshInterval * 1000);
3227
+ }
3228
+
3229
+ // Clean up on unmount
3230
+ return () => {
3231
+ if (refreshTimerRef.current) {
3232
+ clearInterval(refreshTimerRef.current);
3233
+ }
3234
+ };
3235
+ }, [autoRefreshEnabled, refreshInterval, ajaxSetting]);
3236
+
3149
3237
  return (
3150
3238
  <div
3151
3239
  style={{
@@ -3156,12 +3244,15 @@ const DataGrid = forwardRef(
3156
3244
  overflow: 'hidden',
3157
3245
  }}
3158
3246
  >
3159
- {/* Search and action buttons container - separated from the DataGrid */}
3160
- {renderSearch() && (
3161
- <div className={styles.dataGridTopContainer}>
3247
+ {/* Top container with search, action buttons, and auto-refresh */}
3248
+ <div className={styles.dataGridTopContainer}>
3249
+ <div className={styles.dataGridTopLeftContainer}>
3162
3250
  {renderSearch()}
3163
3251
  </div>
3164
- )}
3252
+ <div className={styles.dataGridTopRightContainer}>
3253
+ {renderAutoRefreshControls()}
3254
+ </div>
3255
+ </div>
3165
3256
 
3166
3257
  {/* DataGrid container with its own border */}
3167
3258
  <div className={styles.dataGridContainer}>
@@ -3221,6 +3312,11 @@ const DataGrid = forwardRef(
3221
3312
  open={modalShow}
3222
3313
  onClose={modalClose}
3223
3314
  closeOnDocumentClick={false}
3315
+ contentStyle={{
3316
+ width: windowWidth < 768 ? '95vw' : '80vw',
3317
+ minWidth: windowWidth < 768 ? '95vw' : '600px',
3318
+ maxWidth: '1200px',
3319
+ }}
3224
3320
  nested
3225
3321
  >
3226
3322
  <Form
@@ -3244,7 +3340,11 @@ const DataGrid = forwardRef(
3244
3340
  open={modalGalleryShow}
3245
3341
  onClose={modalGalleryClose}
3246
3342
  closeOnDocumentClick={false}
3247
- contentStyle={{ width: 'auto', maxWidth: '90vw' }}
3343
+ contentStyle={{
3344
+ width: windowWidth < 768 ? '95vw' : '80vw',
3345
+ minWidth: windowWidth < 768 ? '95vw' : '600px',
3346
+ maxWidth: '1200px',
3347
+ }}
3248
3348
  overlayStyle={{ background: 'rgba(0, 0, 0, 0.7)' }}
3249
3349
  >
3250
3350
  <div
@@ -1085,6 +1085,7 @@ function Field({
1085
1085
  ? settings.options
1086
1086
  : options
1087
1087
  }
1088
+ isSearchable={settings.isSearchable || false}
1088
1089
  isCreatable={settings.isCreatable || false}
1089
1090
  dataOptions={dataOptions}
1090
1091
  style={style}
@@ -27,6 +27,7 @@ function MultiSelect({
27
27
  settings,
28
28
  style,
29
29
  isCreatable = false,
30
+ isSearchable = true,
30
31
  creatableConfig = {},
31
32
  }) {
32
33
  const [selectOptions, setSelectOptions] = useState([]);
@@ -142,7 +143,7 @@ function MultiSelect({
142
143
  <SelectComponent
143
144
  closeMenuOnSelect={!multi || settings.limit === 1}
144
145
  isClearable
145
- isSearchable
146
+ isSearchable={isSearchable}
146
147
  isMulti={multi}
147
148
  filterOption={createFilter({ ignoreAccents: false })}
148
149
  components={{ SelectList }}
@@ -1565,7 +1565,6 @@ function GenericDetail({
1565
1565
  src={
1566
1566
  imageSource
1567
1567
  }
1568
- width="300"
1569
1568
  alt={
1570
1569
  a.file_name
1571
1570
  }
@@ -1590,6 +1589,17 @@ function GenericDetail({
1590
1589
  styles.delete
1591
1590
  }
1592
1591
  />
1592
+ {a.file_name && (
1593
+ <div
1594
+ className={
1595
+ styles.filename
1596
+ }
1597
+ >
1598
+ {
1599
+ a.file_name
1600
+ }
1601
+ </div>
1602
+ )}
1593
1603
  </div>
1594
1604
  </li>
1595
1605
  )
@@ -1605,21 +1615,29 @@ function GenericDetail({
1605
1615
  }
1606
1616
  >
1607
1617
  {files.map((a, b) => (
1608
- <li
1609
- onClick={() => {
1610
- downloadFile(
1611
- a
1612
- );
1613
- }}
1614
- key={'tf-' + b}
1615
- >
1616
- <File
1617
- strokeWidth={
1618
- 2
1619
- }
1620
- size={18}
1621
- />
1622
- {a.file_name}
1618
+ <li key={'tf-' + b}>
1619
+ <div
1620
+ className="file-info"
1621
+ onClick={() => {
1622
+ downloadFile(
1623
+ a
1624
+ );
1625
+ }}
1626
+ >
1627
+ <File
1628
+ strokeWidth={
1629
+ 2
1630
+ }
1631
+ size={
1632
+ 20
1633
+ }
1634
+ />
1635
+ <div className="filename">
1636
+ {
1637
+ a.file_name
1638
+ }
1639
+ </div>
1640
+ </div>
1623
1641
  <button
1624
1642
  onClick={(
1625
1643
  event
@@ -1630,6 +1648,7 @@ function GenericDetail({
1630
1648
  a.file_name
1631
1649
  );
1632
1650
  }}
1651
+ title="Delete file"
1633
1652
  >
1634
1653
  <TrashCan
1635
1654
  strokeWidth={
@@ -285,76 +285,129 @@
285
285
 
286
286
  .dropzone__files {
287
287
  list-style: none;
288
- padding: 0;
288
+ padding: 16px 0;
289
289
  margin: 0;
290
290
  box-sizing: border-box;
291
+ display: grid;
292
+ grid-template-columns: repeat(2, 1fr); /* 2 columns by default */
293
+ gap: 16px;
294
+
295
+ /* Responsive grid layout */
296
+ @media (max-width: 768px) {
297
+ grid-template-columns: repeat(1, 1fr); /* 1 column on small screens */
298
+ }
291
299
 
292
300
  li {
293
- width: 100%;
294
- display: block;
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: space-between;
295
304
  background: rgba(var(--primary-rgb), 0.05);
296
305
  color: var(--primary-color);
297
- border-radius: 3px;
298
- transition: background 0.25s cubic-bezier(0.25, 0.8, 0.25, 1);
299
- outline: none;
306
+ border-radius: 6px;
307
+ transition: all 0.2s ease;
300
308
  cursor: pointer;
301
- border-radius: 3px;
302
- padding: 0.5rem;
309
+ padding: 12px 16px;
303
310
  box-sizing: border-box;
304
311
  position: relative;
305
312
  border: 1px solid rgba(var(--primary-rgb), 0.15);
306
- line-height: 1;
307
- font-size: 0.85rem;
308
- margin-bottom: 5px;
313
+ line-height: 1.4;
314
+ font-size: 0.9rem;
315
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
316
+
317
+ &:hover {
318
+ transform: translateY(-2px);
319
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
320
+ background: rgba(var(--primary-rgb), 0.08);
321
+ }
309
322
 
310
- svg {
311
- position: relative;
312
- top: -2px;
313
- color: rgba(var(--secondary-rgb), 1);
314
- float: left;
315
- stroke-width: 1;
316
- line-height: 1;
317
- margin-right: 5px;
323
+ .file-info {
324
+ display: flex;
325
+ align-items: center;
326
+ flex: 1;
327
+ overflow: hidden;
328
+
329
+ svg {
330
+ color: var(--secondary-color);
331
+ flex-shrink: 0;
332
+ margin-right: 12px;
333
+ }
334
+
335
+ .filename {
336
+ white-space: nowrap;
337
+ overflow: hidden;
338
+ text-overflow: ellipsis;
339
+ }
318
340
  }
319
341
 
320
342
  button {
321
- position: relative;
322
- float: right;
323
343
  background: none;
324
344
  outline: none;
325
345
  border: none;
326
- z-index: 200;
346
+ padding: 6px;
347
+ margin-left: 8px;
348
+ border-radius: 4px;
327
349
  cursor: pointer;
350
+ display: flex;
351
+ align-items: center;
352
+ justify-content: center;
353
+ transition: background-color 0.2s ease;
354
+
355
+ &:hover {
356
+ background-color: rgba(var(--primary-rgb), 0.1);
357
+ }
328
358
 
329
359
  svg {
330
- font-size: 1em;
331
360
  color: var(--primary-color);
332
- line-height: 1;
333
361
  }
334
362
  }
335
363
  }
336
-
337
- li:hover {
338
- background: rgba(var(--secondary-color-rgb, 0.65));
339
- }
340
364
  }
341
365
 
342
366
  .sortlist {
343
- display: flex;
344
- flex-wrap: wrap;
345
- gap: 20px;
346
- padding: 0;
367
+ display: grid;
368
+ grid-template-columns: repeat(4, 1fr); /* 4 columns by default */
369
+ gap: 16px;
370
+ padding: 16px 0;
347
371
  list-style: none;
348
372
 
373
+ /* Responsive grid layout */
374
+ @media (max-width: 1200px) {
375
+ grid-template-columns: repeat(3, 1fr); /* 3 columns on medium screens */
376
+ }
377
+
378
+ @media (max-width: 768px) {
379
+ grid-template-columns: repeat(2, 1fr); /* 2 columns on small screens */
380
+ }
381
+
382
+ @media (max-width: 480px) {
383
+ grid-template-columns: repeat(
384
+ 1,
385
+ 1fr
386
+ ); /* 1 column on very small screens */
387
+ }
388
+
349
389
  li {
350
390
  position: relative;
391
+ aspect-ratio: 4/3;
392
+ border-radius: 8px;
393
+ overflow: hidden;
394
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
395
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
396
+ cursor: pointer;
397
+
398
+ &:hover {
399
+ transform: translateY(-2px);
400
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
401
+ }
351
402
 
352
403
  .sortimg {
353
- width: 300px;
354
- height: 300px;
404
+ width: 100%;
405
+ height: 100%;
355
406
  border-radius: 8px;
356
407
  overflow: hidden;
357
408
  position: relative;
409
+ display: flex;
410
+ flex-direction: column;
358
411
 
359
412
  img {
360
413
  width: 100%;
@@ -376,6 +429,21 @@
376
429
  background: rgba(255, 255, 255, 1);
377
430
  }
378
431
  }
432
+
433
+ .filename {
434
+ position: absolute;
435
+ bottom: 0;
436
+ left: 0;
437
+ right: 0;
438
+ padding: 8px;
439
+ background-color: rgba(0, 0, 0, 0.6);
440
+ color: white;
441
+ font-size: 0.8rem;
442
+ white-space: nowrap;
443
+ overflow: hidden;
444
+ text-overflow: ellipsis;
445
+ z-index: 1;
446
+ }
379
447
  }
380
448
  }
381
449
  }
@@ -1,7 +1,7 @@
1
1
  .header {
2
2
  background: var(--primary-color);
3
3
  color: var(--primary-color);
4
- position: relative;
4
+ position: sticky;
5
5
  top: 0;
6
6
  width: 100%;
7
7
  z-index: 995;
@@ -11,7 +11,7 @@
11
11
  .headerAlternate {
12
12
  background: var(--bg-color);
13
13
  color: var(--paragraph-color);
14
- position: relative;
14
+ position: sticky;
15
15
  top: 0;
16
16
  width: 100%;
17
17
  z-index: 995;
@@ -596,7 +596,22 @@
596
596
 
597
597
  /* DataGrid top container and search container styles */
598
598
  .dataGridTopContainer {
599
+ display: flex;
600
+ justify-content: space-between;
601
+ align-items: center;
599
602
  margin-bottom: 8px;
603
+ padding: 0 5px;
604
+ }
605
+
606
+ .dataGridTopLeftContainer {
607
+ flex: 1;
608
+ }
609
+
610
+ .dataGridTopRightContainer {
611
+ display: flex;
612
+ align-items: center;
613
+ justify-content: flex-end;
614
+ padding-right: 10px;
600
615
  }
601
616
 
602
617
  .searchContainer {
@@ -651,12 +666,44 @@
651
666
  justify-content: flex-end;
652
667
  }
653
668
 
669
+ /* Auto-refresh controls */
670
+ .autoRefreshContainer {
671
+ display: inline-flex;
672
+ align-items: center;
673
+ padding: 0 15px;
674
+ height: 38px;
675
+ background-color: #f9fafb;
676
+ border: 1px solid #d1d5db;
677
+ border-radius: var(--br, 4px);
678
+ margin-left: 10px;
679
+ margin-bottom: 0;
680
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
681
+ }
682
+
683
+ .autoRefreshLabel {
684
+ display: flex;
685
+ align-items: center;
686
+ gap: 8px;
687
+ font-size: 0.9rem;
688
+ color: var(--secondary-color, #1f2937);
689
+ white-space: nowrap;
690
+ cursor: pointer;
691
+ font-weight: normal;
692
+ }
693
+
694
+ .autoRefreshCheckbox {
695
+ margin: 0;
696
+ cursor: pointer;
697
+ width: 16px;
698
+ height: 16px;
699
+ }
700
+
654
701
  /* Container for when only the action button is shown (search disabled) */
655
702
  .actionButtonsContainer {
656
703
  display: flex;
657
704
  justify-content: flex-end;
658
705
  width: 100%;
659
- margin-bottom: 8px; /* Reduced spacing to match when searchbar is present */
706
+ margin-bottom: 0; /* No bottom margin to align with auto-refresh */
660
707
  }
661
708
 
662
709
  .actionButtons {
@@ -618,6 +618,9 @@
618
618
  -webkit-overflow-scrolling: touch;
619
619
  scrollbar-width: none; /* Firefox */
620
620
  -ms-overflow-style: none; /* IE and Edge */
621
+ position: sticky;
622
+ top: 0;
623
+ z-index: 995;
621
624
 
622
625
  /* Hide scrollbar for Chrome/Safari/Opera */
623
626
  &::-webkit-scrollbar {
@@ -805,6 +808,12 @@
805
808
 
806
809
  .content {
807
810
  margin-left: 0;
811
+
812
+ .content-header {
813
+ position: sticky;
814
+ top: 0;
815
+ z-index: 995;
816
+ }
808
817
  }
809
818
 
810
819
  .popup-content {