@cdc/core 4.25.10 → 4.25.11

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 (73) hide show
  1. package/_stories/StoryRenderingTests.stories.tsx +164 -0
  2. package/components/AdvancedEditor/AdvancedEditor.tsx +3 -1
  3. package/components/CustomColorsEditor/CustomColorsEditor.css +299 -0
  4. package/components/CustomColorsEditor/CustomColorsEditor.tsx +209 -0
  5. package/components/CustomColorsEditor/index.ts +1 -0
  6. package/components/DataTable/DataTableStandAlone.tsx +8 -3
  7. package/components/DataTable/components/DataTableEditorPanel.tsx +12 -2
  8. package/components/DataTable/data-table.css +6 -0
  9. package/components/DataTable/helpers/mapCellMatrix.tsx +14 -3
  10. package/components/DataTable/helpers/standardizeState.js +2 -2
  11. package/components/DataTable/helpers/tests/standardizeState.test.js +54 -0
  12. package/components/EditorPanel/DataTableEditor.tsx +3 -3
  13. package/components/EditorPanel/EditorPanel.styles.css +423 -0
  14. package/components/EditorPanel/FootnotesEditor.tsx +44 -37
  15. package/components/EditorPanel/Inputs.tsx +12 -2
  16. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +35 -62
  17. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +12 -2
  18. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +61 -22
  19. package/components/Filters/Filters.tsx +26 -5
  20. package/components/Filters/components/Dropdown.tsx +6 -1
  21. package/components/Footnotes/Footnotes.tsx +35 -25
  22. package/components/Footnotes/FootnotesStandAlone.tsx +42 -6
  23. package/components/HeaderThemeSelector/HeaderThemeSelector.css +43 -0
  24. package/components/HeaderThemeSelector/HeaderThemeSelector.stories.tsx +74 -0
  25. package/components/HeaderThemeSelector/HeaderThemeSelector.tsx +61 -0
  26. package/components/HeaderThemeSelector/index.ts +2 -0
  27. package/components/Layout/styles/editor.scss +2 -1
  28. package/components/Loader/Loader.tsx +1 -1
  29. package/components/MediaControls.tsx +21 -18
  30. package/components/PaletteConversionModal.tsx +7 -4
  31. package/components/PaletteSelector/PaletteSelector.css +49 -6
  32. package/components/Table/components/Cell.tsx +23 -2
  33. package/components/Table/components/Row.tsx +5 -3
  34. package/components/_stories/Filters.stories.tsx +20 -1
  35. package/components/_stories/Footnotes.CSV.stories.tsx +247 -0
  36. package/components/_stories/Footnotes.stories.tsx +768 -3
  37. package/components/_stories/Inputs.stories.tsx +2 -2
  38. package/components/_stories/styles.scss +0 -1
  39. package/components/ui/Accordion.jsx +1 -1
  40. package/components/ui/accordion.styles.css +57 -0
  41. package/data/chartColorPalettes.ts +1 -1
  42. package/dist/cove-main.css +49 -3
  43. package/dist/cove-main.css.map +1 -1
  44. package/helpers/addValuesToFilters.ts +5 -0
  45. package/helpers/constants.ts +37 -0
  46. package/helpers/cove/number.ts +33 -12
  47. package/helpers/coveUpdateWorker.ts +3 -1
  48. package/helpers/fetchRemoteData.ts +3 -15
  49. package/helpers/markupProcessor.ts +27 -12
  50. package/helpers/mergeCustomOrderValues.ts +37 -0
  51. package/helpers/parseCsvWithQuotes.ts +65 -0
  52. package/helpers/testing.ts +17 -4
  53. package/helpers/ver/4.25.11.ts +13 -0
  54. package/helpers/viewports.ts +2 -0
  55. package/package.json +4 -3
  56. package/styles/_common-components.css +73 -0
  57. package/styles/_global.scss +25 -5
  58. package/styles/base.scss +0 -50
  59. package/styles/cove-main.scss +3 -1
  60. package/styles/filters.scss +10 -3
  61. package/styles/v2/base/index.scss +0 -1
  62. package/styles/v2/components/editor.scss +14 -6
  63. package/styles/v2/utils/_breakpoints.scss +1 -1
  64. package/styles/v2/utils/index.scss +0 -1
  65. package/styles/waiting.scss +1 -1
  66. package/types/MarkupInclude.ts +4 -3
  67. package/types/MarkupVariable.ts +1 -1
  68. package/types/VizFilter.ts +1 -0
  69. package/styles/_mixins.scss +0 -13
  70. package/styles/_typography.scss +0 -0
  71. package/styles/v2/base/_typography.scss +0 -0
  72. package/styles/v2/components/guidance-block.scss +0 -74
  73. package/styles/v2/utils/_functions.scss +0 -0
