@cdc/core 4.25.8 → 4.25.10

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 (117) hide show
  1. package/components/AdvancedEditor/AdvancedEditor.tsx +29 -8
  2. package/components/DataTable/DataTable.tsx +56 -38
  3. package/components/DataTable/components/ChartHeader.tsx +44 -14
  4. package/components/DataTable/components/ExpandCollapse.tsx +10 -1
  5. package/components/DataTable/components/MapHeader.tsx +24 -13
  6. package/components/DataTable/data-table.css +6 -0
  7. package/components/DataTable/helpers/chartCellMatrix.tsx +11 -8
  8. package/components/DataTable/helpers/mapCellMatrix.tsx +19 -1
  9. package/components/DownloadButton.tsx +40 -14
  10. package/components/EditorPanel/components/MarkupHighlightedTextField.tsx +227 -0
  11. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +411 -0
  12. package/components/EditorPanel/components/PanelMarkup.tsx +59 -0
  13. package/components/ErrorBoundary.jsx +3 -1
  14. package/components/Filters/Filters.tsx +27 -20
  15. package/components/Filters/components/Tabs.tsx +1 -0
  16. package/components/Legend/Legend.Gradient.tsx +3 -6
  17. package/components/LegendShape.tsx +121 -3
  18. package/components/MediaControls.tsx +51 -3
  19. package/components/PaletteConversionModal.tsx +87 -0
  20. package/components/PaletteSelector/DeveloperPaletteRollback.tsx +114 -0
  21. package/components/PaletteSelector/PaletteSelector.css +51 -0
  22. package/components/PaletteSelector/PaletteSelector.tsx +112 -0
  23. package/components/PaletteSelector/index.ts +2 -0
  24. package/components/RichTooltip/RichTooltip.tsx +1 -0
  25. package/components/Table/Table.tsx +3 -1
  26. package/components/_stories/BlurStrokeTest.stories.tsx +1 -1
  27. package/components/_stories/DataTable.stories.tsx +1 -1
  28. package/components/_stories/Filters.stories.tsx +1 -1
  29. package/components/_stories/Footnotes.stories.tsx +1 -1
  30. package/components/_stories/Inputs.stories.tsx +1 -1
  31. package/components/_stories/MultiSelect.stories.tsx +3 -3
  32. package/components/_stories/NestedDropdown.stories.tsx +1 -1
  33. package/components/_stories/Table.stories.tsx +1 -1
  34. package/components/elements/_stories/Button.stories.tsx +1 -1
  35. package/components/elements/_stories/Card.stories.tsx +1 -1
  36. package/components/inputs/InputToggle.tsx +2 -0
  37. package/components/managers/DataDesigner.tsx +10 -9
  38. package/components/managers/_stories/DataDesigner.stories.tsx +1 -1
  39. package/components/ui/Tooltip.tsx +2 -1
  40. package/components/ui/_stories/Accordion.stories.tsx +1 -1
  41. package/components/ui/_stories/ColorPaletteMigration.stories.mdx +275 -0
  42. package/components/ui/_stories/Colors.stories.tsx +330 -0
  43. package/components/ui/_stories/IconGallery.stories.tsx +316 -0
  44. package/components/ui/_stories/Title.stories.tsx +1 -1
  45. package/contexts/EditorContext.ts +18 -0
  46. package/contexts/editor.actions.ts +28 -0
  47. package/contexts/editor.reducer.ts +94 -0
  48. package/data/chartColorPalettes.ts +118 -0
  49. package/data/colorPalettes.ts +9 -0
  50. package/data/mapColorPalettes.ts +45 -0
  51. package/data/sharedPalettes.ts +50 -0
  52. package/dist/cove-main.css +14 -11
  53. package/dist/cove-main.css.map +1 -1
  54. package/generateViteConfig.js +80 -0
  55. package/helpers/addValuesToFilters.ts +2 -3
  56. package/helpers/cloneConfig.ts +31 -0
  57. package/helpers/configDataHelpers.ts +128 -0
  58. package/helpers/configHelpers.ts +27 -0
  59. package/helpers/constants.ts +5 -2
  60. package/helpers/coveUpdateWorker.ts +13 -3
  61. package/helpers/filterColorPalettes.ts +152 -0
  62. package/helpers/generateColorsArray.ts +13 -0
  63. package/helpers/getColorPaletteVersion.ts +33 -0
  64. package/helpers/getPaletteAccessor.ts +18 -0
  65. package/helpers/markupProcessor.ts +205 -0
  66. package/helpers/metrics/helpers.ts +42 -19
  67. package/helpers/metrics/types.ts +48 -9
  68. package/helpers/metrics/utils.ts +34 -0
  69. package/helpers/palettes/colorDistributions.ts +56 -0
  70. package/helpers/palettes/migratePaletteName.ts +150 -0
  71. package/helpers/palettes/standardizePaletteNames.ts +77 -0
  72. package/helpers/palettes/utils.ts +267 -0
  73. package/helpers/queryStringUtils.ts +13 -0
  74. package/helpers/testing.ts +345 -0
  75. package/helpers/tests/addValuesToFilters.test.ts +1 -2
  76. package/helpers/tests/generateColorsArray.test.ts +24 -0
  77. package/helpers/tests/markupProcessor.test.ts +538 -0
  78. package/helpers/tests/testStandaloneBuild.ts +44 -0
  79. package/helpers/useMarkupVariables.ts +31 -0
  80. package/helpers/vegaConfig.ts +0 -1
  81. package/helpers/ver/4.24.10.ts +2 -1
  82. package/helpers/ver/4.24.11.ts +2 -1
  83. package/helpers/ver/4.24.3.ts +2 -1
  84. package/helpers/ver/4.24.4.ts +2 -1
  85. package/helpers/ver/4.24.5.ts +2 -1
  86. package/helpers/ver/4.24.7.ts +2 -1
  87. package/helpers/ver/4.24.9.ts +2 -1
  88. package/helpers/ver/4.25.1.ts +2 -1
  89. package/helpers/ver/4.25.10.ts +36 -0
  90. package/helpers/ver/4.25.3.ts +2 -1
  91. package/helpers/ver/4.25.4.ts +2 -1
  92. package/helpers/ver/4.25.6.ts +2 -1
  93. package/helpers/ver/4.25.7.ts +2 -1
  94. package/helpers/ver/4.25.8.ts +2 -1
  95. package/helpers/ver/4.25.9.ts +293 -0
  96. package/helpers/ver/tests/4.25.10.test.ts +204 -0
  97. package/helpers/ver/tests/4.25.8.test.ts +1 -1
  98. package/helpers/ver/tests/4.25.9.test.ts +51 -0
  99. package/hooks/useColorPalette.ts +79 -0
  100. package/package.json +12 -4
  101. package/styles/_global.scss +7 -5
  102. package/styles/base.scss +8 -5
  103. package/styles/v2/components/button.scss +4 -3
  104. package/styles/v2/components/editor.scss +2 -1
  105. package/styles/v2/layout/_data-table.scss +3 -2
  106. package/styles/v2/themes/_color-definitions.scss +18 -17
  107. package/testBuild.js +0 -0
  108. package/testing-setup.js +32 -0
  109. package/types/MarkupInclude.ts +6 -1
  110. package/types/MarkupVariable.ts +19 -0
  111. package/types/VizFilter.ts +1 -0
  112. package/vitest.config.ts +16 -0
  113. package/components/ui/_stories/Colors.stories.mdx +0 -220
  114. package/components/ui/_stories/IconGallery.stories.mdx +0 -14
  115. package/data/colorPalettes.js +0 -171
  116. package/helpers/formatConfigBeforeSave.ts +0 -135
  117. package/helpers/tests/formatConfigBeforeSave.test.ts +0 -68
