@cdc/core 4.26.1 → 4.26.3

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 (249) hide show
  1. package/.claude/agents/qa-test-developer.md +126 -0
  2. package/CLAUDE.local.md +67 -0
  3. package/LICENSE +201 -0
  4. package/_stories/Gallery.Charts.stories.tsx +35 -42
  5. package/_stories/Gallery.DataBite.stories.tsx +15 -8
  6. package/_stories/Gallery.Maps.stories.tsx +37 -28
  7. package/_stories/Gallery.WaffleChart.stories.tsx +1 -1
  8. package/_stories/PageART.stories.tsx +5 -4
  9. package/_stories/PageBRFSS.stories.tsx +21 -16
  10. package/_stories/PageCancerRegistries.stories.tsx +15 -15
  11. package/_stories/PageEasternEquineEncephalitis.stories.tsx +33 -19
  12. package/_stories/PageExcessiveAlcoholUse.stories.tsx +148 -143
  13. package/_stories/PageMaternalMortality.stories.tsx +5 -4
  14. package/_stories/PageOralHealth.stories.tsx +15 -10
  15. package/_stories/PageRespiratory.stories.tsx +4 -4
  16. package/_stories/PageSmokingTobacco.stories.tsx +15 -10
  17. package/_stories/PageStateDiabetesProfiles.stories.tsx +15 -10
  18. package/_stories/PageWastewater.stories.tsx +44 -30
  19. package/_stories/VegaImport.stories.tsx +401 -0
  20. package/_stories/vega-fixtures/bars-with-line.json +444 -0
  21. package/_stories/vega-fixtures/bars.json +58 -0
  22. package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
  23. package/_stories/vega-fixtures/combo.json +68 -0
  24. package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
  25. package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
  26. package/_stories/vega-fixtures/horizontal-bar.json +427 -0
  27. package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
  28. package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
  29. package/_stories/vega-fixtures/lines.json +227 -0
  30. package/_stories/vega-fixtures/measles-bars.json +348 -0
  31. package/_stories/vega-fixtures/measles-map.json +11101 -0
  32. package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
  33. package/_stories/vega-fixtures/multi-dataset.json +255 -0
  34. package/_stories/vega-fixtures/no-data.json +14 -0
  35. package/_stories/vega-fixtures/pie-chart.json +94 -0
  36. package/_stories/vega-fixtures/repeat-spec.json +47 -0
  37. package/_stories/vega-fixtures/stacked-area.json +222 -0
  38. package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
  39. package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
  40. package/_stories/vega-fixtures/stacked-bars.json +212 -0
  41. package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
  42. package/_stories/vega-fixtures/warning-combo.json +59 -0
  43. package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
  44. package/assets/callout-flag.svg +7 -0
  45. package/assets/icon-chart-area.svg +1 -0
  46. package/assets/icon-chart-radar.svg +23 -0
  47. package/assets/logo2.svg +31 -0
  48. package/components/AdvancedEditor/EmbedEditor.tsx +270 -38
  49. package/components/Alert/components/Alert.styles.css +2 -2
  50. package/components/ComboBox/combobox.styles.css +48 -48
  51. package/components/CustomColorsEditor/CustomColorsEditor.css +53 -53
  52. package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
  53. package/components/DataTable/DataTable.tsx +46 -18
  54. package/components/DataTable/DataTableStandAlone.tsx +1 -0
  55. package/components/DataTable/components/ChartHeader.tsx +21 -12
  56. package/components/DataTable/components/MapHeader.tsx +34 -28
  57. package/components/DataTable/components/SortIcon/sort-icon.css +5 -5
  58. package/components/DataTable/data-table.css +50 -52
  59. package/components/DataTable/helpers/applyCustomOrder.ts +17 -0
  60. package/components/DataTable/helpers/getChartCellValue.ts +10 -7
  61. package/components/DataTable/helpers/getMapDataTableColumnKeys.ts +22 -0
  62. package/components/DataTable/helpers/getSeriesName.ts +6 -0
  63. package/components/DataTable/helpers/mapCellMatrix.tsx +33 -23
  64. package/components/DataTable/helpers/tests/mapCellMatrix.test.ts +33 -0
  65. package/components/DownloadButton.tsx +14 -6
  66. package/components/EditorPanel/ColumnsEditor.tsx +38 -31
  67. package/components/EditorPanel/CustomSortOrder.tsx +94 -0
  68. package/components/EditorPanel/DataTableEditor.tsx +139 -23
  69. package/components/EditorPanel/EditorPanel.styles.css +71 -71
  70. package/components/EditorPanel/EditorPanel.tsx +3 -8
  71. package/components/EditorPanel/EditorPanelDispatch.tsx +4 -4
  72. package/components/EditorPanel/FootnotesEditor.tsx +2 -2
  73. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +21 -12
  74. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +16 -10
  75. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
  76. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +160 -106
  77. package/components/EditorPanel/components/PanelMarkup.tsx +5 -1
  78. package/{styles/v2/components → components/EditorPanel}/editor.scss +76 -22
  79. package/components/EditorPanel/sections/StyleTreatmentSection.tsx +99 -0
  80. package/components/EditorPanel/sections/VisualSection.tsx +11 -0
  81. package/components/EditorWrapper/editor-wrapper.style.css +1 -1
  82. package/components/Filters/Filters.tsx +3 -5
  83. package/components/Filters/components/Tabs.tsx +19 -7
  84. package/{styles → components/Filters}/filters.scss +3 -3
  85. package/components/Footnotes/FootnotesStandAlone.tsx +4 -2
  86. package/components/HeaderThemeSelector/HeaderThemeSelector.css +61 -5
  87. package/components/Layout/components/Responsive.tsx +14 -6
  88. package/components/Layout/components/Sidebar/components/Sidebar.tsx +1 -1
  89. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +14 -20
  90. package/components/Layout/components/Visualization/index.tsx +50 -38
  91. package/components/Layout/components/Visualization/visualizations.scss +232 -15
  92. package/components/Layout/components/VisualizationContainer.test.tsx +67 -0
  93. package/components/Layout/components/VisualizationContainer.tsx +37 -0
  94. package/components/Layout/components/VisualizationContent.test.tsx +182 -0
  95. package/components/Layout/components/VisualizationContent.tsx +75 -0
  96. package/components/Layout/index.tsx +5 -5
  97. package/components/Layout/styles/editor-utils.scss +3 -3
  98. package/components/Layout/styles/editor.scss +4 -4
  99. package/components/Legend/Legend.Gradient.tsx +7 -1
  100. package/components/Loader/loader.styles.css +2 -2
  101. package/components/Loading.jsx +1 -1
  102. package/components/MediaControls.tsx +10 -3
  103. package/components/MultiSelect/multiselect.styles.css +19 -19
  104. package/components/NestedDropdown/nesteddropdown.styles.css +15 -15
  105. package/components/PaletteSelector/PaletteSelector.css +15 -15
  106. package/components/RichTooltip/richTooltip.css +6 -6
  107. package/components/Table/table.styles.css +2 -2
  108. package/components/Waiting.tsx +1 -1
  109. package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
  110. package/components/_stories/DataTable.stories.tsx +1 -0
  111. package/components/_stories/Filters.stories.tsx +1 -1
  112. package/components/_stories/styles.scss +0 -1
  113. package/components/elements/Button.jsx +1 -1
  114. package/components/elements/Card.jsx +1 -1
  115. package/{styles/v2/components → components/elements}/button.scss +9 -8
  116. package/components/inputs/InputCheckbox.jsx +1 -1
  117. package/components/inputs/InputSelect.tsx +1 -1
  118. package/components/inputs/InputText.jsx +1 -1
  119. package/components/inputs/InputToggle.tsx +1 -1
  120. package/{styles/v2/components/input → components/inputs}/_input-check-radio.scss +2 -2
  121. package/{styles/v2/components/input → components/inputs}/_input-group.scss +3 -3
  122. package/{styles/v2/components/input → components/inputs}/_input-slider.scss +2 -2
  123. package/{styles/v2/components/input → components/inputs}/_input.scss +5 -5
  124. package/{styles/v2/components/input → components/inputs}/index.scss +2 -2
  125. package/{styles → components}/loading.scss +1 -1
  126. package/components/managers/DataDesigner.tsx +1 -1
  127. package/{styles/v2/components → components/managers}/data-designer.scss +6 -7
  128. package/components/ui/Accordion.jsx +1 -1
  129. package/components/ui/Icon.tsx +1 -1
  130. package/components/ui/LoadSpin.jsx +1 -1
  131. package/components/ui/Modal.jsx +1 -1
  132. package/components/ui/Overlay.jsx +1 -1
  133. package/components/ui/Title/index.test.tsx +34 -0
  134. package/components/ui/Title/index.tsx +24 -7
  135. package/components/ui/Title/title.styles.css +119 -25
  136. package/components/ui/Tooltip.tsx +1 -1
  137. package/components/ui/_stories/Title.stories.tsx +1 -1
  138. package/{styles/v2/components → components/ui}/accordion.scss +3 -3
  139. package/components/ui/accordion.styles.css +11 -11
  140. package/{styles/v2/components → components/ui}/modal.scss +2 -2
  141. package/{styles/v2/components → components/ui}/overlay.scss +6 -6
  142. package/{styles/v2/components → components}/ui/tooltip.scss +1 -1
  143. package/{styles → components}/waiting.scss +9 -3
  144. package/data/colorPalettes.ts +18 -5
  145. package/data/mapColorPalettes.ts +10 -0
  146. package/devTemplate/dev.js +285 -0
  147. package/devTemplate/index.html +30 -0
  148. package/devTemplate/preview.html +1503 -0
  149. package/devTemplate/sidebar.css +151 -0
  150. package/dist/cove-main.css +2530 -3901
  151. package/dist/cove-main.css.map +1 -1
  152. package/generateViteConfig.js +111 -2
  153. package/helpers/DataTransform.ts +1 -5
  154. package/helpers/backfillDefaults.ts +35 -0
  155. package/helpers/constants.ts +12 -0
  156. package/helpers/cove/date.ts +64 -3
  157. package/helpers/cove/number.ts +29 -15
  158. package/helpers/cove/string.ts +29 -0
  159. package/helpers/coveUpdateWorker.ts +14 -8
  160. package/helpers/displayDataAsText.ts +1 -1
  161. package/helpers/embed/embedCodeGenerator.ts +80 -0
  162. package/helpers/embed/embedHelper.js +169 -0
  163. package/helpers/embed/filterUtils.ts +121 -0
  164. package/helpers/embed/index.ts +17 -0
  165. package/helpers/embed/urlValidation.ts +119 -0
  166. package/helpers/extractDataAndMetadata.ts +20 -0
  167. package/helpers/fetchRemoteData.ts +14 -8
  168. package/helpers/filterVizData.ts +6 -1
  169. package/helpers/getFileExtension.ts +0 -6
  170. package/helpers/labelHash.ts +9 -0
  171. package/helpers/markupProcessor.ts +56 -38
  172. package/helpers/metrics/types.ts +3 -0
  173. package/helpers/palettes/colorDistributions.ts +1 -1
  174. package/helpers/palettes/utils.ts +12 -12
  175. package/helpers/parseCsvWithQuotes.ts +15 -14
  176. package/helpers/prepareScreenshot.ts +33 -10
  177. package/helpers/testing.ts +44 -0
  178. package/helpers/tests/DataTransform.test.ts +125 -0
  179. package/helpers/tests/abbreviateNumber.test.ts +59 -0
  180. package/helpers/tests/backfillDefaults.test.ts +253 -0
  181. package/helpers/tests/date.test.ts +110 -0
  182. package/helpers/tests/extractDataAndMetadata.test.ts +93 -0
  183. package/helpers/tests/markupProcessor.test.ts +315 -124
  184. package/helpers/tests/number.test.ts +42 -0
  185. package/helpers/tests/prepareScreenshot.test.ts +28 -28
  186. package/helpers/tests/testStandaloneBuild.ts +36 -26
  187. package/helpers/tests/useDataVizClasses.test.ts +66 -0
  188. package/helpers/tests/visualizationWrapperUsage.test.ts +57 -0
  189. package/helpers/useDataVizClasses.ts +13 -7
  190. package/helpers/vegaConfig.ts +1 -1
  191. package/helpers/vegaConfigImport.ts +160 -0
  192. package/helpers/ver/4.24.4.ts +24 -0
  193. package/helpers/ver/4.26.1.ts +1 -1
  194. package/helpers/ver/4.26.2.ts +84 -0
  195. package/helpers/ver/4.26.3.ts +44 -0
  196. package/helpers/ver/4.26.4.ts +31 -0
  197. package/helpers/ver/tests/4.26.1.test.ts +105 -0
  198. package/helpers/ver/tests/4.26.2.test.ts +298 -0
  199. package/helpers/ver/tests/4.26.3.test.ts +168 -0
  200. package/helpers/ver/tests/4.26.4.test.ts +88 -0
  201. package/helpers/ver/tests/coveUpdateWorker.test.ts +57 -0
  202. package/helpers/viewports.ts +2 -0
  203. package/package.json +27 -32
  204. package/styles/_global.scss +7 -7
  205. package/styles/_reset.scss +2 -2
  206. package/styles/{v2/base → base}/_file-selector.scss +4 -4
  207. package/styles/{v2/base → base}/_general.scss +2 -4
  208. package/styles/{v2/base → base}/index.scss +1 -1
  209. package/styles/base.scss +107 -165
  210. package/styles/cove-main.scss +3 -6
  211. package/styles/layout/_component.scss +110 -0
  212. package/styles/{v2/layout → layout}/_data-table.scss +7 -7
  213. package/styles/layout/_wrapper-padding.scss +27 -0
  214. package/styles/{v2/main.scss → main.scss} +3 -1
  215. package/styles/{v2/themes → themes}/_color-definitions.scss +46 -41
  216. package/styles/{_accessibility.scss → utils/_accessibility.scss} +1 -1
  217. package/styles/{v2/utils → utils}/_grid.scss +8 -3
  218. package/styles/{_global-variables.scss → utils/_properties.scss} +133 -112
  219. package/styles/{v2/utils → utils}/index.scss +2 -1
  220. package/types/Annotation.ts +10 -11
  221. package/types/Axis.ts +2 -0
  222. package/types/ComponentStyles.ts +1 -0
  223. package/types/ConfigureData.ts +1 -0
  224. package/types/General.ts +2 -0
  225. package/types/MarkupInclude.ts +1 -0
  226. package/types/MarkupVariable.ts +2 -1
  227. package/types/Palette.ts +22 -0
  228. package/types/Table.ts +9 -0
  229. package/types/Visualization.ts +7 -0
  230. package/_stories/StoryRenderingTests.stories.tsx +0 -164
  231. package/helpers/embedCodeGenerator.ts +0 -109
  232. package/styles/_common-components.css +0 -73
  233. package/styles/_variables.scss +0 -63
  234. package/styles/v2/layout/_component.scss +0 -21
  235. package/styles/v2/utils/_variables.scss +0 -9
  236. package/{styles/v2/components/card.scss → components/elements/card.css} +2 -2
  237. /package/{styles/v2/components → components/ui}/icon.scss +0 -0
  238. /package/{styles/v2/components → components/ui}/loadspin.scss +0 -0
  239. /package/styles/{v2/base → base}/_heading.scss +0 -0
  240. /package/styles/{v2/base → base}/_reset.scss +0 -0
  241. /package/styles/{v2/layout → layout}/_alert.scss +0 -0
  242. /package/styles/{v2/layout → layout}/_progression.scss +0 -0
  243. /package/styles/{v2/layout → layout}/_tooltip.scss +0 -0
  244. /package/styles/{v2/layout → layout}/index.scss +0 -0
  245. /package/styles/{v2/themes → themes}/index.scss +0 -0
  246. /package/styles/{v2/utils → utils}/_align.scss +0 -0
  247. /package/styles/{v2/utils → utils}/_animations.scss +0 -0
  248. /package/styles/{v2/utils → utils}/_breakpoints.scss +0 -0
  249. /package/styles/{v2/utils → utils}/_mixins.scss +0 -0
