@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.
- package/_stories/StoryRenderingTests.stories.tsx +164 -0
- package/components/AdvancedEditor/AdvancedEditor.tsx +3 -1
- package/components/CustomColorsEditor/CustomColorsEditor.css +299 -0
- package/components/CustomColorsEditor/CustomColorsEditor.tsx +209 -0
- package/components/CustomColorsEditor/index.ts +1 -0
- package/components/DataTable/DataTableStandAlone.tsx +8 -3
- package/components/DataTable/components/DataTableEditorPanel.tsx +12 -2
- package/components/DataTable/data-table.css +6 -0
- package/components/DataTable/helpers/mapCellMatrix.tsx +14 -3
- package/components/DataTable/helpers/standardizeState.js +2 -2
- package/components/DataTable/helpers/tests/standardizeState.test.js +54 -0
- package/components/EditorPanel/DataTableEditor.tsx +3 -3
- package/components/EditorPanel/EditorPanel.styles.css +423 -0
- package/components/EditorPanel/FootnotesEditor.tsx +44 -37
- package/components/EditorPanel/Inputs.tsx +12 -2
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +35 -62
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +12 -2
- package/components/EditorPanel/components/MarkupVariablesEditor.tsx +61 -22
- package/components/Filters/Filters.tsx +26 -5
- package/components/Filters/components/Dropdown.tsx +6 -1
- package/components/Footnotes/Footnotes.tsx +35 -25
- package/components/Footnotes/FootnotesStandAlone.tsx +42 -6
- package/components/HeaderThemeSelector/HeaderThemeSelector.css +43 -0
- package/components/HeaderThemeSelector/HeaderThemeSelector.stories.tsx +74 -0
- package/components/HeaderThemeSelector/HeaderThemeSelector.tsx +61 -0
- package/components/HeaderThemeSelector/index.ts +2 -0
- package/components/Layout/styles/editor.scss +2 -1
- package/components/Loader/Loader.tsx +1 -1
- package/components/MediaControls.tsx +21 -18
- package/components/PaletteConversionModal.tsx +7 -4
- package/components/PaletteSelector/PaletteSelector.css +49 -6
- package/components/Table/components/Cell.tsx +23 -2
- package/components/Table/components/Row.tsx +5 -3
- package/components/_stories/Filters.stories.tsx +20 -1
- package/components/_stories/Footnotes.CSV.stories.tsx +247 -0
- package/components/_stories/Footnotes.stories.tsx +768 -3
- package/components/_stories/Inputs.stories.tsx +2 -2
- package/components/_stories/styles.scss +0 -1
- package/components/ui/Accordion.jsx +1 -1
- package/components/ui/accordion.styles.css +57 -0
- package/data/chartColorPalettes.ts +1 -1
- package/dist/cove-main.css +49 -3
- package/dist/cove-main.css.map +1 -1
- package/helpers/addValuesToFilters.ts +5 -0
- package/helpers/constants.ts +37 -0
- package/helpers/cove/number.ts +33 -12
- package/helpers/coveUpdateWorker.ts +3 -1
- package/helpers/fetchRemoteData.ts +3 -15
- package/helpers/markupProcessor.ts +27 -12
- package/helpers/mergeCustomOrderValues.ts +37 -0
- package/helpers/parseCsvWithQuotes.ts +65 -0
- package/helpers/testing.ts +17 -4
- package/helpers/ver/4.25.11.ts +13 -0
- package/helpers/viewports.ts +2 -0
- package/package.json +4 -3
- package/styles/_common-components.css +73 -0
- package/styles/_global.scss +25 -5
- package/styles/base.scss +0 -50
- package/styles/cove-main.scss +3 -1
- package/styles/filters.scss +10 -3
- package/styles/v2/base/index.scss +0 -1
- package/styles/v2/components/editor.scss +14 -6
- package/styles/v2/utils/_breakpoints.scss +1 -1
- package/styles/v2/utils/index.scss +0 -1
- package/styles/waiting.scss +1 -1
- package/types/MarkupInclude.ts +4 -3
- package/types/MarkupVariable.ts +1 -1
- package/types/VizFilter.ts +1 -0
- package/styles/_mixins.scss +0 -13
- package/styles/_typography.scss +0 -0
- package/styles/v2/base/_typography.scss +0 -0
- package/styles/v2/components/guidance-block.scss +0 -74
- 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
|
|
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
|
|
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
|
}
|