@@ -0,0 +1,227 @@
1
+ import React, { memo, useEffect, useState, useRef, useMemo } from 'react'
2
+ import { useDebounce } from 'use-debounce'
3
+ import { MarkupVariable } from '../../../types/MarkupVariable'
4
+
5
+ type MarkupHighlightedTextFieldProps = {
6
+ className?: string
7
+ value: string | number
8
+ type?: 'text' | 'textarea'
9
+ label: string
10
+ placeholder?: string
11
+ fieldName?: string
12
+ section?: any
13
+ subsection?: any
14
+ updateField?: (section: any, subsection: any, fieldName: string, value: string) => void
15
+ markupVariables?: MarkupVariable[]
16
+ isEditor?: boolean
17
+ }
18
+
19
+ const MarkupHighlightedTextField: React.FC<MarkupHighlightedTextFieldProps> = memo((props) => {
20
+ const {
21
+ value: stateValue,
22
+ type = 'text',
23
+ label,
24
+ placeholder = '',
25
+ fieldName = '',
26
+ section = null,
27
+ subsection = null,
28
+ updateField,
29
+ markupVariables = [],
30
+ isEditor = false,
31
+ className = '',
32
+ ...attributes
33
+ } = props
34
+
35
+ const [value, setValue] = useState(String(stateValue))
36
+ const [debouncedValue] = useDebounce(value, 300) // Reduced debounce for faster response
37
+ const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null)
38
+ const highlightRef = useRef<HTMLDivElement>(null)
39
+
40
+ // Update field when debounced value changes
41
+ useEffect(() => {
42
+ if (stateValue !== debouncedValue && updateField) {
43
+ updateField(section, subsection, fieldName, debouncedValue)
44
+ }
45
+ }, [debouncedValue, section, subsection, fieldName, updateField, stateValue])
46
+
47
+ // Update local state when props change
48
+ useEffect(() => {
49
+ setValue(String(stateValue))
50
+ }, [stateValue])
51
+
52
+ // Get valid markup variable tags for highlighting (memoized)
53
+ const validTags = useMemo(() =>
54
+ markupVariables.map(variable => variable.tag).filter(Boolean),
55
+ [markupVariables]
56
+ )
57
+
58
+ // Memoized highlighting function to reduce recalculations
59
+ const highlightedContent = useMemo(() => {
60
+ if (!isEditor || validTags.length === 0) {
61
+ return value
62
+ }
63
+
64
+ // Escape HTML
65
+ const escapedText = value.replace(/[&<>"']/g, (match) => {
66
+ const escapeMap: { [key: string]: string } = {
67
+ '&': '&amp;',
68
+ '<': '&lt;',
69
+ '>': '&gt;',
70
+ '"': '&quot;',
71
+ "'": '&#39;'
72
+ }
73
+ return escapeMap[match]
74
+ })
75
+
76
+ // Highlight valid markup variables
77
+ return escapedText.replace(/\{\{([^}]+)\}\}/g, (match) => {
78
+ const isValid = validTags.includes(match)
79
+ const cssClass = isValid ? 'markup-variable-valid' : 'markup-variable-invalid'
80
+ return `<span class="${cssClass}">${match}</span>`
81
+ })
82
+ }, [value, validTags, isEditor])
83
+
84
+ // Optimized scroll sync
85
+ const handleScroll = useMemo(() => {
86
+ let rafId: number | null = null
87
+ return () => {
88
+ if (rafId) cancelAnimationFrame(rafId)
89
+ rafId = requestAnimationFrame(() => {
90
+ if (inputRef.current && highlightRef.current) {
91
+ highlightRef.current.scrollTop = inputRef.current.scrollTop
92
+ highlightRef.current.scrollLeft = inputRef.current.scrollLeft
93
+ }
94
+ })
95
+ }
96
+ }, [])
97
+
98
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
99
+ setValue(e.target.value)
100
+ }
101
+
102
+ const inputId = `${fieldName}-${section}-${subsection}`
103
+ const shouldShowHighlighting = isEditor && validTags.length > 0
104
+
105
+ // Optimized styles
106
+ const containerStyle = useMemo(() => ({
107
+ position: 'relative' as const,
108
+ display: 'block',
109
+ width: '100%',
110
+ }), [])
111
+
112
+ const inputStyle = useMemo(() => ({
113
+ width: '100%',
114
+ padding: '8px 12px',
115
+ border: '1px solid #ddd',
116
+ borderRadius: '4px',
117
+ fontFamily: 'inherit',
118
+ fontSize: '14px',
119
+ lineHeight: '1.4',
120
+ background: shouldShowHighlighting ? 'transparent' : '#fff',
121
+ color: shouldShowHighlighting ? 'transparent' : 'inherit',
122
+ caretColor: shouldShowHighlighting ? '#333' : 'inherit',
123
+ position: 'relative' as const,
124
+ zIndex: 2,
125
+ resize: type === 'textarea' ? 'vertical' as const : 'none' as const,
126
+ }), [shouldShowHighlighting, type])
127
+
128
+ const highlightStyle = useMemo(() => ({
129
+ position: 'absolute' as const,
130
+ top: 0,
131
+ left: 0,
132
+ right: 0,
133
+ bottom: 0,
134
+ padding: '8px 12px',
135
+ margin: 0,
136
+ border: '1px solid transparent',
137
+ background: '#fff',
138
+ borderRadius: '4px',
139
+ color: '#333',
140
+ whiteSpace: type === 'textarea' ? 'pre-wrap' as const : 'pre' as const,
141
+ overflow: 'hidden',
142
+ pointerEvents: 'none' as const,
143
+ fontFamily: 'inherit',
144
+ fontSize: '14px',
145
+ lineHeight: '1.4',
146
+ zIndex: 1,
147
+ wordWrap: 'break-word' as const,
148
+ }), [type])
149
+
150
+ const focusStyle = useMemo(() => ({
151
+ outline: 'none',
152
+ borderColor: '#0066cc',
153
+ boxShadow: '0 0 0 2px rgba(0, 102, 204, 0.2)',
154
+ }), [])
155
+
156
+ return (
157
+ <div className={`form-group ${className}`}>
158
+ <label htmlFor={inputId}>
159
+ <span className='edit-label'>{label}</span>
160
+ <div style={containerStyle}>
161
+ {/* Highlighting overlay */}
162
+ {shouldShowHighlighting && (
163
+ <div
164
+ ref={highlightRef}
165
+ style={highlightStyle}
166
+ dangerouslySetInnerHTML={{ __html: highlightedContent }}
167
+ className="markup-highlight-overlay"
168
+ />
169
+ )}
170
+
171
+ {/* Input field */}
172
+ {type === 'textarea' ? (
173
+ <textarea
174
+ ref={inputRef as React.RefObject<HTMLTextAreaElement>}
175
+ id={inputId}
176
+ value={value}
177
+ onChange={handleChange}
178
+ onScroll={handleScroll}
179
+ placeholder={placeholder}
180
+ style={inputStyle}
181
+ onFocus={(e) => Object.assign(e.target.style, focusStyle)}
182
+ onBlur={(e) => Object.assign(e.target.style, inputStyle)}
183
+ {...attributes}
184
+ />
185
+ ) : (
186
+ <input
187
+ ref={inputRef as React.RefObject<HTMLInputElement>}
188
+ type="text"
189
+ id={inputId}
190
+ value={value}
191
+ onChange={handleChange}
192
+ onScroll={handleScroll}
193
+ placeholder={placeholder}
194
+ style={inputStyle}
195
+ onFocus={(e) => Object.assign(e.target.style, focusStyle)}
196
+ onBlur={(e) => Object.assign(e.target.style, inputStyle)}
197
+ {...attributes}
198
+ />
199
+ )}
200
+ </div>
201
+ </label>
202
+
203
+ <style jsx>{`
204
+ .markup-highlight-overlay .markup-variable-valid {
205
+ background-color: #e6f3ff;
206
+ color: #0066cc;
207
+ border-radius: 3px;
208
+ padding: 0px 2px;
209
+ font-weight: 500;
210
+ }
211
+
212
+ .markup-highlight-overlay .markup-variable-invalid {
213
+ background-color: #ffe6e6;
214
+ color: #cc0000;
215
+ border-radius: 3px;
216
+ padding: 0px 2px;
217
+ font-weight: 500;
218
+ text-decoration: underline wavy #cc0000;
219
+ }
220
+ `}</style>
221
+ </div>
222
+ )
223
+ })
224
+
225
+ MarkupHighlightedTextField.displayName = 'MarkupHighlightedTextField'
226
+
227
+ export default MarkupHighlightedTextField
@@ -0,0 +1,411 @@
1
+ import React, { useState, useMemo } from 'react'
2
+ import { MarkupVariable, MarkupCondition } from '../../../types/MarkupVariable'
3
+ import Button from '../../elements/Button'
4
+ import { TextField, Select, CheckBox } from '../Inputs'
5
+ import Icon from '../../ui/Icon'
6
+ import Accordion from '../../ui/Accordion'
7
+
8
+ type MarkupVariablesEditorProps = {
9
+ /** Array of markup variable configurations */
10
+ markupVariables: MarkupVariable[]
11
+ /** Dataset to extract column names and values from */
12
+ data: any[]
13
+ /** Callback when variables are added, updated, or removed */
14
+ onChange: (variables: MarkupVariable[]) => void
15
+ /** Whether markup variables feature is enabled */
16
+ enableMarkupVariables?: boolean
17
+ /** Callback when enable/disable toggle changes */
18
+ onToggleEnable?: (enabled: boolean) => void
19
+ }
20
+
21
+ export type { MarkupVariablesEditorProps }
22
+
23
+ /**
24
+ * Editor for creating and managing markup variables with {{variable-name}} syntax.
25
+ * Supports conditional filters, number formatting, and auto-generated tags.
26
+ */
27
+ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
28
+ markupVariables = [],
29
+ data = [],
30
+ onChange,
31
+ enableMarkupVariables = false,
32
+ onToggleEnable
33
+ }) => {
34
+ const [editingIndex, setEditingIndex] = useState<number | null>(null)
35
+ const [validationErrors, setValidationErrors] = useState<Record<number, string[]>>({})
36
+
37
+ // Ensure we always have a valid array
38
+ const safeMarkupVariables = markupVariables || []
39
+ const availableColumns = data.length > 0 ? Object.keys(data[0]) : []
40
+
41
+ // Validate a variable and return array of error messages
42
+ const validateVariable = React.useCallback((variable: MarkupVariable): string[] => {
43
+ const errors: string[] = []
44
+ if (!variable.name || variable.name.trim() === '') {
45
+ errors.push('Variable name is required')
46
+ }
47
+ if (!variable.tag || variable.tag.trim() === '') {
48
+ errors.push('Variable tag is required')
49
+ }
50
+ if (!variable.columnName || variable.columnName.trim() === '') {
51
+ errors.push('Data column is required')
52
+ }
53
+ // Validate conditions
54
+ variable.conditions?.forEach((condition, index) => {
55
+ if (!condition.columnName) {
56
+ errors.push(`Condition ${index + 1}: Column is required`)
57
+ }
58
+ if (!condition.value) {
59
+ errors.push(`Condition ${index + 1}: Value is required`)
60
+ }
61
+ })
62
+ return errors
63
+ }, [])
64
+
65
+ // Validate all variables on mount and when variables change
66
+ React.useEffect(() => {
67
+ const errors: Record<number, string[]> = {}
68
+ safeMarkupVariables.forEach((variable, index) => {
69
+ const variableErrors = validateVariable(variable)
70
+ if (variableErrors.length > 0) {
71
+ errors[index] = variableErrors
72
+ }
73
+ })
74
+ setValidationErrors(errors)
75
+ }, [safeMarkupVariables, validateVariable]) // Re-validate when variables change
76
+
77
+
78
+ const addVariable = () => {
79
+ const newVariable: MarkupVariable = {
80
+ name: '',
81
+ tag: '',
82
+ columnName: '',
83
+ conditions: [],
84
+ addCommas: false,
85
+ hideOnNull: false
86
+ }
87
+ const newVariables = [...safeMarkupVariables, newVariable]
88
+ onChange(newVariables)
89
+ const newIndex = safeMarkupVariables.length
90
+ setEditingIndex(newIndex)
91
+
92
+ // Immediately show validation errors for the new empty variable
93
+ const errors = validateVariable(newVariable)
94
+ setValidationErrors(prev => ({
95
+ ...prev,
96
+ [newIndex]: errors
97
+ }))
98
+ }
99
+
100
+ const updateVariable = (index: number, updates: Partial<MarkupVariable>) => {
101
+ const updated = safeMarkupVariables.map((variable, i) =>
102
+ i === index ? { ...variable, conditions: variable.conditions || [], ...updates } : variable
103
+ )
104
+ onChange(updated)
105
+
106
+ // Validate and update errors for this variable
107
+ const errors = validateVariable(updated[index])
108
+ setValidationErrors(prev => ({
109
+ ...prev,
110
+ [index]: errors
111
+ }))
112
+ }
113
+
114
+ const removeVariable = (index: number) => {
115
+ const updated = safeMarkupVariables.filter((_, i) => i !== index)
116
+ onChange(updated)
117
+ if (editingIndex === index) {
118
+ setEditingIndex(null)
119
+ }
120
+ }
121
+
122
+ const generateTag = (name: string) => {
123
+ if (!name) return ''
124
+ // Convert name to tag format: "My Variable" -> "{{my-variable}}"
125
+ return `{{${name.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')}}}`
126
+ }
127
+
128
+ // Get unique values for a given column for condition dropdowns
129
+ const getColumnValues = useMemo(() => {
130
+ if (!data || data.length === 0) return {}
131
+
132
+ const columnValues: Record<string, (string | number)[]> = {}
133
+ availableColumns.forEach(column => {
134
+ const uniqueValues = Array.from(new Set(data.map(row => row[column])))
135
+ .filter(val => val !== null && val !== undefined && val !== '')
136
+ columnValues[column] = uniqueValues
137
+ })
138
+ return columnValues
139
+ }, [data, availableColumns])
140
+
141
+ const addCondition = (variableIndex: number) => {
142
+ const variable = safeMarkupVariables[variableIndex]
143
+ const newCondition: MarkupCondition = {
144
+ columnName: '',
145
+ isOrIsNotEqualTo: 'is',
146
+ value: ''
147
+ }
148
+ const updatedConditions = [...(variable.conditions || []), newCondition]
149
+ updateVariable(variableIndex, { conditions: updatedConditions })
150
+ }
151
+
152
+ const updateCondition = (variableIndex: number, conditionIndex: number, updates: Partial<MarkupCondition>) => {
153
+ const variable = safeMarkupVariables[variableIndex]
154
+ const conditions = variable.conditions || []
155
+ const updatedConditions = conditions.map((condition, i) =>
156
+ i === conditionIndex ? { ...condition, ...updates } : condition
157
+ )
158
+ updateVariable(variableIndex, { conditions: updatedConditions })
159
+ }
160
+
161
+ const removeCondition = (variableIndex: number, conditionIndex: number) => {
162
+ const variable = safeMarkupVariables[variableIndex]
163
+ const updatedConditions = variable.conditions.filter((_, i) => i !== conditionIndex)
164
+ updateVariable(variableIndex, { conditions: updatedConditions })
165
+ }
166
+
167
+ return (
168
+ <div className='markup-variables-editor'>
169
+ <div className='mb-3'>
170
+ <CheckBox
171
+ value={enableMarkupVariables}
172
+ fieldName='enableMarkupVariables'
173
+ label='Enable Markup Variables'
174
+ updateField={(_section, _subsection, _fieldName, value) => {
175
+ if (onToggleEnable) {
176
+ onToggleEnable(value)
177
+ }
178
+ }}
179
+ />
180
+ </div>
181
+
182
+ {enableMarkupVariables && (
183
+ <>
184
+ <div className='mb-3'>
185
+ <p className='text-sm text-gray-600'>
186
+ Use variables in your content with <code>{'{{variable-name}}'}</code> syntax.
187
+ Variables will be replaced with values from your data.
188
+ </p>
189
+ </div>
190
+
191
+ {safeMarkupVariables.length > 0 && (
192
+ <div className='variables-list mb-3'>
193
+ {safeMarkupVariables.map((variable, index) => variable ? (
194
+ <div key={index} className='variable-item p-3 border rounded mb-2' style={{ backgroundColor: '#fff' }}>
195
+ <div className='d-flex justify-content-between align-items-start mb-2'>
196
+ <div style={{ flex: 1 }}>
197
+ <div style={{ fontSize: '16px', fontWeight: 600, marginBottom: '4px' }}>
198
+ {variable.name || 'Unnamed Variable'}
199
+ </div>
200
+ <div style={{ fontSize: '13px', color: '#6c757d', fontFamily: 'monospace', marginBottom: '4px' }}>
201
+ {variable.tag}
202
+ </div>
203
+ <div style={{ fontSize: '13px', color: '#6c757d' }}>
204
+ Column: <strong>{variable.columnName || 'Not selected'}</strong>
205
+ {variable.conditions && variable.conditions.length > 0 && (
206
+ <span> • {variable.conditions.length} condition{variable.conditions.length !== 1 ? 's' : ''}</span>
207
+ )}
208
+ </div>
209
+ {validationErrors[index] && validationErrors[index].length > 0 && (
210
+ <div style={{ fontSize: '12px', color: '#dc3545', marginTop: '8px' }}>
211
+ <strong>⚠ Validation Errors:</strong>
212
+ <ul style={{ margin: '4px 0 0 0', paddingLeft: '20px' }}>
213
+ {validationErrors[index].map((error, errorIndex) => (
214
+ <li key={errorIndex}>{error}</li>
215
+ ))}
216
+ </ul>
217
+ </div>
218
+ )}
219
+ </div>
220
+ <div className='d-flex gap-2'>
221
+ <Button
222
+ className='btn-sm'
223
+ onClick={() => setEditingIndex(editingIndex === index ? null : index)}
224
+ >
225
+ {editingIndex === index ? 'Close' : 'Edit'}
226
+ </Button>
227
+ </div>
228
+ </div>
229
+
230
+ {editingIndex === index && (
231
+ <div className='mt-3 pt-3 border-t'>
232
+ <Accordion>
233
+ <Accordion.Section title='Basic Settings'>
234
+ <div className='mb-3'>
235
+ <TextField
236
+ value={variable.name}
237
+ fieldName='name'
238
+ label='Variable Name'
239
+ placeholder='e.g., "State Name"'
240
+ updateField={(section, subsection, fieldName, value) => {
241
+ updateVariable(index, {
242
+ name: value,
243
+ tag: generateTag(value)
244
+ })
245
+ }}
246
+ />
247
+ </div>
248
+ <div className='mb-3'>
249
+ <label>
250
+ <span className='edit-label column-heading'>Tag (auto-generated)</span>
251
+ <input
252
+ type='text'
253
+ value={variable.tag}
254
+ placeholder='{{variable-name}}'
255
+ readOnly
256
+ style={{ backgroundColor: '#e9ecef', cursor: 'not-allowed' }}
257
+ />
258
+ </label>
259
+ </div>
260
+
261
+ <div className='mb-3'>
262
+ <Select
263
+ value={variable.columnName}
264
+ fieldName='columnName'
265
+ label='Data Column'
266
+ options={[
267
+ { value: '', label: 'Select Column...' },
268
+ ...availableColumns.map(col => ({ value: col, label: col }))
269
+ ]}
270
+ updateField={(_section, _subsection, _fieldName, value) => {
271
+ updateVariable(index, { columnName: value })
272
+ }}
273
+ />
274
+ </div>
275
+ </Accordion.Section>
276
+
277
+ <Accordion.Section title='Conditions'>
278
+ <div className='text-sm text-gray-500 mb-2'>
279
+ Add conditions to filter when this variable should display data
280
+ </div>
281
+
282
+ {variable.conditions && variable.conditions.length > 0 && (
283
+ <div className='conditions-list mb-2'>
284
+ {variable.conditions.map((condition, conditionIndex) => (
285
+ <div key={`condition-${index}-${conditionIndex}`} className='condition-item p-2 border rounded mb-2' style={{ backgroundColor: '#f8f9fa' }}>
286
+ <div className='mb-2'>
287
+ <Select
288
+ value={condition.columnName || ''}
289
+ fieldName={`condition-column-${index}-${conditionIndex}`}
290
+ label='Column'
291
+ options={[
292
+ { value: '', label: 'Select Column...' },
293
+ ...availableColumns.map(col => ({ value: col, label: col }))
294
+ ]}
295
+ updateField={(_section, _subsection, _fieldName, newColumnName) => {
296
+ // Reset value when column changes
297
+ updateCondition(index, conditionIndex, {
298
+ columnName: newColumnName,
299
+ value: ''
300
+ })
301
+ }}
302
+ />
303
+ </div>
304
+ <div className='mb-2'>
305
+ <Select
306
+ value={condition.isOrIsNotEqualTo || 'is'}
307
+ fieldName={`condition-operator-${index}-${conditionIndex}`}
308
+ label='Operator'
309
+ options={[
310
+ { value: 'is', label: 'is' },
311
+ { value: 'is not', label: 'is not' }
312
+ ]}
313
+ updateField={(_section, _subsection, _fieldName, value) => {
314
+ updateCondition(index, conditionIndex, { isOrIsNotEqualTo: value as 'is' | 'is not' })
315
+ }}
316
+ />
317
+ </div>
318
+ <div className='mb-2'>
319
+ <Select
320
+ value={condition.value || ''}
321
+ fieldName={`condition-value-${index}-${conditionIndex}`}
322
+ label='Value'
323
+ options={[
324
+ { value: '', label: 'Select Value...' },
325
+ ...(condition.columnName && getColumnValues[condition.columnName]
326
+ ? getColumnValues[condition.columnName].map(val => ({
327
+ value: String(val),
328
+ label: String(val)
329
+ }))
330
+ : [])
331
+ ]}
332
+ updateField={(_section, _subsection, _fieldName, value) => {
333
+ updateCondition(index, conditionIndex, { value })
334
+ }}
335
+ />
336
+ </div>
337
+ <Button
338
+ className='btn-sm btn-danger'
339
+ onClick={() => removeCondition(index, conditionIndex)}
340
+ >
341
+ Remove Condition
342
+ </Button>
343
+ </div>
344
+ ))}
345
+ </div>
346
+ )}
347
+
348
+ <Button
349
+ className='btn-sm'
350
+ onClick={() => addCondition(index)}
351
+ >
352
+ <Icon display='plus' size={14} className='mr-1' />
353
+ Add Condition
354
+ </Button>
355
+ </Accordion.Section>
356
+
357
+ <Accordion.Section title='Formatting Options'>
358
+ <div className='mb-3'>
359
+ <CheckBox
360
+ value={variable.addCommas || false}
361
+ fieldName='addCommas'
362
+ label='Format numbers with commas'
363
+ updateField={(_section, _subsection, _fieldName, value) =>
364
+ updateVariable(index, { addCommas: value })
365
+ }
366
+ />
367
+ </div>
368
+
369
+ <div className='mb-3'>
370
+ <CheckBox
371
+ value={variable.hideOnNull || false}
372
+ fieldName='hideOnNull'
373
+ label='Hide section when value is null'
374
+ updateField={(_section, _subsection, _fieldName, value) =>
375
+ updateVariable(index, { hideOnNull: value })
376
+ }
377
+ />
378
+ </div>
379
+ </Accordion.Section>
380
+ </Accordion>
381
+
382
+ <div className='mt-3 pt-3 border-t' style={{ textAlign: 'center' }}>
383
+ <Button
384
+ className='btn-sm btn-danger'
385
+ onClick={() => {
386
+ if (window.confirm(`Are you sure you want to delete the variable "${variable.name || 'Unnamed Variable'}"?`)) {
387
+ removeVariable(index)
388
+ }
389
+ }}
390
+ >
391
+ Delete Variable
392
+ </Button>
393
+ </div>
394
+ </div>
395
+ )}
396
+ </div>
397
+ ) : <></>)}
398
+ </div>
399
+ )}
400
+
401
+ <Button className='btn-primary' onClick={addVariable}>
402
+ <Icon display='plus' size={16} className='mr-2' />
403
+ Add Variable
404
+ </Button>
405
+ </>
406
+ )}
407
+ </div>
408
+ )
409
+ }
410
+
411
+ export default MarkupVariablesEditor