@visns-studio/visns-components 5.14.19 → 5.14.21

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
@@ -9,6 +9,18 @@ A comprehensive React component library used by the VISNS Studio team for CRM an
9
9
 
10
10
  VISNS Components is a React-based UI component library that provides a set of reusable, consistent, and customizable components for building web applications. It includes components for authentication, data grids, forms, navigation, and more, designed to work seamlessly together.
11
11
 
12
+ ## Recent Updates (v5.14.20)
13
+
14
+ ### Latest Enhancements
15
+
16
+ - **GenericGrid Advanced Features**: Enhanced filtering with URL-based options, multiselect support, contact display integration, interactive date pickers with timezone support, and toggleable stage functionality
17
+ - **Modular Architecture**: GenericIndex refactored into standalone GenericGrid and GenericReportForm components for better reusability
18
+ - **Proposal System Integration**: GenericQuote enhanced with comprehensive proposal generation capabilities
19
+ - **Smart Data Extraction**: DropZone component with intelligent data extraction for complex API responses
20
+ - **Interactive Stage Management**: Clickable stage toggles with API integration and visual feedback
21
+ - **Timezone Support**: Automatic timezone detection and conversion for date operations
22
+ - **Contact Selection Workflows**: Two-step date and contact selection for survey systems
23
+
12
24
  ## Features
13
25
 
14
26
  ### Authentication System
@@ -180,6 +192,169 @@ The refactoring also introduced shared utility functions:
180
192
 
181
193
  These utilities are available for use in custom components and provide consistent functionality across the library.
182
194
 
195
+ ### GenericGrid Advanced Features
196
+
197
+ #### Filter Enhancements (v5.10.12+)
198
+
199
+ The GenericGrid component includes powerful filtering capabilities with URL-based options and multiselect support:
200
+
201
+ ```jsx
202
+ // URL-based filter options
203
+ {
204
+ id: 'status',
205
+ type: 'dropdown',
206
+ label: 'Status',
207
+ optionsUrl: '/api/filter-options/status',
208
+ optionsMethod: 'POST',
209
+ isMulti: true // Enables multiple selection
210
+ }
211
+
212
+ // Year filter with "now" default value
213
+ {
214
+ id: 'year',
215
+ type: 'year',
216
+ label: 'Year',
217
+ defaultValue: 'now' // Displays current year
218
+ }
219
+ ```
220
+
221
+ **Key Features:**
222
+ - **Dynamic Options**: Filters can fetch options from API endpoints using POST method
223
+ - **Multiselect Control**: Use `isMulti` property for single/multiple selection control
224
+ - **Smart Defaults**: Year filters properly handle "now" value to display current year
225
+ - **Value Initialization**: Enhanced value formatting for MultiSelect components
226
+
227
+ #### Contact Display Integration (v5.11.1+)
228
+
229
+ GenericGrid automatically displays associated contacts when date fields have contact relationships:
230
+
231
+ ```jsx
232
+ // Date column with contact display
233
+ {
234
+ id: 'survey_date',
235
+ type: 'date',
236
+ header: 'Survey Date',
237
+ onClick: {
238
+ type: 'date_with_contacts',
239
+ title: 'Mark Survey Complete',
240
+ message: 'Select the survey completion date'
241
+ }
242
+ }
243
+ ```
244
+
245
+ **Features:**
246
+ - **Date + Contact Display**: Shows formatted date with contact summary badge
247
+ - **Contact Tooltips**: Hover over contact badge for full contact details
248
+ - **Flexible Data Structure**: Supports both existing contacts and manual entries
249
+ - **Responsive Design**: Contact badges adapt to different screen sizes
250
+
251
+ #### Interactive Date Picker (v5.10.13+)
252
+
253
+ Enhanced date cell interaction with confirmation dialogs and timezone support:
254
+
255
+ ```jsx
256
+ // Date picker with timezone support
257
+ {
258
+ id: 'completion_date',
259
+ type: 'date',
260
+ onClick: {
261
+ type: 'date_picker',
262
+ title: 'Set Completion Date',
263
+ message: 'Choose the completion date',
264
+ placeholder: 'Select date',
265
+ timezone: 'auto', // or 'Australia/Perth'
266
+ dateOnly: true // For DATE fields vs DATETIME
267
+ }
268
+ }
269
+ ```
270
+
271
+ **Timezone Features (v5.10.15+):**
272
+ - **Automatic Detection**: `timezone: "auto"` detects browser timezone
273
+ - **Australia/Perth Support**: Built-in support with `forcePerth: true`
274
+ - **Custom Timezones**: Support any timezone with `timezone: "specific/timezone"`
275
+ - **UTC Conversion**: Proper conversion between local timezone and UTC for API calls
276
+ - **Field Type Awareness**: `dateOnly` parameter handles DATE vs DATETIME fields correctly
277
+
278
+ #### Contact Selection Workflow (v5.11.0+)
279
+
280
+ Two-step date and contact selection process for comprehensive data collection:
281
+
282
+ ```jsx
283
+ // Date + contact selection configuration
284
+ {
285
+ id: 'survey_date',
286
+ type: 'date',
287
+ onClick: {
288
+ type: 'date_with_contacts',
289
+ contactConfig: {
290
+ maxContacts: 5,
291
+ allowManual: true,
292
+ contactsEndpoint: '/api/contacts/search',
293
+ requiredFields: ['name', 'email']
294
+ }
295
+ }
296
+ }
297
+ ```
298
+
299
+ **Contact Selection Features:**
300
+ - **ContactSelectorModal**: Modal for selecting existing or adding manual contacts
301
+ - **Multiple Contact Types**: Database contacts (`type: 'contact'`) and manual entries (`type: 'manual'`)
302
+ - **Flexible Limits**: Configurable contact limits and required fields
303
+ - **Survey Integration**: Perfect for survey systems requiring contact associations
304
+
305
+ #### Toggleable Stage Functionality (v5.12.2+)
306
+
307
+ Interactive stage management with toggle functionality for both stage and stageCounter columns:
308
+
309
+ ```jsx
310
+ // Basic stage column with toggle
311
+ {
312
+ id: 'opportunity_stage',
313
+ type: 'stage',
314
+ keyIds: ['prospect', 'qualified', 'proposal', 'won'],
315
+ valueIds: ['Prospect', 'Qualified', 'Proposal', 'Won'],
316
+ toggleable: true,
317
+ toggleConfig: {
318
+ updateUrl: '/api/opportunities/{id}/stage',
319
+ updateMethod: 'PATCH',
320
+ updateKey: 'stage_field'
321
+ }
322
+ }
323
+
324
+ // StageCounter with individual stage toggles
325
+ {
326
+ id: 'project_stages',
327
+ type: 'stageCounter',
328
+ stageConfig: {
329
+ key: 'milestones',
330
+ stageLabelKey: 'milestone_name',
331
+ stageColour: {
332
+ key: 'status',
333
+ options: [
334
+ { id: 'completed', colour: '#28a745' },
335
+ { id: 'pending', colour: '#6c757d' }
336
+ ]
337
+ },
338
+ toggleable: true,
339
+ toggleConfig: {
340
+ activeStatusValue: 'completed',
341
+ inactiveStatusValue: 'pending',
342
+ statusField: 'status',
343
+ updateUrl: '/api/project-stages/{stage_id}',
344
+ updateMethod: 'PATCH',
345
+ updateKey: 'status'
346
+ }
347
+ }
348
+ }
349
+ ```
350
+
351
+ **Toggle Features:**
352
+ - **Interactive Management**: Click stages to enable/disable with visual feedback
353
+ - **API Integration**: Configurable endpoints with URL placeholders and CSRF protection
354
+ - **Toast Notifications**: Real-time success/error feedback
355
+ - **Auto-Refresh**: Grid data refreshes after successful stage updates
356
+ - **Mixed Interactions**: StageCounter supports both stage toggling and container navigation
357
+
183
358
  ## DropZone Component Enhancements (v5.14.0+)