@@ -0,0 +1,164 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { within, expect } from 'storybook/test'
3
+ import { performAndAssert } from '@cdc/core/helpers/testing'
4
+
5
+ const ChartRenderingValidator = () => (
6
+ <div data-testid='chart-rendering-validator'>
7
+ <h2>Simple COVE Visualization Tests</h2>
8
+ <p>This test validates all stories load and render.</p>
9
+ </div>
10
+ )
11
+
12
+ const meta: Meta<typeof ChartRenderingValidator> = {
13
+ title: 'Testing/Story Rendering Tests',
14
+ component: ChartRenderingValidator,
15
+ parameters: {
16
+ layout: 'fullscreen'
17
+ },
18
+ tags: ['!dev', '!autodocs']
19
+ }
20
+
21
+ export default meta
22
+ type Story = StoryObj<typeof ChartRenderingValidator>
23
+
24
+ /**
25
+ * Fetch all stories from Storybook's JSON API and filter for visualization stories
26
+ * @returns Promise that resolves to an array of story URLs to test
27
+ */
28
+ const getVisualizationStoryUrls = async (): Promise<string[]> => {
29
+ let response
30
+ try {
31
+ response = await fetch('http://localhost:6006/index.json')
32
+ } catch (error) {
33
+ console.error('Error fetching visualization story URLs:', error)
34
+ return []
35
+ }
36
+
37
+ const data = await response.json()
38
+
39
+ const storyUrls: string[] = []
40
+
41
+ Object.values(data.entries).forEach((story: any) => {
42
+ if (story.type === 'story') {
43
+ const isVisualizationStory =
44
+ story.title.includes('Components/Templates/') &&
45
+ !story.name.toLowerCase().includes('test') &&
46
+ !story.title.includes('Guide')
47
+
48
+ if (isVisualizationStory) {
49
+ const iframeUrl = `http://localhost:6006/iframe.html?id=${story.id}`
50
+ storyUrls.push(iframeUrl)
51
+ }
52
+ }
53
+ })
54
+ return storyUrls
55
+ }
56
+
57
+ /**
58
+ * Convert iframe URL to Storybook story URL for better debugging
59
+ * @param iframeUrl - The iframe URL (e.g., 'http://localhost:6006/iframe.html?id=components-templates-chart--multiple-lines')
60
+ * @returns The Storybook story URL (e.g., 'http://localhost:6006/?path=/story/components-templates-chart--multiple-lines')
61
+ */
62
+ const iframeUrlToStoryUrl = (iframeUrl: string): string => {
63
+ const url = new URL(iframeUrl)
64
+ const storyId = url.searchParams.get('id')
65
+ return `http://localhost:6006/?path=/story/${storyId}`
66
+ }
67
+
68
+ /**
69
+ * Test a single Storybook iframe URL for successful visualization rendering
70
+ * @param iframeUrl - The complete iframe URL to test (e.g., 'http://localhost:6006/iframe.html?id=...')
71
+ * @returns Promise that resolves with test results
72
+ */
73
+ const testIframeVisualization = async (iframeUrl: string) => {
74
+ iframeUrl = iframeUrl
75
+
76
+ const iframe = document.createElement('iframe')
77
+ iframe.style.width = '1200px'
78
+ iframe.style.height = '800px'
79
+ iframe.src = iframeUrl
80
+ document.body.appendChild(iframe)
81
+
82
+ try {
83
+ await performAndAssert(
84
+ 'Wait for iframe to load',
85
+ () => {
86
+ try {
87
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document
88
+ return {
89
+ loaded: !!iframeDoc && iframeDoc.readyState !== 'loading',
90
+ readyState: iframeDoc?.readyState || 'unknown'
91
+ }
92
+ } catch (error: any) {
93
+ return { loaded: false, readyState: 'error', error: error.message }
94
+ }
95
+ },
96
+ async () => {},
97
+ (before, after) => {
98
+ return after.loaded
99
+ }
100
+ )
101
+
102
+ await performAndAssert(
103
+ 'Wait for SVG elements to render in iframe',
104
+ () => {
105
+ try {
106
+ const iframeDoc = iframe?.contentDocument || iframe?.contentWindow?.document
107
+ if (!iframeDoc) return { svgCount: 0, hasCoveModule: false, error: 'No document access' }
108
+
109
+ const svgCount = iframeDoc.querySelectorAll('svg').length
110
+ const hasCoveModule = !!iframeDoc.querySelector('.cdc-open-viz-module')
111
+ const isDataBite = !!iframeDoc.querySelector('.bite-content')
112
+ const isDataTable = !!iframeDoc.querySelector('.data-table')
113
+
114
+ return { svgCount, hasCoveModule, isDataBite, isDataTable, error: null }
115
+ } catch (error: any) {
116
+ return { svgCount: 0, hasCoveModule: false, isDataBite: false, isDataTable: false, error: error.message }
117
+ }
118
+ },
119
+ async () => {},
120
+ (before, after) => {
121
+ return (after.svgCount > 0 && after.hasCoveModule) || after.isDataBite || after.isDataTable
122
+ }
123
+ )
124
+ } finally {
125
+ if (iframe.parentNode) {
126
+ document.body.removeChild(iframe)
127
+ }
128
+ }
129
+ }
130
+
131
+ export const StoryRenderingTests: Story = {
132
+ play: async ({ canvasElement }) => {
133
+ const canvas = within(canvasElement)
134
+ expect(canvas.getByTestId('chart-rendering-validator')).toBeInTheDocument()
135
+
136
+ const storyUrls = await getVisualizationStoryUrls()
137
+
138
+ if (storyUrls.length === 0) {
139
+ console.warn('No visualization stories found to test')
140
+ return
141
+ }
142
+
143
+ const results: { iframeUrl: string; storyUrl: string; success: boolean; error?: string }[] = []
144
+
145
+ for (const [i, iframeUrl] of storyUrls.entries()) {
146
+ const storyUrl = iframeUrlToStoryUrl(iframeUrl)
147
+
148
+ try {
149
+ await testIframeVisualization(iframeUrl)
150
+ results.push({ iframeUrl, storyUrl, success: true })
151
+ } catch (error: any) {
152
+ if (i > 0) {
153
+ results.push({ iframeUrl, storyUrl, success: false, error: error.message })
154
+ }
155
+ }
156
+ }
157
+
158
+ const failed = results.filter(r => !r.success).length
159
+
160
+ if (failed > 0) {
161
+ throw new Error(`${failed} out of ${storyUrls.length} visualization stories failed to render`)
162
+ }
163
+ }
164
+ }
@@ -59,7 +59,9 @@ export const AdvancedEditor = ({
59
59
  chart: ['Charts', 'https://www.cdc.gov/cove/index.html', <ChartIcon />],
60
60
  dashboard: ['Dashboard', 'https://www.cdc.gov/cove/index.html', <ChartIcon />],
61
61
  map: ['Maps', 'https://www.cdc.gov/cove/index.html', <MapIcon />],
62
- 'markup-include': ['Markup Include', 'https://www.cdc.gov/cove/index.html', <MarkupIncludeIcon />]
62
+ 'markup-include': ['Markup Include', 'https://www.cdc.gov/cove/index.html', <MarkupIncludeIcon />],
63
+ 'data-bite': ['Data Bite', 'https://www.cdc.gov/cove/index.html', <ChartIcon />],
64
+ 'waffle-chart': ['Waffle Chart', 'https://www.cdc.gov/cove/index.html', <ChartIcon />]
63
65
  }
64
66
 
65
67
  if (!config.type) return <></>
@@ -0,0 +1,299 @@
1
+ .custom-colors-editor {
2
+ padding: 0.75rem;
3
+ border: 1px solid #dee2e6;
4
+ border-radius: 0.25rem;
5
+ background-color: #f8f9fa;
6
+ }
7
+
8
+ .custom-colors-editor .custom-colors-label {
9
+ display: block;
10
+ font-weight: 600;
11
+ margin-bottom: 0.5rem;
12
+ font-size: 0.875rem;
13
+ color: #495057;
14
+ }
15
+
16
+ .custom-colors-editor .custom-colors-notice {
17
+ display: flex;
18
+ align-items: flex-start;
19
+ gap: 0.5rem;
20
+ padding: 0.75rem;
21
+ margin-bottom: 0.75rem;
22
+ background-color: #d1ecf1;
23
+ border: 1px solid #bee5eb;
24
+ border-radius: 0.25rem;
25
+ color: #0c5460;
26
+ font-size: 0.8125rem;
27
+ line-height: 1.5;
28
+ }
29
+
30
+ .custom-colors-editor .custom-colors-notice .notice-icon {
31
+ width: 16px;
32
+ height: 16px;
33
+ flex-shrink: 0;
34
+ margin-top: 0.125rem;
35
+ color: #17a2b8;
36
+ }
37
+
38
+ .custom-colors-editor .custom-colors-notice strong {
39
+ font-weight: 600;
40
+ }
41
+
42
+ .custom-colors-editor .custom-colors-notice a {
43
+ color: #0c5460;
44
+ text-decoration: underline;
45
+ font-weight: 500;
46
+ }
47
+
48
+ .custom-colors-editor .custom-colors-notice a:hover {
49
+ color: #062a30;
50
+ text-decoration: none;
51
+ }
52
+
53
+ .custom-colors-editor .custom-colors-preview {
54
+ display: flex;
55
+ gap: 2px;
56
+ margin-bottom: 0.75rem;
57
+ padding: 0.5rem;
58
+ background-color: #fff;
59
+ border: 1px solid #dee2e6;
60
+ border-radius: 0.25rem;
61
+ min-height: 36px;
62
+ overflow: hidden;
63
+ }
64
+
65
+ .custom-colors-editor .custom-colors-preview .preview-swatch {
66
+ flex: 1;
67
+ min-width: 8px;
68
+ border-radius: 2px;
69
+ transition: transform 0.15s ease;
70
+ }
71
+
72
+ .custom-colors-editor .custom-colors-preview .preview-swatch:hover {
73
+ transform: scale(1.1);
74
+ }
75
+
76
+ .custom-colors-editor .custom-colors-list {
77
+ display: flex;
78
+ flex-direction: column;
79
+ gap: 0.5rem;
80
+ margin-bottom: 0.75rem;
81
+ max-height: 400px;
82
+ overflow-y: auto;
83
+ padding-right: 0.25rem;
84
+ }
85
+
86
+ .custom-colors-editor .custom-colors-list::-webkit-scrollbar {
87
+ width: 6px;
88
+ }
89
+
90
+ .custom-colors-editor .custom-colors-list::-webkit-scrollbar-track {
91
+ background: #f1f1f1;
92
+ border-radius: 3px;
93
+ }
94
+
95
+ .custom-colors-editor .custom-colors-list::-webkit-scrollbar-thumb {
96
+ background: #c1c1c1;
97
+ border-radius: 3px;
98
+ }
99
+
100
+ .custom-colors-editor .custom-colors-list::-webkit-scrollbar-thumb:hover {
101
+ background: #a8a8a8;
102
+ }
103
+
104
+ .custom-colors-editor .custom-color-item {
105
+ background-color: #fff;
106
+ border: 1px solid #dee2e6;
107
+ border-radius: 0.25rem;
108
+ padding: 0.5rem;
109
+ transition: box-shadow 0.15s ease, opacity 0.15s ease;
110
+ }
111
+
112
+ .custom-colors-editor .custom-color-item.dragging {
113
+ opacity: 0.5;
114
+ box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
115
+ cursor: grabbing;
116
+ }
117
+
118
+ .custom-colors-editor .custom-color-item:hover:not(.dragging) {
119
+ box-shadow: 0 0.125rem 0.5rem rgba(0, 0, 0, 0.1);
120
+ border-color: #adb5bd;
121
+ }
122
+
123
+ .custom-colors-editor .color-item-controls {
124
+ display: flex;
125
+ align-items: center;
126
+ gap: 0.5rem;
127
+ flex-wrap: wrap;
128
+ }
129
+
130
+ @media (max-width: 576px) {
131
+ .custom-colors-editor .color-item-controls {
132
+ gap: 0.375rem;
133
+ }
134
+ }
135
+
136
+ .custom-colors-editor .color-item-drag-handle {
137
+ color: #adb5bd;
138
+ font-size: 0.875rem;
139
+ cursor: grab;
140
+ user-select: none;
141
+ line-height: 1;
142
+ flex-shrink: 0;
143
+ padding: 0 0.125rem;
144
+ }
145
+
146
+ .custom-colors-editor .color-item-drag-handle:active {
147
+ cursor: grabbing;
148
+ }
149
+
150
+ .custom-colors-editor .custom-color-item:hover .color-item-drag-handle {
151
+ color: #6c757d;
152
+ }
153
+
154
+ .custom-colors-editor .color-item-number {
155
+ font-weight: 600;
156
+ min-width: 24px;
157
+ text-align: right;
158
+ color: #6c757d;
159
+ font-size: 0.75rem;
160
+ flex-shrink: 0;
161
+ }
162
+
163
+ .custom-colors-editor .color-picker {
164
+ width: 48px;
165
+ height: 34px;
166
+ border: 1px solid #ced4da;
167
+ border-radius: 0.25rem;
168
+ cursor: pointer;
169
+ padding: 2px;
170
+ flex-shrink: 0;
171
+ transition: border-color 0.15s ease;
172
+ }
173
+
174
+ .custom-colors-editor .color-picker:hover {
175
+ border-color: #80bdff;
176
+ }
177
+
178
+ .custom-colors-editor .color-picker:focus {
179
+ outline: none;
180
+ border-color: #80bdff;
181
+ box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
182
+ }
183
+
184
+ .custom-colors-editor .color-input-wrapper {
185
+ flex: 1;
186
+ min-width: 0;
187
+ margin-bottom: 0;
188
+ }
189
+
190
+ /* Override TextField label wrapper styles */
191
+ .custom-colors-editor .color-input-wrapper label {
192
+ margin-bottom: 0;
193
+ }
194
+
195
+ /* Hide the label text since we're passing empty string */
196
+ .custom-colors-editor .color-input-wrapper label .edit-label {
197
+ display: none;
198
+ }
199
+
200
+ .custom-colors-editor .color-input-wrapper label input[type='text'] {
201
+ font-family: 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', monospace;
202
+ text-transform: uppercase;
203
+ }
204
+
205
+ @media (max-width: 576px) {
206
+ .custom-colors-editor .color-input-wrapper {
207
+ flex-basis: 100%;
208
+ }
209
+ }
210
+
211
+ .custom-colors-editor .color-item-buttons {
212
+ display: flex;
213
+ gap: 0.25rem;
214
+ margin-left: auto;
215
+ flex-shrink: 0;
216
+ }
217
+
218
+ .custom-colors-editor .btn-move,
219
+ .custom-colors-editor .btn-remove {
220
+ padding: 0.25rem 0.5rem;
221
+ border: 1px solid #ced4da;
222
+ border-radius: 0.25rem;
223
+ background-color: #fff;
224
+ cursor: pointer;
225
+ font-size: 0.875rem;
226
+ line-height: 1.5;
227
+ transition: all 0.15s ease;
228
+ display: inline-flex;
229
+ align-items: center;
230
+ justify-content: center;
231
+ min-width: 28px;
232
+ height: 28px;
233
+ }
234
+
235
+ .custom-colors-editor .btn-move:hover:not(:disabled),
236
+ .custom-colors-editor .btn-remove:hover:not(:disabled) {
237
+ background-color: #e9ecef;
238
+ border-color: #adb5bd;
239
+ }
240
+
241
+ .custom-colors-editor .btn-move:active:not(:disabled),
242
+ .custom-colors-editor .btn-remove:active:not(:disabled) {
243
+ background-color: #dee2e6;
244
+ transform: scale(0.95);
245
+ }
246
+
247
+ .custom-colors-editor .btn-move:disabled,
248
+ .custom-colors-editor .btn-remove:disabled {
249
+ opacity: 0.35;
250
+ cursor: not-allowed;
251
+ }
252
+
253
+ .custom-colors-editor .btn-remove {
254
+ color: #dc3545;
255
+ font-weight: 700;
256
+ font-size: 1.125rem;
257
+ }
258
+
259
+ .custom-colors-editor .btn-remove:hover:not(:disabled) {
260
+ background-color: #dc3545;
261
+ color: #fff;
262
+ border-color: #dc3545;
263
+ }
264
+
265
+ .custom-colors-editor .btn-add-color {
266
+ width: 100%;
267
+ padding: 0.5rem;
268
+ border: 2px dashed #ced4da;
269
+ border-radius: 0.25rem;
270
+ background-color: #fff;
271
+ color: #6c757d;
272
+ font-weight: 600;
273
+ font-size: 0.875rem;
274
+ cursor: pointer;
275
+ transition: all 0.15s ease;
276
+ }
277
+
278
+ .custom-colors-editor .btn-add-color:hover:not(:disabled) {
279
+ border-color: #007bff;
280
+ color: #007bff;
281
+ background-color: #e7f1ff;
282
+ }
283
+
284
+ .custom-colors-editor .btn-add-color:active:not(:disabled) {
285
+ transform: scale(0.98);
286
+ }
287
+
288
+ .custom-colors-editor .btn-add-color:disabled {
289
+ opacity: 0.5;
290
+ cursor: not-allowed;
291
+ }
292
+
293
+ .custom-colors-editor .custom-colors-info {
294
+ text-align: center;
295
+ font-size: 0.75rem;
296
+ color: #6c757d;
297
+ margin-top: 0.5rem;
298
+ font-style: italic;
299
+ }
@@ -0,0 +1,209 @@
1
+ import React, { useState } from 'react'
2
+ import { TextField } from '../EditorPanel/Inputs'
3
+ import './CustomColorsEditor.css'
4
+
5
+ interface CustomColorsEditorProps {
6
+ colors: string[]
7
+ onChange: (colors: string[]) => void
8
+ label?: string
9
+ minColors?: number
10
+ maxColors?: number
11
+ }
12
+
13
+ const CustomColorsEditor: React.FC<CustomColorsEditorProps> = ({
14
+ colors = [],
15
+ onChange,
16
+ label = 'Custom Colors',
17
+ minColors = 1,
18
+ maxColors = 20
19
+ }) => {
20
+ const [draggedIndex, setDraggedIndex] = useState<number | null>(null)
21
+
22
+ const handleColorChange = (index: number, newColor: string) => {
23
+ const newColors = [...colors]
24
+ newColors[index] = newColor
25
+ onChange(newColors)
26
+ }
27
+
28
+ const handleAddColor = () => {
29
+ if (colors.length < maxColors) {
30
+ // Add a new color (default to the last color or a neutral color)
31
+ const defaultColor = colors.length > 0 ? colors[colors.length - 1] : '#3366cc'
32
+ onChange([...colors, defaultColor])
33
+ }
34
+ }
35
+
36
+ const handleRemoveColor = (index: number) => {
37
+ if (colors.length > minColors) {
38
+ const newColors = colors.filter((_, i) => i !== index)
39
+ onChange(newColors)
40
+ }
41
+ }
42
+
43
+ const handleMoveUp = (index: number) => {
44
+ if (index > 0) {
45
+ const newColors = [...colors]
46
+ ;[newColors[index - 1], newColors[index]] = [newColors[index], newColors[index - 1]]
47
+ onChange(newColors)
48
+ }
49
+ }
50
+
51
+ const handleMoveDown = (index: number) => {
52
+ if (index < colors.length - 1) {
53
+ const newColors = [...colors]
54
+ ;[newColors[index], newColors[index + 1]] = [newColors[index + 1], newColors[index]]
55
+ onChange(newColors)
56
+ }
57
+ }
58
+
59
+ const handleDragStart = (index: number) => {
60
+ setDraggedIndex(index)
61
+ }
62
+
63
+ const handleDragOver = (e: React.DragEvent, index: number) => {
64
+ e.preventDefault()
65
+ }
66
+
67
+ const handleDrop = (e: React.DragEvent, dropIndex: number) => {
68
+ e.preventDefault()
69
+
70
+ if (draggedIndex === null || draggedIndex === dropIndex) {
71
+ setDraggedIndex(null)
72
+ return
73
+ }
74
+
75
+ const newColors = [...colors]
76
+ const draggedColor = newColors[draggedIndex]
77
+ newColors.splice(draggedIndex, 1)
78
+ newColors.splice(dropIndex, 0, draggedColor)
79
+
80
+ onChange(newColors)
81
+ setDraggedIndex(null)
82
+ }
83
+
84
+ return (
85
+ <div className="custom-colors-editor">
86
+ <label className="custom-colors-label">{label}</label>
87
+
88
+ <div className="custom-colors-notice">
89
+ <svg className="notice-icon" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
90
+ <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
91
+ <path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
92
+ </svg>
93
+ <span>
94
+ <strong>Accessibility Notice:</strong> When using custom colors, conduct a{' '}
95
+ Section 508 review to ensure adequate color contrast and accessibility compliance.{' '}
96
+ </span>
97
+ </div>
98
+
99
+ <div className="custom-colors-preview">
100
+ {colors.map((color, index) => (
101
+ <div
102
+ key={index}
103
+ className="preview-swatch"
104
+ style={{ backgroundColor: color }}
105
+ title={`Color ${index + 1}: ${color}`}
106
+ />
107
+ ))}
108
+ </div>
109
+
110
+ <div className="custom-colors-list">
111
+ {colors.map((color, index) => (
112
+ <div
113
+ key={index}
114
+ className={`custom-color-item ${draggedIndex === index ? 'dragging' : ''}`}
115
+ draggable
116
+ onDragStart={() => handleDragStart(index)}
117
+ onDragOver={(e) => handleDragOver(e, index)}
118
+ onDrop={(e) => handleDrop(e, index)}
119
+ >
120
+ <div className="color-item-controls">
121
+ <span className="color-item-drag-handle" title="Drag to reorder">
122
+ ⋮⋮
123
+ </span>
124
+ {/* <span className="color-item-number">{index + 1}.</span> */}
125
+
126
+ {/* <input
127
+ type="color"
128
+ value={color}
129
+ onChange={(e) => handleColorChange(index, e.target.value)}
130
+ className="color-picker"
131
+ title="Click to change color"
132
+ aria-label={`Color picker for color ${index + 1}`}
133
+ /> */}
134
+
135
+ <div className="color-input-wrapper">
136
+ <TextField
137
+ type="text"
138
+ value={color}
139
+ label=""
140
+ section="colors"
141
+ subsection={null}
142
+ fieldName={`color-${index}`}
143
+ updateField={(section, subsection, fieldName, value) => {
144
+ handleColorChange(index, value)
145
+ }}
146
+ placeholder="#000000"
147
+ maxLength={15}
148
+ className="color-text-input"
149
+ />
150
+ </div>
151
+
152
+ <div className="color-item-buttons">
153
+ <button
154
+ type="button"
155
+ onClick={() => handleMoveUp(index)}
156
+ disabled={index === 0}
157
+ className="btn-move"
158
+ title="Move up"
159
+ aria-label="Move color up"
160
+ >
161
+
162
+ </button>
163
+
164
+ <button
165
+ type="button"
166
+ onClick={() => handleMoveDown(index)}
167
+ disabled={index === colors.length - 1}
168
+ className="btn-move"
169
+ title="Move down"
170
+ aria-label="Move color down"
171
+ >
172
+
173
+ </button>
174
+
175
+ <button
176
+ type="button"
177
+ onClick={() => handleRemoveColor(index)}
178
+ disabled={colors.length <= minColors}
179
+ className="btn-remove"
180
+ title="Remove color"
181
+ aria-label="Remove color"
182
+ >
183
+ ×
184
+ </button>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ ))}
189
+ </div>
190
+
191
+ <button
192
+ type="button"
193
+ onClick={handleAddColor}
194
+ disabled={colors.length >= maxColors}
195
+ className="btn-add-color"
196
+ >
197
+ + Add Color
198
+ </button>
199
+
200
+ <div className="custom-colors-info">
201
+ {colors.length} color{colors.length !== 1 ? 's' : ''}
202
+ {colors.length < minColors && ` (minimum ${minColors} required)`}
203
+ {colors.length >= maxColors && ` (maximum reached)`}
204
+ </div>
205
+ </div>
206
+ )
207
+ }
208
+
209
+ export default CustomColorsEditor
@@ -0,0 +1 @@
1
+ export { default as CustomColorsEditor } from './CustomColorsEditor'
@@ -1,7 +1,6 @@
1
1
  import { useEffect, useState } from 'react'
2
2
  import { ViewPort } from '../../types/ViewPort'
3
- import Footnotes from '../../types/Footnotes'
4
- import EditorWrapper from '../EditorWrapper/EditorWrapper'
3
+ import EditorWrapper from '../EditorWrapper'
5
4
  import DataTable from './DataTable'
6
5
  import DataTableEditorPanel from './components/DataTableEditorPanel'
7
6
  import Filters from '../Filters'
@@ -73,7 +72,13 @@ const DataTableStandAlone: React.FC<StandAloneProps> = ({
73
72
  viewport={viewport || 'lg'}
74
73
  interactionLabel={interactionLabel}
75
74
  />
76
- <FootnotesStandAlone config={config.footnotes} filters={config.filters?.filter(f => f.filterFootnotes)} />
75
+ <FootnotesStandAlone
76
+ config={config.footnotes}
77
+ filters={config.filters?.filter(f => f.filterFootnotes)}
78
+ markupVariables={config['markupVariables']}
79
+ enableMarkupVariables={config['enableMarkupVariables']}
80
+ data={config.data}
81
+ />
77
82
  </>
78
83
  )
79
84
  }