@@ -1,46 +1,263 @@
1
- .cdc-open-viz-module {
2
- .cdc-chart-inner-container .cove-component__content {
3
- padding: 0 0 27px 0 !important;
4
- }
5
- &.isEditor {
1
+ .cove-visualization {
2
+ &.is-editor {
6
3
  overflow: auto;
7
4
  display: grid;
5
+ position: relative;
8
6
  transition: grid-template-columns 400ms ease-in-out;
9
7
  min-height: 100vh;
10
8
 
11
9
  .editor-panel__toggle {
12
- transition: left 400ms ease-in-out;
10
+ transition: left 400ms ease-in-out, background 0.1s;
13
11
  }
14
12
 
15
13
  .sidebar {
16
- transition: left 400ms ease-in-out;
14
+ overflow: hidden;
15
+ }
16
+
17
+ // Override dashboard's .editor-panel absolute positioning that conflicts with the grid layout.
18
+ // .cove-visualization.type-dashboard scopes .editor-panel { position: absolute } (specificity 0,3,0),
19
+ // so we need 4 classes here (specificity 0,4,0) to win.
20
+ .editor-panel.sidebar {
21
+ position: static;
22
+ z-index: auto;
23
+ overflow: hidden;
24
+ top: auto;
25
+ bottom: auto;
26
+ width: auto;
17
27
  }
18
28
 
19
29
  &.editor-panel--visible {
20
30
  grid-template-areas: 'panel content';
21
31
  grid-template-columns: 350px calc(100% - 350px);
22
- overflow: hidden;
32
+
33
+ .cove-visualization__outer {
34
+ overflow: hidden;
35
+ }
23
36
  }
24
37
 
25
38
  &.editor-panel--hidden {
26
39
  grid-template-areas: 'panel content';
27
40
  grid-template-columns: 0px 100%;
41
+ overflow: hidden;
28
42
  }
29
43
 
30
44
  .cove-editor__content,
31
- .cove-component__content {
45
+ .cove-visualization__outer,
46
+ .cove-visualization__inner {
32
47
  grid-area: content;
33
48
  position: relative;
34
49
  left: 0;
35
50
  width: 100% !important;
36
- grid-area: content;
51
+ }
52
+
53
+ .cove-editor__content {
37
54
  padding: 1rem;
55
+ }
38
56
 
39
- // Prevent double padding on nested .cove-component__content divs
40
- // (e.g., in markup-include, waffle-chart, filtered-text)
41
- .cove-component__content {
42
- padding: 0;
43
- }
57
+ .cove-visualization__outer {
58
+ padding: 1rem;
44
59
  }
45
60
  }
46
61
  }
62
+
63
+ // Developer highlight tool — only active when .is-editor + config.visual.highlightWrappers
64
+ // Enabled via ?isCoveDeveloper=true URL param + "Highlight Layout Wrappers" checkbox
65
+ .cove-visualization.is-editor.cove-highlight-wrappers {
66
+ outline: 2px dashed #0050d8;
67
+ position: relative;
68
+
69
+ &::before {
70
+ background: #0050d8;
71
+ color: #fff;
72
+ content: 'cove-visualization';
73
+ font-family: monospace;
74
+ font-size: 10px;
75
+ line-height: 1;
76
+ padding: 2px 4px;
77
+ pointer-events: none;
78
+ position: absolute;
79
+ top: 0;
80
+ left: 0;
81
+ z-index: 9999;
82
+ }
83
+
84
+ .cove-visualization__outer {
85
+ outline: 2px dashed #00a91c;
86
+ position: relative;
87
+
88
+ &::before {
89
+ background: #00a91c;
90
+ color: #fff;
91
+ content: '__outer';
92
+ font-family: monospace;
93
+ font-size: 10px;
94
+ line-height: 1;
95
+ padding: 2px 4px;
96
+ pointer-events: none;
97
+ position: absolute;
98
+ top: 0;
99
+ left: 0;
100
+ z-index: 9999;
101
+ }
102
+ }
103
+
104
+ .cove-visualization__inner {
105
+ outline: 2px dashed #e52207;
106
+ position: relative;
107
+
108
+ &::before {
109
+ background: #e52207;
110
+ color: #fff;
111
+ content: '__inner';
112
+ font-family: monospace;
113
+ font-size: 10px;
114
+ line-height: 1;
115
+ padding: 2px 4px;
116
+ pointer-events: none;
117
+ position: absolute;
118
+ top: 0;
119
+ left: 0;
120
+ z-index: 9999;
121
+ }
122
+ }
123
+
124
+ .cove-visualization__title,
125
+ .cove-visualization__header {
126
+ outline: 2px dashed #8a2be2;
127
+ position: relative;
128
+
129
+ &::before {
130
+ background: #8a2be2;
131
+ color: #fff;
132
+ content: '__title';
133
+ font-family: monospace;
134
+ font-size: 10px;
135
+ line-height: 1;
136
+ padding: 2px 4px;
137
+ pointer-events: none;
138
+ position: absolute;
139
+ top: 0;
140
+ left: 0;
141
+ z-index: 9999;
142
+ }
143
+ }
144
+
145
+ .cove-visualization__body {
146
+ outline: 2px dashed #e52207;
147
+ position: relative;
148
+
149
+ &::before {
150
+ background: #e52207;
151
+ color: #fff;
152
+ content: '__body';
153
+ font-family: monospace;
154
+ font-size: 10px;
155
+ line-height: 1;
156
+ padding: 2px 4px;
157
+ pointer-events: none;
158
+ position: absolute;
159
+ top: 0;
160
+ left: 0;
161
+ z-index: 9999;
162
+ }
163
+ }
164
+
165
+ .cove-visualization__message {
166
+ outline: 2px dashed #0050d8;
167
+ position: relative;
168
+
169
+ &::before {
170
+ background: #0050d8;
171
+ color: #fff;
172
+ content: '__message';
173
+ font-family: monospace;
174
+ font-size: 10px;
175
+ line-height: 1;
176
+ padding: 2px 4px;
177
+ pointer-events: none;
178
+ position: absolute;
179
+ top: 0;
180
+ left: 0;
181
+ z-index: 9999;
182
+ }
183
+ }
184
+
185
+ .cove-visualization__body-wrap {
186
+ outline: 2px dashed #e66f0e;
187
+ position: relative;
188
+
189
+ &::before {
190
+ background: #e66f0e;
191
+ color: #fff;
192
+ content: '__body-wrap';
193
+ font-family: monospace;
194
+ font-size: 10px;
195
+ line-height: 1;
196
+ padding: 2px 4px;
197
+ pointer-events: none;
198
+ position: absolute;
199
+ top: 0;
200
+ left: 0;
201
+ z-index: 9999;
202
+ }
203
+ }
204
+
205
+ }
206
+
207
+ .cove-visualization {
208
+ font-size: var(--app-font-size);
209
+
210
+ .cove-visualization__body,
211
+ .cove-visualization__body-wrap,
212
+ .cove-visualization__message-section,
213
+ .cove-visualization__content-section,
214
+ .cove-visualization__body-subtext-section,
215
+ .cove-visualization__body-footer-section,
216
+ .cove-visualization__subtext-section {
217
+ color: var(--baseColor);
218
+ font-family: var(--visualization-body-font-family, var(--app-font-main));
219
+ line-height: var(--visualization-body-line-height, 1.5);
220
+ }
221
+
222
+ .cove-visualization__content-section,
223
+ .cove-visualization__message-section,
224
+ .cove-visualization__body-subtext-section,
225
+ .cove-visualization__body-footer-section,
226
+ .cove-visualization__subtext-section {
227
+ width: 100%;
228
+ }
229
+
230
+ .cove-visualization__title~.cove-visualization__body .cove-visualization__body-wrap,
231
+ .cove-visualization__body.component--has-legacy-border .cove-visualization__body-wrap,
232
+ .cove-visualization__body.component--has-border-color-theme .cove-visualization__body-wrap,
233
+ .cove-visualization__body.component--has-background .cove-visualization__body-wrap {
234
+ padding-top: var(--cove-visualization-section-gap, 1.5rem);
235
+ padding-bottom: var(--cove-visualization-section-gap, 1.5rem);
236
+ }
237
+
238
+ .cove-visualization__body.component--has-legacy-border .cove-visualization__content-section,
239
+ .cove-visualization__body.component--has-border-color-theme .cove-visualization__content-section,
240
+ .cove-visualization__body.component--has-background .cove-visualization__content-section {
241
+ padding-inline: 0;
242
+ }
243
+
244
+ &.type-data-bite .cove-visualization__body-wrap {
245
+ padding-top: var(--cove-visualization-section-gap, 1.5rem);
246
+ padding-bottom: var(--cove-visualization-section-gap, 1.5rem);
247
+ }
248
+
249
+ .cove-visualization__body-wrap {
250
+ display: flex;
251
+ flex-direction: column;
252
+ row-gap: var(--cove-visualization-section-gap, 1.5rem);
253
+
254
+ // Prevent content margins from stacking with row-gap
255
+ >*> :first-child {
256
+ margin-top: 0;
257
+ }
258
+
259
+ >*> :last-child {
260
+ margin-bottom: 0;
261
+ }
262
+ }
263
+ }
@@ -0,0 +1,67 @@
1
+ import React from 'react'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { describe, expect, it } from 'vitest'
4
+ import VisualizationContainer from './VisualizationContainer'
5
+
6
+ describe('VisualizationContainer', () => {
7
+ it('renders the shared outer shell contract for dashboard wrappers', () => {
8
+ const { container } = render(
9
+ <VisualizationContainer
10
+ config={{ type: 'dashboard', theme: 'theme-blue' }}
11
+ currentViewport='md'
12
+ imageId='dashboard-image'
13
+ isEditor={false}
14
+ renderResponsive={false}
15
+ >
16
+ <div>Dashboard body</div>
17
+ </VisualizationContainer>
18
+ )
19
+
20
+ const shell = container.querySelector('.cove-visualization')
21
+
22
+ expect(shell).toHaveClass('cdc-open-viz-module', 'type-dashboard', 'md', 'theme-blue')
23
+ expect(shell).toHaveAttribute('data-download-id', 'dashboard-image')
24
+ expect(shell?.querySelector('.cove-visualization__outer')).toBeNull()
25
+ })
26
+
27
+ it('renders the editor panel and responsive wrapper when enabled', () => {
28
+ const { container } = render(
29
+ <VisualizationContainer
30
+ config={{ type: 'chart', showEditorPanel: true }}
31
+ currentViewport='lg'
32
+ editorPanel={<div data-testid='editor-panel'>Editor Panel</div>}
33
+ isEditor={true}
34
+ >
35
+ <div>Chart body</div>
36
+ </VisualizationContainer>
37
+ )
38
+
39
+ expect(screen.getByTestId('editor-panel')).toBeInTheDocument()
40
+ expect(container.querySelector('.cove-visualization')).toHaveClass('type-chart', 'is-editor', 'editor-panel--visible')
41
+ expect(container.querySelector('.cove-visualization__outer')).toBeInTheDocument()
42
+ })
43
+
44
+ it('hides the editor panel class when editor panels are disabled in config', () => {
45
+ const { container } = render(
46
+ <VisualizationContainer config={{ type: 'map', showEditorPanel: false }} isEditor={true}>
47
+ <div>Map body</div>
48
+ </VisualizationContainer>
49
+ )
50
+
51
+ expect(container.querySelector('.cove-visualization')).toHaveClass('editor-panel--hidden')
52
+ })
53
+
54
+ it('uses the map header theme on the shared visualization shell', () => {
55
+ const { container } = render(
56
+ <VisualizationContainer
57
+ config={{ type: 'map', general: { headerColor: 'theme-purple' } }}
58
+ currentViewport='lg'
59
+ isEditor={false}
60
+ >
61
+ <div>Map body</div>
62
+ </VisualizationContainer>
63
+ )
64
+
65
+ expect(container.querySelector('.cove-visualization')).toHaveClass('type-map', 'theme-purple')
66
+ })
67
+ })
@@ -0,0 +1,37 @@
1
+ import React, { forwardRef } from 'react'
2
+ import Responsive from './Responsive'
3
+ import Visualization, { type VisualizationShellConfig } from './Visualization'
4
+
5
+ type VisualizationContainerProps = {
6
+ children: React.ReactNode
7
+ className?: string
8
+ config: VisualizationShellConfig
9
+ currentViewport?: string
10
+ editorPanel?: React.ReactNode
11
+ imageId?: string
12
+ isEditor: boolean
13
+ renderResponsive?: boolean
14
+ }
15
+
16
+ const VisualizationContainer = forwardRef<HTMLDivElement, VisualizationContainerProps>(
17
+ ({ children, className, config, currentViewport, editorPanel, imageId, isEditor, renderResponsive = true }, ref) => {
18
+ return (
19
+ <Visualization
20
+ ref={ref}
21
+ config={config}
22
+ isEditor={isEditor}
23
+ showEditorPanel={config?.showEditorPanel}
24
+ currentViewport={currentViewport}
25
+ imageId={imageId}
26
+ className={className}
27
+ >
28
+ {isEditor && editorPanel}
29
+ {renderResponsive ? <Responsive isEditor={isEditor}>{children}</Responsive> : children}
30
+ </Visualization>
31
+ )
32
+ }
33
+ )
34
+
35
+ VisualizationContainer.displayName = 'VisualizationContainer'
36
+
37
+ export default VisualizationContainer
@@ -0,0 +1,182 @@
1
+ import React from 'react'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { describe, expect, it } from 'vitest'
4
+ import VisualizationContent from './VisualizationContent'
5
+
6
+ describe('VisualizationContent', () => {
7
+ it('renders the standard inner, body, and body-wrap layers by default', () => {
8
+ const { container } = render(
9
+ <VisualizationContent innerClassName='inner-extra' bodyClassName='body-extra' bodyWrapClassName='wrap-extra'>
10
+ <div>Inner content</div>
11
+ </VisualizationContent>
12
+ )
13
+
14
+ const inner = container.querySelector('.cove-visualization__inner')
15
+ const body = container.querySelector('.cove-visualization__body')
16
+ const bodyWrap = container.querySelector('.cove-visualization__body-wrap')
17
+ const content = container.querySelector('.cove-visualization__content-section')
18
+
19
+ expect(inner).toHaveClass('inner-extra')
20
+ expect(body).toHaveClass('body-extra')
21
+ expect(bodyWrap).toHaveClass('wrap-extra')
22
+ expect(inner?.firstElementChild).toBe(body)
23
+ expect(body?.firstElementChild).toBe(bodyWrap)
24
+ expect(bodyWrap?.firstElementChild).toBe(content)
25
+ expect(screen.getByText('Inner content')).toBeInTheDocument()
26
+ })
27
+
28
+ it('renders header and footer outside the body wrapper', () => {
29
+ const { container } = render(
30
+ <VisualizationContent
31
+ header={<div data-testid='header'>Header</div>}
32
+ footer={<div data-testid='footer'>Footer</div>}
33
+ >
34
+ <div>Wrapped content</div>
35
+ </VisualizationContent>
36
+ )
37
+
38
+ expect(screen.getByTestId('header')).toBeInTheDocument()
39
+ expect(screen.getByTestId('footer')).toBeInTheDocument()
40
+ expect(container.querySelector('.cove-visualization__body-wrap')?.textContent).toContain('Wrapped content')
41
+ })
42
+
43
+ it('renders message content inside the body-wrap before the wrapped content', () => {
44
+ const { container } = render(
45
+ <VisualizationContent message={<div data-testid='message'>Message</div>}>
46
+ <div>Wrapped content</div>
47
+ </VisualizationContent>
48
+ )
49
+
50
+ const body = container.querySelector('.cove-visualization__body')
51
+ const messageLayer = container.querySelector('.cove-visualization__message')
52
+ const message = container.querySelector('.cove-visualization__message-section')
53
+ const bodyWrap = container.querySelector('.cove-visualization__body-wrap')
54
+ const content = container.querySelector('.cove-visualization__content-section')
55
+
56
+ expect(screen.getByTestId('message')).toBeInTheDocument()
57
+ expect(messageLayer).toBe(message)
58
+ expect(body?.firstElementChild).toBe(bodyWrap)
59
+ expect(bodyWrap?.firstElementChild).toBe(message)
60
+ expect(bodyWrap?.children[1]).toBe(content)
61
+ })
62
+
63
+ it('adds the shared no-subtext body state only when outer subtext is absent', () => {
64
+ const { container: withoutSubtext } = render(
65
+ <VisualizationContent header={<div>Header</div>} message={<div>Message</div>}>
66
+ <div>Wrapped content</div>
67
+ </VisualizationContent>
68
+ )
69
+ const { container: withSubtext } = render(
70
+ <VisualizationContent header={<div>Header</div>} subtext={<div>Subtext</div>}>
71
+ <div>Wrapped content</div>
72
+ </VisualizationContent>
73
+ )
74
+
75
+ expect(withoutSubtext.querySelector('.cove-visualization__body')).toHaveClass(
76
+ 'cove-visualization__body--no-subtext'
77
+ )
78
+ expect(withSubtext.querySelector('.cove-visualization__body')).not.toHaveClass(
79
+ 'cove-visualization__body--no-subtext'
80
+ )
81
+ })
82
+
83
+ it('applies custom classes to the message wrapper', () => {
84
+ const { container } = render(
85
+ <VisualizationContent message={<div data-testid='message'>Message</div>} messageClassName='introText mb-4'>
86
+ <div>Wrapped content</div>
87
+ </VisualizationContent>
88
+ )
89
+
90
+ const message = container.querySelector('.cove-visualization__message')
91
+
92
+ expect(message).toHaveClass('introText')
93
+ expect(message).toHaveClass('mb-4')
94
+ })
95
+
96
+ it('applies the introText wrapper class when requested', () => {
97
+ const { container } = render(
98
+ <VisualizationContent message={<div data-testid='message'>Message</div>} messageIsIntroText>
99
+ <div>Wrapped content</div>
100
+ </VisualizationContent>
101
+ )
102
+
103
+ const message = container.querySelector('.cove-visualization__message')
104
+
105
+ expect(message).toHaveClass('introText')
106
+ })
107
+
108
+ it('renders subtext content inside the body after the body-wrap', () => {
109
+ const { container } = render(
110
+ <VisualizationContent subtext={<div data-testid='subtext'>Subtext</div>}>
111
+ <div>Wrapped content</div>
112
+ </VisualizationContent>
113
+ )
114
+
115
+ const body = container.querySelector('.cove-visualization__body')
116
+ const bodyWrap = container.querySelector('.cove-visualization__body-wrap')
117
+ const subtext = container.querySelector('.cove-visualization__subtext-section')
118
+
119
+ expect(screen.getByTestId('subtext')).toBeInTheDocument()
120
+ expect(body?.firstElementChild).toBe(bodyWrap)
121
+ expect(body?.lastElementChild).toBe(subtext)
122
+ expect(subtext).not.toHaveClass('mt-4')
123
+ expect(subtext).not.toHaveClass('mb-4')
124
+ expect(subtext).not.toHaveAttribute('style')
125
+ })
126
+
127
+ it('renders body subtext content inside the body-wrap after the wrapped content', () => {
128
+ const { container } = render(
129
+ <VisualizationContent bodySubtext={<div data-testid='body-subtext'>Body Subtext</div>}>
130
+ <div>Wrapped content</div>
131
+ </VisualizationContent>
132
+ )
133
+
134
+ const bodyWrap = container.querySelector('.cove-visualization__body-wrap')
135
+ const content = container.querySelector('.cove-visualization__content-section')
136
+ const bodySubtext = container.querySelector('.cove-visualization__body-subtext-section')
137
+
138
+ expect(screen.getByTestId('body-subtext')).toBeInTheDocument()
139
+ expect(bodyWrap?.lastElementChild).toBe(bodySubtext)
140
+ expect(bodyWrap?.firstElementChild).toBe(content)
141
+ expect(bodySubtext).not.toHaveClass('mt-4')
142
+ expect(bodySubtext).not.toHaveClass('mb-4')
143
+ expect(bodySubtext).not.toHaveAttribute('style')
144
+ })
145
+
146
+ it('renders body footer content inside the body-wrap after body subtext', () => {
147
+ const { container } = render(
148
+ <VisualizationContent
149
+ bodySubtext={<div data-testid='body-subtext'>Body Subtext</div>}
150
+ bodyFooter={<div data-testid='body-footer'>Body Footer</div>}
151
+ >
152
+ <div>Wrapped content</div>
153
+ </VisualizationContent>
154
+ )
155
+
156
+ const bodyWrap = container.querySelector('.cove-visualization__body-wrap')
157
+ const content = container.querySelector('.cove-visualization__content-section')
158
+ const bodySubtext = container.querySelector('.cove-visualization__body-subtext-section')
159
+ const bodyFooter = container.querySelector('.cove-visualization__body-footer-section')
160
+
161
+ expect(screen.getByTestId('body-footer')).toBeInTheDocument()
162
+ expect(bodyWrap?.children[0]).toBe(content)
163
+ expect(bodyWrap?.children[1]).toBe(bodySubtext)
164
+ expect(bodyWrap?.lastElementChild).toBe(bodyFooter)
165
+ })
166
+
167
+ it('passes through non-class inner div props, including refs', () => {
168
+ const innerRef = React.createRef<HTMLDivElement>()
169
+
170
+ const { container } = render(
171
+ <VisualizationContent innerClassName='inner-extra' innerProps={{ 'aria-label': 'Visualization content', ref: innerRef }}>
172
+ <div>Inner content</div>
173
+ </VisualizationContent>
174
+ )
175
+
176
+ const inner = container.querySelector('.cove-visualization__inner')
177
+
178
+ expect(inner).toHaveAttribute('aria-label', 'Visualization content')
179
+ expect(inner).toHaveClass('inner-extra')
180
+ expect(innerRef.current).toBe(inner)
181
+ })
182
+ })
@@ -0,0 +1,75 @@
1
+ import React from 'react'
2
+
3
+ type VisualizationContentInnerProps = Omit<React.ComponentPropsWithRef<'div'>, 'className'>
4
+
5
+ type VisualizationContentProps = {
6
+ bodyFooter?: React.ReactNode
7
+ bodySubtext?: React.ReactNode
8
+ bodyClassName?: string
9
+ bodyWrapClassName?: string
10
+ children: React.ReactNode
11
+ filters?: React.ReactNode
12
+ footer?: React.ReactNode
13
+ header?: React.ReactNode
14
+ innerClassName?: string
15
+ innerProps?: VisualizationContentInnerProps
16
+ message?: React.ReactNode
17
+ messageClassName?: string
18
+ messageIsIntroText?: boolean
19
+ subtext?: React.ReactNode
20
+ }
21
+
22
+ const VisualizationContent = ({
23
+ bodyFooter,
24
+ bodySubtext,
25
+ bodyClassName,
26
+ bodyWrapClassName,
27
+ children,
28
+ filters,
29
+ footer,
30
+ header,
31
+ innerClassName,
32
+ innerProps,
33
+ message,
34
+ messageClassName,
35
+ messageIsIntroText,
36
+ subtext
37
+ }: VisualizationContentProps) => {
38
+ const resolvedInnerClasses = ['cove-visualization__inner', innerClassName].filter(Boolean).join(' ')
39
+ const bodyClasses = ['cove-visualization__body', !subtext && 'cove-visualization__body--no-subtext', bodyClassName].filter(Boolean).join(' ')
40
+ const bodyWrapClasses = ['cove-visualization__body-wrap', bodyWrapClassName].filter(Boolean).join(' ')
41
+
42
+ return (
43
+ <div {...innerProps} className={resolvedInnerClasses}>
44
+ {header}
45
+ <div className={bodyClasses}>
46
+ <div className={bodyWrapClasses}>
47
+ {message ? (
48
+ <section
49
+ className={[
50
+ 'cove-visualization__message',
51
+ 'cove-visualization__message-section',
52
+ messageIsIntroText && 'introText',
53
+ messageClassName
54
+ ]
55
+ .filter(Boolean)
56
+ .join(' ')}
57
+ >
58
+ {message}
59
+ </section>
60
+ ) : null}
61
+ {filters ? <section className='cove-visualization__filters-section'>{filters}</section> : null}
62
+ <section className='cove-visualization__content-section'>{children}</section>
63
+ {bodySubtext ? (
64
+ <section className='cove-visualization__body-subtext-section'>{bodySubtext}</section>
65
+ ) : null}
66
+ {bodyFooter ? <section className='cove-visualization__body-footer-section'>{bodyFooter}</section> : null}
67
+ </div>
68
+ {subtext ? <section className='cove-visualization__subtext-section'>{subtext}</section> : null}
69
+ </div>
70
+ {footer}
71
+ </div>
72
+ )
73
+ }
74
+
75
+ export default VisualizationContent
@@ -1,11 +1,11 @@
1
1
  import Responsive from './components/Responsive'
2
2
  import Sidebar from './components/Sidebar'
3
+ import VisualizationContainer from './components/VisualizationContainer'
4
+ import VisualizationContent from './components/VisualizationContent'
3
5
  import VisualizationWrapper from './components/Visualization'
4
6
 
5
- const Layout = {
6
- Responsive,
7
- Sidebar,
8
- VisualizationWrapper
9
- }
7
+ export { Responsive, Sidebar, VisualizationContainer, VisualizationContent, VisualizationWrapper }
10
8
 
9
+ /** @deprecated Use named imports instead: `import { Responsive, Sidebar, VisualizationContainer, VisualizationContent } from '@cdc/core/components/Layout'` */
10
+ const Layout = { Responsive, Sidebar, VisualizationContainer, VisualizationContent, VisualizationWrapper }
11
11
  export default Layout
@@ -1,7 +1,7 @@
1
1
  $editorAnimationTimer: 400ms;
2
2
  $primary: #3e92d5;
3
3
 
4
- .cdc-open-viz-module .cove-editor-utils__hotkeys {
4
+ .cove-visualization .cove-editor-utils__hotkeys {
5
5
  display: inline-flex;
6
6
  flex-flow: row nowrap;
7
7
  position: fixed;
@@ -54,7 +54,7 @@ $primary: #3e92d5;
54
54
  }
55
55
  }
56
56
 
57
- .cdc-open-viz-module .cove-editor-utils__breakpoints {
57
+ .cove-visualization .cove-editor-utils__breakpoints {
58
58
  position: fixed;
59
59
  bottom: 5rem;
60
60
  right: 4rem;
@@ -97,7 +97,7 @@ $primary: #3e92d5;
97
97
  list-style: none;
98
98
  }
99
99
 
100
- .cdc-open-viz-module .cove-editor-utils__breakpoints-item {
100
+ .cove-visualization .cove-editor-utils__breakpoints-item {
101
101
  display: block;
102
102
  position: relative;
103
103
  margin-right: 1rem;