184
359
 
185
360
  ### Smart Data Extraction
package/package.json CHANGED
@@ -89,7 +89,7 @@
89
89
  "react-dom": "^17.0.0 || ^18.0.0"
90
90
  },
91
91
  "name": "@visns-studio/visns-components",
92
- "version": "5.14.19",
92
+ "version": "5.14.21",
93
93
  "description": "Various packages to assist in the development of our Custom Applications.",
94
94
  "main": "src/index.js",
95
95
  "files": [
@@ -65,6 +65,9 @@ function Field({
65
65
  onChangeTableRadio,
66
66
  onChangeAddTableTextRow,
67
67
  onChangeTableText,
68
+ onChangeJsonTable,
69
+ onAddJsonTableRow,
70
+ onDeleteJsonTableRow,
68
71
  onChangeToggle,
69
72
  onFileDownload,
70
73
  setFormData,
@@ -1409,6 +1412,387 @@ function Field({
1409
1412
  autoComplete="off"
1410
1413
  />
1411
1414
  );
1415
+ case 'json-table':
1416
+ // Parse JSON value to get table data
1417
+ let jsonTableData = [];
1418
+ let jsonTableError = null;
1419
+
1420
+ try {
1421
+ if (inputValue && inputValue !== 'null' && inputValue !== '') {
1422
+ const parsedValue = typeof inputValue === 'string' ? JSON.parse(inputValue) : inputValue;
1423
+
1424
+ // Handle jsonPath setting to extract nested data
1425
+ if (settings.jsonPath && parsedValue[settings.jsonPath]) {
1426
+ jsonTableData = Array.isArray(parsedValue[settings.jsonPath])
1427
+ ? parsedValue[settings.jsonPath]
1428
+ : [];
1429
+ } else if (Array.isArray(parsedValue)) {
1430
+ jsonTableData = parsedValue;
1431
+ } else if (typeof parsedValue === 'object' && parsedValue !== null) {
1432
+ // If it's an object but not an array, convert to single row
1433
+ jsonTableData = [parsedValue];
1434
+ }
1435
+ }
1436
+ } catch (e) {
1437
+ jsonTableError = 'Invalid JSON format';
1438
+ jsonTableData = [];
1439
+ }
1440
+
1441
+ // Ensure minimum rows
1442
+ const minRows = settings.validation?.minRows || settings.minRows || 0;
1443
+ while (jsonTableData.length < minRows) {
1444
+ jsonTableData.push(settings.defaultRow || {});
1445
+ }
1446
+
1447
+ // Custom validation for JSON table
1448
+ const validateJsonTable = (data, validationConfig) => {
1449
+ if (!validationConfig || !validationConfig.customValidation) {
1450
+ return { isValid: true, errors: [] };
1451
+ }
1452
+
1453
+ const { type, startTimeField, endTimeField, message } = validationConfig.customValidation;
1454
+ const errors = [];
1455
+
1456
+ if (type === '24hour_coverage') {
1457
+ // Convert time strings to minutes for easier calculation
1458
+ const timeToMinutes = (timeStr) => {
1459
+ if (!timeStr) return null;
1460
+ const [hours, minutes] = timeStr.split(':').map(Number);
1461
+ return hours * 60 + minutes;
1462
+ };
1463
+
1464
+ // Sort intervals by start time
1465
+ const sortedIntervals = [...data].sort((a, b) => {
1466
+ const aStart = timeToMinutes(a[startTimeField]);
1467
+ const bStart = timeToMinutes(b[startTimeField]);
1468
+ return aStart - bStart;
1469
+ });
1470
+
1471
+ let totalCoverage = 0;
1472
+ let previousEnd = null;
1473
+
1474
+ for (let i = 0; i < sortedIntervals.length; i++) {
1475
+ const interval = sortedIntervals[i];
1476
+ const startMinutes = timeToMinutes(interval[startTimeField]);
1477
+ const endMinutes = timeToMinutes(interval[endTimeField]);
1478
+
1479
+ if (startMinutes === null || endMinutes === null) {
1480
+ errors.push(`Interval ${i + 1}: Missing start or end time`);
1481
+ continue;
1482
+ }
1483
+
1484
+ // Handle cross-midnight intervals (e.g., 18:00 to 06:00)
1485
+ let duration;
1486
+ if (endMinutes <= startMinutes) {
1487
+ // Cross-midnight: add 24 hours to end time
1488
+ duration = (endMinutes + 1440) - startMinutes;
1489
+ } else {
1490
+ duration = endMinutes - startMinutes;
1491
+ }
1492
+
1493
+ totalCoverage += duration;
1494
+
1495
+ // Check for gaps (except for the first interval)
1496
+ if (previousEnd !== null && startMinutes !== previousEnd) {
1497
+ // Allow for cross-midnight transitions
1498
+ const gapSize = startMinutes > previousEnd
1499
+ ? startMinutes - previousEnd
1500
+ : (startMinutes + 1440) - previousEnd;
1501
+
1502
+ if (gapSize > 0 && gapSize < 1440) { // Don't count full day gaps as errors
1503
+ errors.push(`Gap detected between intervals: ${Math.floor(gapSize / 60)}h ${gapSize % 60}m`);
1504
+ }
1505
+ }
1506
+
1507
+ // Update previousEnd, handling cross-midnight
1508
+ previousEnd = endMinutes <= startMinutes ? endMinutes + 1440 : endMinutes;
1509
+ if (previousEnd >= 1440) previousEnd -= 1440; // Normalize back to 0-1439
1510
+ }
1511
+
1512
+ // Check if total coverage equals 24 hours (1440 minutes)
1513
+ if (Math.abs(totalCoverage - 1440) > 1) { // Allow 1 minute tolerance
1514
+ errors.push(`Total coverage is ${Math.floor(totalCoverage / 60)}h ${totalCoverage % 60}m. Required: 24h 0m`);
1515
+ }
1516
+
1517
+ // Check for overlaps
1518
+ for (let i = 0; i < sortedIntervals.length - 1; i++) {
1519
+ const current = sortedIntervals[i];
1520
+ const next = sortedIntervals[i + 1];
1521
+
1522
+ const currentStart = timeToMinutes(current[startTimeField]);
1523
+ const currentEnd = timeToMinutes(current[endTimeField]);
1524
+ const nextStart = timeToMinutes(next[startTimeField]);
1525
+
1526
+ // Handle normal and cross-midnight intervals
1527
+ const currentActualEnd = currentEnd <= currentStart ? currentEnd + 1440 : currentEnd;
1528
+ const nextActualStart = nextStart;
1529
+
1530
+ if (currentActualEnd > nextActualStart && currentActualEnd < nextActualStart + 1440) {
1531
+ const overlapMinutes = currentActualEnd - nextActualStart;
1532
+ errors.push(`Overlap detected: ${Math.floor(overlapMinutes / 60)}h ${overlapMinutes % 60}m between intervals ${i + 1} and ${i + 2}`);
1533
+ }
1534
+ }
1535
+ }
1536
+
1537
+ return {
1538
+ isValid: errors.length === 0,
1539
+ errors: errors.length > 0 ? [message, ...errors] : []
1540
+ };
1541
+ };
1542
+
1543
+ // Perform validation
1544
+ const validationResult = validateJsonTable(jsonTableData, settings.validation);
1545
+ const hasValidationErrors = !validationResult.isValid;
1546
+
1547
+ // Render JSON table field
1548
+ const renderJsonTableField = (column, cellValue, rowIndex, columnIndex) => {
1549
+ const fieldSettings = {
1550
+ ...column,
1551
+ id: `${settings.id}_${rowIndex}_${column.id}`,
1552
+ parent: settings.id
1553
+ };
1554
+
1555
+ switch (column.type) {
1556
+ case 'checkbox':
1557
+ return (
1558
+ <div className={styles.fi__checkboxcontainer}>
1559
+ <input
1560
+ type="checkbox"
1561
+ checked={cellValue || false}
1562
+ data-id={settings.id}
1563
+ className={styles.fi__customcheckbox}
1564
+ onChange={(e) =>
1565
+ onChangeJsonTable && onChangeJsonTable(e, rowIndex, column.id, 'checkbox')
1566
+ }
1567
+ />
1568
+ </div>
1569
+ );
1570
+ case 'html5_date':
1571
+ return (
1572
+ <input
1573
+ type="date"
1574
+ value={cellValue || ''}
1575
+ data-id={settings.id}
1576
+ className={styles.jsonTableInput}
1577
+ onChange={(e) =>
1578
+ onChangeJsonTable && onChangeJsonTable(e, rowIndex, column.id, 'date')
1579
+ }
1580
+ />
1581
+ );
1582
+ case 'html5_time':
1583
+ return (
1584
+ <input
1585
+ type="time"
1586
+ value={cellValue || ''}
1587
+ data-id={settings.id}
1588
+ className={styles.jsonTableInput}
1589
+ onChange={(e) =>
1590
+ onChangeJsonTable && onChangeJsonTable(e, rowIndex, column.id, 'time')
1591
+ }
1592
+ />
1593
+ );
1594
+ case 'html5_datetime':
1595
+ return (
1596
+ <input
1597
+ type="datetime-local"
1598
+ value={cellValue || ''}
1599
+ data-id={settings.id}
1600
+ className={styles.jsonTableInput}
1601
+ onChange={(e) =>
1602
+ onChangeJsonTable && onChangeJsonTable(e, rowIndex, column.id, 'datetime')
1603
+ }
1604
+ />
1605
+ );
1606
+ case 'number':
1607
+ case 'decimal':
1608
+ // Special handling for interval numbers - make them auto-generated and read-only
1609
+ if (column.id === 'number' || column.autoGenerate) {
1610
+ return (
1611
+ <div className={styles.jsonTableDisplay}>
1612
+ {rowIndex + 1}
1613
+ </div>
1614
+ );
1615
+ }
1616
+
1617
+ return (
1618
+ <input
1619
+ type="number"
1620
+ value={cellValue || ''}
1621
+ min={column.min}
1622
+ max={column.max}
1623
+ step={column.type === 'decimal' ? '0.01' : '1'}
1624
+ data-id={settings.id}
1625
+ className={styles.jsonTableInput}
1626
+ onChange={(e) =>
1627
+ onChangeJsonTable && onChangeJsonTable(e, rowIndex, column.id, 'number')
1628
+ }
1629
+ />
1630
+ );
1631
+ case 'textarea':
1632
+ return (
1633
+ <textarea
1634
+ value={cellValue || ''}
1635
+ placeholder={column.placeholder || ''}
1636
+ data-id={settings.id}
1637
+ className={styles.jsonTableTextarea}
1638
+ onChange={(e) =>
1639
+ onChangeJsonTable && onChangeJsonTable(e, rowIndex, column.id, 'textarea')
1640
+ }
1641
+ rows={column.rows || 3}
1642
+ />
1643
+ );
1644
+ case 'toggle':
1645
+ return (
1646
+ <div style={{ display: 'flex', justifyContent: 'center' }}>
1647
+ <Toggle
1648
+ checked={cellValue || false}
1649
+ onChange={(e) =>
1650
+ onChangeJsonTable && onChangeJsonTable(e, rowIndex, column.id, 'toggle')
1651
+ }
1652
+ />
1653
+ </div>
1654
+ );
1655
+ case 'text':
1656
+ default:
1657
+ return (
1658
+ <input
1659
+ type="text"
1660
+ value={cellValue || ''}
1661
+ placeholder={column.placeholder || ''}
1662
+ data-id={settings.id}
1663
+ className={styles.jsonTableInput}
1664
+ onChange={(e) =>
1665
+ onChangeJsonTable && onChangeJsonTable(e, rowIndex, column.id, 'text')
1666
+ }
1667
+ />
1668
+ );
1669
+ }
1670
+ };
1671
+
1672
+ return (
1673
+ <div>
1674
+ {settings.label && (
1675
+ <div className={styles.jsonTableLabel} style={{
1676
+ marginBottom: '0.75rem',
1677
+ fontWeight: '500',
1678
+ fontSize: '0.9rem',
1679
+ color: 'var(--paragraph-color)',
1680
+ display: 'flex',
1681
+ alignItems: 'center'
1682
+ }}>
1683
+ {settings.label}
1684
+ {settings.required && (
1685
+ <span style={{
1686
+ color: 'var(--danger-color, #dc3545)',
1687
+ marginLeft: '3px'
1688
+ }}>
1689
+ *
1690
+ </span>
1691
+ )}
1692
+ </div>
1693
+ )}
1694
+ <div className={styles.jsonTableContainer}>
1695
+ {jsonTableError && (
1696
+ <div className={styles.jsonTableError} style={{
1697
+ color: 'var(--danger-color, #dc3545)',
1698
+ marginBottom: '10px',
1699
+ padding: '8px 12px',
1700
+ backgroundColor: 'var(--danger-bg, #f8d7da)',
1701
+ border: '1px solid var(--danger-border, #f5c6cb)',
1702
+ borderRadius: 'var(--br, 4px)',
1703
+ fontSize: '0.875rem'
1704
+ }}>
1705
+ <strong>Invalid JSON:</strong> {jsonTableError}
1706
+ </div>
1707
+ )}
1708
+
1709
+ <div className={styles.jsonTableWrapper}>
1710
+ <table className={styles.jsonTable}>
1711
+ <thead>
1712
+ <tr>
1713
+ {settings.columns?.map((column, index) => (
1714
+ <th key={index} style={{
1715
+ width: column.width || 'auto',
1716
+ minWidth: column.minWidth || 'auto'
1717
+ }}>
1718
+ {column.label}
1719
+ {column.required && (
1720
+ <span style={{
1721
+ color: 'var(--danger-color, #dc3545)',
1722
+ marginLeft: '3px'
1723
+ }}>
1724
+ *
1725
+ </span>
1726
+ )}
1727
+ </th>
1728
+ ))}
1729
+ {settings.deleteRowButton?.enabled !== false && (
1730
+ <th className={styles.jsonTableActionsHeader}>
1731
+ Actions
1732
+ </th>
1733
+ )}
1734
+ </tr>
1735
+ </thead>
1736
+ <tbody>
1737
+ {jsonTableData.map((row, rowIndex) => (
1738
+ <tr key={rowIndex}>
1739
+ {settings.columns?.map((column, columnIndex) => (
1740
+ <td key={columnIndex}>
1741
+ {renderJsonTableField(
1742
+ column,
1743
+ row[column.id],
1744
+ rowIndex,
1745
+ columnIndex
1746
+ )}
1747
+ </td>
1748
+ ))}
1749
+ {settings.deleteRowButton?.enabled !== false && (
1750
+ <td className={styles.jsonTableActionsCell}>
1751
+ <button
1752
+ type="button"
1753
+ onClick={() =>
1754
+ onDeleteJsonTableRow && onDeleteJsonTableRow(rowIndex, settings.id)
1755
+ }
1756
+ className={styles.jsonTableDeleteBtn}
1757
+ title="Delete row"
1758
+ >
1759
+ <Trash2 size={14} />
1760
+ </button>
1761
+ </td>
1762
+ )}
1763
+ </tr>
1764
+ ))}
1765
+ </tbody>
1766
+ </table>
1767
+ </div>
1768
+
1769
+ {settings.addRowButton?.enabled !== false && (
1770
+ <div className={styles.jsonTableActions}>
1771
+ <button
1772
+ type="button"
1773
+ onClick={() =>
1774
+ onAddJsonTableRow && onAddJsonTableRow(settings.id, settings.defaultRow || {})
1775
+ }
1776
+ className={styles.jsonTableAddBtn}
1777
+ >
1778
+ <CirclePlus size={16} />
1779
+ <span>{settings.addRowButton?.label || 'Add Row'}</span>
1780
+ </button>
1781
+ </div>
1782
+ )}
1783
+
1784
+ {hasValidationErrors && (
1785
+ <div className={styles.validationErrors}>
1786
+ {validationResult.errors.map((error, index) => (
1787
+ <div key={index} className={styles.validationError}>
1788
+ {error}
1789
+ </div>
1790
+ ))}
1791
+ </div>
1792
+ )}
1793
+ </div>
1794
+ </div>
1795
+ );
1412
1796
  case 'line-break':
1413
1797
  return (
1414
1798
  <div className={styles.formbreak}>
@@ -2246,6 +2630,7 @@ function Field({
2246
2630
  'file',
2247
2631
  'gallery',
2248
2632
  'image',
2633
+ 'json-table',
2249
2634
  'line-break',
2250
2635
  'multi-dropdown',
2251
2636
  'multi-dropdown-ajax',
@@ -2690,7 +3075,7 @@ function Field({
2690
3075
  <div className={formItemClass} style={formItemStyle} ref={containerRef}>
2691
3076
  <label
2692
3077
  className={
2693
- ['richeditor', 'textarea'].includes(settings.type)
3078
+ ['richeditor', 'textarea', 'json-table'].includes(settings.type)
2694
3079
  ? ''
2695
3080
  : styles.fi__label
2696
3081
  }
@@ -567,6 +567,188 @@ function Form({
567
567
  }
568
568
  };
569
569
 
570
+ // JSON Table handlers
571
+ const handleChangeJsonTable = (event, rowIndex, columnId, fieldType) => {
572
+ const fieldId = event.target.getAttribute('data-id');
573
+ let value = event.target.value;
574
+
575
+ // Handle different field types
576
+ if (fieldType === 'checkbox' || fieldType === 'toggle') {
577
+ value = event.target.checked;
578
+ } else if (fieldType === 'number') {
579
+ value = parseFloat(value) || 0;
580
+ }
581
+
582
+ setFormData(prevData => {
583
+ const newData = { ...prevData };
584
+
585
+ // Get the current JSON value for this field
586
+ let currentValue = newData[fieldId];
587
+ if (typeof currentValue === 'string') {
588
+ try {
589
+ currentValue = JSON.parse(currentValue);
590
+ } catch (e) {
591
+ currentValue = [];
592
+ }
593
+ }
594
+
595
+ // Find field configuration from form settings
596
+ let fieldConfig = null;
597
+ if (formSettings && formSettings.fields) {
598
+ fieldConfig = formSettings.fields.find(f => f.id === fieldId);
599
+ }
600
+
601
+ // Handle nested JSON path (e.g., intervals within galvanizing_intervals)
602
+ let tableData;
603
+
604
+ if (fieldConfig && fieldConfig.jsonPath && currentValue[fieldConfig.jsonPath]) {
605
+ tableData = [...currentValue[fieldConfig.jsonPath]];
606
+ } else if (Array.isArray(currentValue)) {
607
+ tableData = [...currentValue];
608
+ } else {
609
+ tableData = [];
610
+ }
611
+
612
+ // Update the specific cell
613
+ if (tableData[rowIndex]) {
614
+ tableData[rowIndex] = {
615
+ ...tableData[rowIndex],
616
+ [columnId]: value
617
+ };
618
+ }
619
+
620
+ // Update the data structure
621
+ if (fieldConfig && fieldConfig.jsonPath) {
622
+ newData[fieldId] = {
623
+ ...currentValue,
624
+ [fieldConfig.jsonPath]: tableData
625
+ };
626
+ } else {
627
+ newData[fieldId] = tableData;
628
+ }
629
+
630
+ return newData;
631
+ });
632
+ };
633
+
634
+ const handleAddJsonTableRow = (fieldId, defaultRow = {}) => {
635
+ setFormData(prevData => {
636
+ const newData = { ...prevData };
637
+ let currentValue = newData[fieldId];
638
+
639
+ if (typeof currentValue === 'string') {
640
+ try {
641
+ currentValue = JSON.parse(currentValue);
642
+ } catch (e) {
643
+ currentValue = [];
644
+ }
645
+ }
646
+
647
+ // Find field configuration from form settings
648
+ let fieldConfig = null;
649
+ if (formSettings && formSettings.fields) {
650
+ fieldConfig = formSettings.fields.find(f => f.id === fieldId);
651
+ }
652
+
653
+ let tableData;
654
+
655
+ if (fieldConfig && fieldConfig.jsonPath && currentValue[fieldConfig.jsonPath]) {
656
+ tableData = [...currentValue[fieldConfig.jsonPath]];
657
+ } else if (Array.isArray(currentValue)) {
658
+ tableData = [...currentValue];
659
+ } else {
660
+ tableData = [];
661
+ }
662
+
663
+ // Add new row with auto-generated number
664
+ const newRow = {
665
+ ...defaultRow,
666
+ number: tableData.length + 1
667
+ };
668
+ tableData.push(newRow);
669
+
670
+ // Update the data structure
671
+ if (fieldConfig && fieldConfig.jsonPath) {
672
+ newData[fieldId] = {
673
+ ...currentValue,
674
+ [fieldConfig.jsonPath]: tableData
675
+ };
676
+ } else {
677
+ newData[fieldId] = tableData;
678
+ }
679
+
680
+ return newData;
681
+ });
682
+ };
683
+
684
+ const handleDeleteJsonTableRow = (rowIndex, fieldId) => {
685
+ confirmDialog({
686
+ title: 'Delete Interval',
687
+ message: 'Are you sure you want to delete this interval?',
688
+ buttons: [
689
+ {
690
+ label: 'Yes',
691
+ onClick: () => {
692
+ setFormData(prevData => {
693
+ const newData = { ...prevData };
694
+ let currentValue = newData[fieldId];
695
+
696
+ if (typeof currentValue === 'string') {
697
+ try {
698
+ currentValue = JSON.parse(currentValue);
699
+ } catch (e) {
700
+ currentValue = [];
701
+ }
702
+ }
703
+
704
+ // Find field configuration from form settings
705
+ let fieldConfig = null;
706
+ if (formSettings && formSettings.fields) {
707
+ fieldConfig = formSettings.fields.find(f => f.id === fieldId);
708
+ }
709
+
710
+ let tableData;
711
+
712
+ if (fieldConfig && fieldConfig.jsonPath && currentValue[fieldConfig.jsonPath]) {
713
+ tableData = [...currentValue[fieldConfig.jsonPath]];
714
+ } else if (Array.isArray(currentValue)) {
715
+ tableData = [...currentValue];
716
+ } else {
717
+ tableData = [];
718
+ }
719
+
720
+ // Remove row
721
+ tableData.splice(rowIndex, 1);
722
+
723
+ // Re-number the remaining rows
724
+ tableData.forEach((row, index) => {
725
+ row.number = index + 1;
726
+ });
727
+
728
+ // Update the data structure
729
+ if (fieldConfig && fieldConfig.jsonPath) {
730
+ newData[fieldId] = {
731
+ ...currentValue,
732
+ [fieldConfig.jsonPath]: tableData
733
+ };
734
+ } else {
735
+ newData[fieldId] = tableData;
736
+ }
737
+
738
+ return newData;
739
+ });
740
+ }
741
+ },
742
+ {
743
+ label: 'No',
744
+ onClick: () => {
745
+ // User cancelled - do nothing
746
+ }
747
+ }
748
+ ]
749
+ });
750
+ };
751
+
570
752
  const autocompleteSelect = (place, id) => {
571
753
  const { address, text, context } = place;
572
754
 
@@ -995,6 +1177,132 @@ function Form({
995
1177
  // Update the form data state with the auto values
996
1178
  setFormData(updatedFormData);
997
1179
 
1180
+ // Custom validation for json-table fields
1181
+ fields.forEach((item) => {
1182
+ if (item.type === 'json-table' && item.validation?.customValidation) {
1183
+ try {
1184
+ let jsonTableData = [];
1185
+ const fieldValue = formData[item.id];
1186
+
1187
+ if (fieldValue && fieldValue !== 'null' && fieldValue !== '') {
1188
+ const parsedValue = typeof fieldValue === 'string' ? JSON.parse(fieldValue) : fieldValue;
1189
+
1190
+ // Handle jsonPath setting to extract nested data
1191
+ if (item.jsonPath && parsedValue[item.jsonPath]) {
1192
+ jsonTableData = Array.isArray(parsedValue[item.jsonPath])
1193
+ ? parsedValue[item.jsonPath]
1194
+ : [];
1195
+ } else if (Array.isArray(parsedValue)) {
1196
+ jsonTableData = parsedValue;
1197
+ } else if (typeof parsedValue === 'object' && parsedValue !== null) {
1198
+ jsonTableData = [parsedValue];
1199
+ }
1200
+ }
1201
+
1202
+ // Perform validation using the same logic as in Field.jsx
1203
+ const validateJsonTable = (data, validationConfig) => {
1204
+ if (!validationConfig || !validationConfig.customValidation) {
1205
+ return { isValid: true, errors: [] };
1206
+ }
1207
+ const { type, startTimeField, endTimeField, message } = validationConfig.customValidation;
1208
+ const errors = [];
1209
+
1210
+ if (type === '24hour_coverage') {
1211
+ // Convert time strings to minutes for easier calculation
1212
+ const timeToMinutes = (timeStr) => {
1213
+ if (!timeStr) return null;
1214
+ const [hours, minutes] = timeStr.split(':').map(Number);
1215
+ return hours * 60 + minutes;
1216
+ };
1217
+
1218
+ // Sort intervals by start time
1219
+ const sortedIntervals = [...data].sort((a, b) => {
1220
+ const aStart = timeToMinutes(a[startTimeField]);
1221
+ const bStart = timeToMinutes(b[startTimeField]);
1222
+ return aStart - bStart;
1223
+ });
1224
+
1225
+ let totalCoverage = 0;
1226
+ let previousEnd = null;
1227
+
1228
+ for (let i = 0; i < sortedIntervals.length; i++) {
1229
+ const interval = sortedIntervals[i];
1230
+ const startMinutes = timeToMinutes(interval[startTimeField]);
1231
+ const endMinutes = timeToMinutes(interval[endTimeField]);
1232
+
1233
+ if (startMinutes === null || endMinutes === null) {
1234
+ errors.push(`Interval ${i + 1}: Missing start or end time`);
1235
+ continue;
1236
+ }
1237
+
1238
+ // Handle cross-midnight intervals (e.g., 18:00 to 06:00)
1239
+ let duration;
1240
+ if (endMinutes <= startMinutes) {
1241
+ duration = (endMinutes + 1440) - startMinutes;
1242
+ } else {
1243
+ duration = endMinutes - startMinutes;
1244
+ }
1245
+
1246
+ totalCoverage += duration;
1247
+
1248
+ // Check for gaps (except for the first interval)
1249
+ if (previousEnd !== null && startMinutes !== previousEnd) {
1250
+ const gapSize = startMinutes > previousEnd
1251
+ ? startMinutes - previousEnd
1252
+ : (startMinutes + 1440) - previousEnd;
1253
+
1254
+ if (gapSize > 0 && gapSize < 1440) {
1255
+ errors.push(`Gap detected between intervals: ${Math.floor(gapSize / 60)}h ${gapSize % 60}m`);
1256
+ }
1257
+ }
1258
+
1259
+ // Update previousEnd, handling cross-midnight
1260
+ previousEnd = endMinutes <= startMinutes ? endMinutes + 1440 : endMinutes;
1261
+ if (previousEnd >= 1440) previousEnd -= 1440;
1262
+ }
1263
+
1264
+ // Check if total coverage equals 24 hours (1440 minutes)
1265
+ if (Math.abs(totalCoverage - 1440) > 1) {
1266
+ errors.push(`Total coverage is ${Math.floor(totalCoverage / 60)}h ${totalCoverage % 60}m. Required: 24h 0m`);
1267
+ }
1268
+
1269
+ // Check for overlaps
1270
+ for (let i = 0; i < sortedIntervals.length - 1; i++) {
1271
+ const current = sortedIntervals[i];
1272
+ const next = sortedIntervals[i + 1];
1273
+
1274
+ const currentStart = timeToMinutes(current[startTimeField]);
1275
+ const currentEnd = timeToMinutes(current[endTimeField]);
1276
+ const nextStart = timeToMinutes(next[startTimeField]);
1277
+
1278
+ const currentActualEnd = currentEnd <= currentStart ? currentEnd + 1440 : currentEnd;
1279
+ const nextActualStart = nextStart;
1280
+
1281
+ if (currentActualEnd > nextActualStart && currentActualEnd < nextActualStart + 1440) {
1282
+ const overlapMinutes = currentActualEnd - nextActualStart;
1283
+ errors.push(`Overlap detected: ${Math.floor(overlapMinutes / 60)}h ${overlapMinutes % 60}m between intervals ${i + 1} and ${i + 2}`);
1284
+ }
1285
+ }
1286
+ }
1287
+
1288
+ return {
1289
+ isValid: errors.length === 0,
1290
+ errors: errors.length > 0 ? [message, ...errors] : []
1291
+ };
1292
+ };
1293
+
1294
+ const validationResult = validateJsonTable(jsonTableData, item.validation);
1295
+ if (!validationResult.isValid) {
1296
+ validation += `${item.label}: ${validationResult.errors.join(', ')}<br/>`;
1297
+ _inputClass[item.id] = styles.inputError;
1298
+ }
1299
+ } catch (e) {
1300
+ validation += `${item.label}: Invalid configuration data<br/>`;
1301
+ _inputClass[item.id] = styles.inputError;
1302
+ }
1303
+ }
1304
+ });
1305
+
998
1306
  if (validation) {
999
1307
  toast.error(<div>{parse(validation)}</div>);
1000
1308
  } else {
@@ -1856,6 +2164,9 @@ function Form({
1856
2164
  handleChangeSignature
1857
2165
  }
1858
2166
  onChangeToggle={handleChangeToggle}
2167
+ onChangeJsonTable={handleChangeJsonTable}
2168
+ onAddJsonTableRow={handleAddJsonTableRow}
2169
+ onDeleteJsonTableRow={handleDeleteJsonTableRow}
1859
2170
  onFileDownload={handleFileDownload}
1860
2171
  settings={item}
1861
2172
  setFormData={setFormData}
@@ -2498,6 +2809,9 @@ function Form({
2498
2809
  }
2499
2810
  onChangeSelect={handleChangeSelect}
2500
2811
  onChangeToggle={handleChangeToggle}
2812
+ onChangeJsonTable={handleChangeJsonTable}
2813
+ onAddJsonTableRow={handleAddJsonTableRow}
2814
+ onDeleteJsonTableRow={handleDeleteJsonTableRow}
2501
2815
  onFileDownload={handleFileDownload}
2502
2816
  settings={item}
2503
2817
  setFormData={setFormData}
@@ -1385,6 +1385,268 @@ input[type='file']:hover {
1385
1385
  box-shadow: none; /* Match react-select no-shadow pattern */
1386
1386
  }
1387
1387
 
1388
+ /* JSON Table Field Styles */
1389
+ .jsonTableLabel {
1390
+ margin-bottom: 0.5rem;
1391
+ font-weight: 500;
1392
+ font-size: 0.875rem;
1393
+ color: var(--paragraph-color);
1394
+ display: flex;
1395
+ align-items: center;
1396
+ line-height: 1.3;
1397
+ }
1398
+
1399
+ .jsonTableContainer {
1400
+ width: 100%;
1401
+ border: 1px solid var(--border-color, #dee2e6);
1402
+ border-radius: var(--br, 4px);
1403
+ background: var(--item-color, #fff);
1404
+ overflow: hidden;
1405
+ margin-bottom: 0.75rem;
1406
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
1407
+ }
1408
+
1409
+ .jsonTableWrapper {
1410
+ overflow-x: auto;
1411
+ }
1412
+
1413
+ .jsonTable {
1414
+ width: 100%;
1415
+ border-collapse: collapse;
1416
+ font-size: 0.8rem;
1417
+ background: var(--item-color, #fff);
1418
+ margin: 0 auto;
1419
+ border: none;
1420
+ border-spacing: 0;
1421
+ box-sizing: border-box;
1422
+ }
1423
+
1424
+ .jsonTable thead {
1425
+ display: table-row-group;
1426
+ }
1427
+
1428
+ .jsonTable th {
1429
+ background: var(--tertiary-color, #f8f9fa);
1430
+ color: var(--paragraph-color, #495057);
1431
+ font-weight: 600;
1432
+ padding: 0.5rem 0.75rem;
1433
+ text-align: left;
1434
+ border-bottom: 1px solid var(--border-color, #dee2e6);
1435
+ border-right: 1px solid var(--border-color, #dee2e6);
1436
+ font-size: 0.75rem;
1437
+ line-height: 1.2;
1438
+ letter-spacing: 0.01em;
1439
+ text-transform: uppercase;
1440
+ border-spacing: 0;
1441
+ border-collapse: collapse;
1442
+ }
1443
+
1444
+ .jsonTable th:last-child {
1445
+ border-right: none;
1446
+ }
1447
+
1448
+ .jsonTable td {
1449
+ padding: 0.4rem 0.75rem;
1450
+ border-bottom: 1px solid var(--table-border, #dee2e6);
1451
+ border-right: 1px solid var(--table-border, #dee2e6);
1452
+ vertical-align: middle;
1453
+ background: var(--item-color, #fff);
1454
+ line-height: 1;
1455
+ border-spacing: 0;
1456
+ border-collapse: collapse;
1457
+ }
1458
+
1459
+ .jsonTable td:last-child {
1460
+ border-right: none;
1461
+ }
1462
+
1463
+ .jsonTable tbody tr:nth-child(even) td {
1464
+ background: rgba(var(--border-color-rgb, 222, 226, 230), 0.3);
1465
+ }
1466
+
1467
+ .jsonTable tbody tr:hover td {
1468
+ background: rgba(var(--primary-color-rgb, 0, 123, 255), 0.03);
1469
+ }
1470
+
1471
+ .jsonTableInput {
1472
+ width: 100%;
1473
+ padding: 0.375rem 0.5rem;
1474
+ border: 1px solid var(--border-color, #dee2e6);
1475
+ border-radius: calc(var(--br, 4px) - 1px);
1476
+ background: var(--item-color, #fff);
1477
+ color: var(--paragraph-color);
1478
+ font-size: 0.8rem;
1479
+ line-height: 1.3;
1480
+ transition: border-color 0.15s ease;
1481
+ box-sizing: border-box;
1482
+ min-height: 1.8rem;
1483
+ }
1484
+
1485
+ .jsonTableInput:focus {
1486
+ outline: none;
1487
+ border-color: var(--primary-color, #007bff);
1488
+ background: var(--item-color, #fff);
1489
+ box-shadow: 0 0 0 1px rgba(var(--primary-color-rgb, 0, 123, 255), 0.25);
1490
+ }
1491
+
1492
+ .jsonTableInput:hover:not(:focus) {
1493
+ border-color: var(--border-color, #ced4da);
1494
+ }
1495
+
1496
+ .jsonTableTextarea {
1497
+ width: 100%;
1498
+ padding: 0.375rem 0.5rem;
1499
+ border: 1px solid var(--border-color, #dee2e6);
1500
+ border-radius: calc(var(--br, 4px) - 1px);
1501
+ background: var(--item-color, #fff);
1502
+ color: var(--paragraph-color);
1503
+ font-size: 0.8rem;
1504
+ line-height: 1.4;
1505
+ transition: border-color 0.15s ease;
1506
+ resize: vertical;
1507
+ min-height: 2rem;
1508
+ font-family: inherit;
1509
+ box-sizing: border-box;
1510
+ }
1511
+
1512
+ .jsonTableTextarea:focus {
1513
+ outline: none;
1514
+ border-color: var(--primary-color, #007bff);
1515
+ background: var(--item-color, #fff);
1516
+ box-shadow: 0 0 0 1px rgba(var(--primary-color-rgb, 0, 123, 255), 0.25);
1517
+ }
1518
+
1519
+ .jsonTableTextarea:hover:not(:focus) {
1520
+ border-color: var(--border-color, #ced4da);
1521
+ }
1522
+
1523
+ .jsonTableDisplay {
1524
+ padding: 0.5rem 0.375rem;
1525
+ color: var(--paragraph-color);
1526
+ font-size: 0.8rem;
1527
+ font-weight: 500;
1528
+ text-align: center;
1529
+ background: var(--tertiary-color, #f8f9fa);
1530
+ border-radius: calc(var(--br, 4px) - 1px);
1531
+ border: 1px solid var(--border-color, #ced4da);
1532
+ line-height: 1.3;
1533
+ user-select: none;
1534
+ min-height: 1.8rem;
1535
+ display: flex;
1536
+ align-items: center;
1537
+ justify-content: center;
1538
+ box-sizing: border-box;
1539
+ }
1540
+
1541
+ .validationErrors {
1542
+ margin-top: 0.75rem;
1543
+ padding: 0.75rem;
1544
+ border-radius: var(--br, 4px);
1545
+ border: 1px solid var(--danger-color, #dc3545);
1546
+ background: rgba(var(--danger-color-rgb, 220, 53, 69), 0.05);
1547
+ }
1548
+
1549
+ .validationError {
1550
+ color: var(--danger-color, #dc3545);
1551
+ font-size: 0.875rem;
1552
+ margin: 0 0 0.5rem 0;
1553
+ font-weight: 500;
1554
+
1555
+ &:last-child {
1556
+ margin-bottom: 0;
1557
+ }
1558
+
1559
+ &:first-child {
1560
+ font-weight: 600;
1561
+ margin-bottom: 0.75rem;
1562
+ padding-bottom: 0.5rem;
1563
+ border-bottom: 1px solid rgba(var(--danger-color-rgb, 220, 53, 69), 0.2);
1564
+ }
1565
+ }
1566
+
1567
+ .jsonTableActions {
1568
+ display: flex;
1569
+ justify-content: center;
1570
+ align-items: center;
1571
+ gap: 0.5rem;
1572
+ padding: 0.75rem;
1573
+ border-top: 1px solid var(--border-color, #dee2e6);
1574
+ background: var(--item-color, #fff);
1575
+ }
1576
+
1577
+ .jsonTableDeleteBtn {
1578
+ display: flex;
1579
+ align-items: center;
1580
+ justify-content: center;
1581
+ width: 1.5rem;
1582
+ height: 1.5rem;
1583
+ padding: 0;
1584
+ background: rgba(var(--danger-color-rgb, 220, 53, 69), 0.08);
1585
+ border: 1px solid rgba(var(--danger-color-rgb, 220, 53, 69), 0.2);
1586
+ border-radius: calc(var(--br, 4px) - 1px);
1587
+ color: var(--danger-color, #dc3545);
1588
+ cursor: pointer;
1589
+ transition: all 0.15s ease;
1590
+ font-size: 0.75rem;
1591
+ line-height: 1;
1592
+ }
1593
+
1594
+ .jsonTableDeleteBtn:hover {
1595
+ background: var(--danger-color, #dc3545);
1596
+ color: var(--secondary-color, #fff);
1597
+ border-color: var(--danger-color, #dc3545);
1598
+ }
1599
+
1600
+ .jsonTableDeleteBtn:focus {
1601
+ outline: none;
1602
+ box-shadow: 0 0 0 2px rgba(var(--danger-color-rgb, 220, 53, 69), 0.25);
1603
+ }
1604
+
1605
+ .jsonTableAddBtn {
1606
+ display: inline-flex;
1607
+ align-items: center;
1608
+ gap: 0.375rem;
1609
+ padding: 0.375rem 0.75rem;
1610
+ background: var(--primary-color, #007bff);
1611
+ border: 1px solid var(--primary-color, #007bff);
1612
+ border-radius: var(--br, 4px);
1613
+ color: var(--secondary-color, #fff);
1614
+ cursor: pointer;
1615
+ transition: all 0.15s ease;
1616
+ font-size: 0.75rem;
1617
+ font-weight: 500;
1618
+ text-decoration: none;
1619
+ outline: none;
1620
+ }
1621
+
1622
+ .jsonTableAddBtn:hover {
1623
+ background: rgba(var(--primary-color-rgb, 0, 123, 255), 0.9);
1624
+ border-color: rgba(var(--primary-color-rgb, 0, 123, 255), 0.9);
1625
+ }
1626
+
1627
+ .jsonTableAddBtn:focus {
1628
+ outline: none;
1629
+ box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb, 0, 123, 255), 0.25);
1630
+ }
1631
+
1632
+ .jsonTableAddBtn svg {
1633
+ width: 0.875rem;
1634
+ height: 0.875rem;
1635
+ }
1636
+
1637
+ .jsonTableActionsHeader {
1638
+ width: 60px;
1639
+ text-align: center;
1640
+ white-space: nowrap;
1641
+ }
1642
+
1643
+ .jsonTableActionsCell {
1644
+ text-align: center;
1645
+ vertical-align: middle;
1646
+ width: 60px;
1647
+ padding: 0.25rem !important;
1648
+ }
1649
+
1388
1650
  /* Dark mode support */
1389
1651
  @media (prefers-color-scheme: dark) {
1390
1652
  .emailSuggestions {
@@ -1401,4 +1663,76 @@ input[type='file']:hover {
1401
1663
  background: var(--dark-primary-color, #3182ce);
1402
1664
  color: var(--dark-primary-text-color, #fff);
1403
1665
  }
1666
+
1667
+ .jsonTableLabel {
1668
+ color: var(--dark-text-color, #f9fafb);
1669
+ }
1670
+
1671
+ .jsonTableContainer {
1672
+ background: var(--dark-item-color, #2d3748);
1673
+ border-color: var(--dark-border-color, #4a5568);
1674
+ }
1675
+
1676
+ .jsonTable {
1677
+ background: var(--dark-item-color, #2d3748);
1678
+ }
1679
+
1680
+ .jsonTable th {
1681
+ background: var(--dark-tertiary-color, #374151);
1682
+ color: var(--dark-text-color, #f9fafb);
1683
+ border-bottom-color: var(--dark-table-border, #4a5568);
1684
+ border-right-color: var(--dark-table-border, #4a5568);
1685
+ }
1686
+
1687
+ .jsonTable td {
1688
+ border-bottom-color: var(--dark-table-border, #4a5568);
1689
+ border-right-color: var(--dark-table-border, #4a5568);
1690
+ background: var(--dark-item-color, #2d3748);
1691
+ }
1692
+
1693
+ .jsonTable tbody tr:nth-child(even) td {
1694
+ background: var(--dark-tertiary-color, #374151);
1695
+ }
1696
+
1697
+ .jsonTable tbody tr:hover td {
1698
+ background: rgba(var(--dark-table-color-rgb, 74, 85, 104), 0.15);
1699
+ }
1700
+
1701
+ .jsonTableInput,
1702
+ .jsonTableTextarea {
1703
+ background: var(--dark-tertiary-color, #374151);
1704
+ border-color: var(--dark-border-color, #6b7280);
1705
+ color: var(--dark-text-color, #f9fafb);
1706
+ }
1707
+
1708
+ .jsonTableInput:focus,
1709
+ .jsonTableTextarea:focus {
1710
+ border-color: var(--dark-primary-color, #3182ce);
1711
+ box-shadow: 0 0 0 2px rgba(var(--dark-primary-color-rgb, 49, 130, 206), 0.15);
1712
+ }
1713
+
1714
+ .jsonTableInput:hover:not(:focus),
1715
+ .jsonTableTextarea:hover:not(:focus) {
1716
+ border-color: var(--dark-text-color, #e2e8f0);
1717
+ }
1718
+
1719
+ .jsonTableDisplay {
1720
+ background: var(--dark-tertiary-color, #374151);
1721
+ border-color: var(--dark-border-color, #6b7280);
1722
+ color: var(--dark-text-color, #f9fafb);
1723
+ }
1724
+
1725
+ .jsonTableActions {
1726
+ background: var(--dark-tertiary-color, #374151);
1727
+ border-top-color: var(--dark-table-border, #4a5568);
1728
+ }
1729
+
1730
+ .validationErrors {
1731
+ background: rgba(var(--danger-color-rgb, 220, 53, 69), 0.15);
1732
+ border-color: var(--danger-color, #dc3545);
1733
+ }
1734
+
1735
+ .validationError {
1736
+ color: var(--danger-color, #dc3545);
1737
+ }
1404
1738
  }