@cdc/core 4.26.1 → 4.26.2

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 (99) hide show
  1. package/.claude/agents/qa-test-developer.md +126 -0
  2. package/CLAUDE.local.md +67 -0
  3. package/_stories/Gallery.Charts.stories.tsx +34 -41
  4. package/_stories/Gallery.DataBite.stories.tsx +14 -7
  5. package/_stories/Gallery.Maps.stories.tsx +36 -27
  6. package/_stories/Gallery.WaffleChart.stories.tsx +1 -1
  7. package/_stories/PageART.stories.tsx +4 -3
  8. package/_stories/PageBRFSS.stories.tsx +20 -15
  9. package/_stories/PageCancerRegistries.stories.tsx +14 -14
  10. package/_stories/PageEasternEquineEncephalitis.stories.tsx +30 -16
  11. package/_stories/PageExcessiveAlcoholUse.stories.tsx +148 -143
  12. package/_stories/PageMaternalMortality.stories.tsx +4 -3
  13. package/_stories/PageOralHealth.stories.tsx +14 -9
  14. package/_stories/PageSmokingTobacco.stories.tsx +14 -9
  15. package/_stories/PageStateDiabetesProfiles.stories.tsx +14 -9
  16. package/_stories/PageWastewater.stories.tsx +40 -26
  17. package/_stories/VegaImport.stories.tsx +401 -0
  18. package/_stories/vega-fixtures/bars-with-line.json +444 -0
  19. package/_stories/vega-fixtures/bars.json +58 -0
  20. package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
  21. package/_stories/vega-fixtures/combo.json +68 -0
  22. package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
  23. package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
  24. package/_stories/vega-fixtures/horizontal-bar.json +427 -0
  25. package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
  26. package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
  27. package/_stories/vega-fixtures/lines.json +227 -0
  28. package/_stories/vega-fixtures/measles-bars.json +348 -0
  29. package/_stories/vega-fixtures/measles-map.json +11101 -0
  30. package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
  31. package/_stories/vega-fixtures/multi-dataset.json +255 -0
  32. package/_stories/vega-fixtures/no-data.json +14 -0
  33. package/_stories/vega-fixtures/pie-chart.json +94 -0
  34. package/_stories/vega-fixtures/repeat-spec.json +47 -0
  35. package/_stories/vega-fixtures/stacked-area.json +222 -0
  36. package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
  37. package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
  38. package/_stories/vega-fixtures/stacked-bars.json +212 -0
  39. package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
  40. package/_stories/vega-fixtures/warning-combo.json +59 -0
  41. package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
  42. package/assets/icon-chart-area.svg +1 -0
  43. package/assets/icon-chart-radar.svg +23 -0
  44. package/assets/logo2.svg +31 -0
  45. package/components/AdvancedEditor/EmbedEditor.tsx +270 -38
  46. package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
  47. package/components/DataTable/helpers/getSeriesName.ts +6 -0
  48. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +14 -6
  49. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +4 -0
  50. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
  51. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +2 -2
  52. package/components/Layout/components/Visualization/index.tsx +11 -0
  53. package/components/MediaControls.tsx +0 -1
  54. package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
  55. package/components/_stories/DataTable.stories.tsx +1 -0
  56. package/data/colorPalettes.ts +18 -5
  57. package/data/mapColorPalettes.ts +10 -0
  58. package/devTemplate/dev.js +235 -0
  59. package/devTemplate/index.html +30 -0
  60. package/devTemplate/preview.html +1503 -0
  61. package/devTemplate/sidebar.css +151 -0
  62. package/dist/cove-main.css +2803 -4471
  63. package/dist/cove-main.css.map +1 -1
  64. package/generateViteConfig.js +111 -2
  65. package/helpers/DataTransform.ts +1 -5
  66. package/helpers/cove/date.ts +33 -1
  67. package/helpers/cove/string.ts +29 -0
  68. package/helpers/coveUpdateWorker.ts +3 -1
  69. package/helpers/embed/embedCodeGenerator.ts +80 -0
  70. package/helpers/embed/embedHelper.js +158 -0
  71. package/helpers/embed/filterUtils.ts +121 -0
  72. package/helpers/embed/index.ts +21 -0
  73. package/helpers/embed/urlValidation.ts +119 -0
  74. package/helpers/filterVizData.ts +6 -1
  75. package/helpers/getFileExtension.ts +0 -6
  76. package/helpers/metrics/types.ts +3 -0
  77. package/helpers/palettes/colorDistributions.ts +1 -1
  78. package/helpers/palettes/utils.ts +12 -12
  79. package/helpers/parseCsvWithQuotes.ts +15 -14
  80. package/helpers/prepareScreenshot.ts +27 -7
  81. package/helpers/testing.ts +44 -0
  82. package/helpers/tests/DataTransform.test.ts +125 -0
  83. package/helpers/tests/date.test.ts +64 -0
  84. package/helpers/vegaConfig.ts +1 -1
  85. package/helpers/vegaConfigImport.ts +160 -0
  86. package/helpers/ver/4.26.1.ts +1 -1
  87. package/helpers/ver/4.26.2.ts +84 -0
  88. package/helpers/ver/tests/4.26.1.test.ts +105 -0
  89. package/helpers/ver/tests/4.26.2.test.ts +298 -0
  90. package/helpers/viewports.ts +2 -0
  91. package/package.json +27 -32
  92. package/styles/v2/components/editor.scss +9 -9
  93. package/styles/v2/utils/_grid.scss +8 -3
  94. package/types/Annotation.ts +10 -11
  95. package/types/General.ts +2 -0
  96. package/types/Palette.ts +21 -0
  97. package/types/Visualization.ts +6 -0
  98. package/_stories/StoryRenderingTests.stories.tsx +0 -164
  99. package/helpers/embedCodeGenerator.ts +0 -109
@@ -3,37 +3,41 @@ import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'
3
3
  type FilterOrderProps = {
4
4
  orderedValues: string[]
5
5
  handleFilterOrder?: (index1: number, index2: number) => void
6
+ onNestedDragAreaHover?: (isHovering: boolean) => void
6
7
  }
7
- const FilterOrder: React.FC<FilterOrderProps> = ({ orderedValues, handleFilterOrder }) => {
8
+
9
+ const FilterOrder: React.FC<FilterOrderProps> = ({ orderedValues, handleFilterOrder, onNestedDragAreaHover }) => {
8
10
  return (
9
- <DragDropContext onDragEnd={({ source, destination }) => handleFilterOrder(source?.index, destination?.index)}>
10
- <Droppable droppableId='filter_order'>
11
- {provided => (
12
- <ul {...provided.droppableProps} className='sort-list' ref={provided.innerRef} style={{ marginTop: '1em' }}>
13
- {orderedValues?.map((value, index) => {
14
- return (
15
- <Draggable key={value} draggableId={`draggableFilter-${value}`} index={index}>
16
- {(provided, snapshot) => (
17
- <li>
18
- <div
19
- className={snapshot.isDragging ? 'currently-dragging' : ''}
20
- style={provided.draggableProps.style}
21
- ref={provided.innerRef}
22
- {...provided.draggableProps}
23
- {...provided.dragHandleProps}
24
- >
25
- {value}
26
- </div>
27
- </li>
28
- )}
29
- </Draggable>
30
- )
31
- })}
32
- {provided.placeholder}
33
- </ul>
34
- )}
35
- </Droppable>
36
- </DragDropContext>
11
+ <div onMouseEnter={() => onNestedDragAreaHover?.(true)} onMouseLeave={() => onNestedDragAreaHover?.(false)}>
12
+ <DragDropContext onDragEnd={({ source, destination }) => handleFilterOrder(source?.index, destination?.index)}>
13
+ <Droppable droppableId='filter_order'>
14
+ {provided => (
15
+ <ul {...provided.droppableProps} className='sort-list' ref={provided.innerRef} style={{ marginTop: '1em' }}>
16
+ {orderedValues?.map((value, index) => {
17
+ return (
18
+ <Draggable key={value} draggableId={`draggableFilter-${value}`} index={index}>
19
+ {(provided, snapshot) => (
20
+ <li>
21
+ <div
22
+ className={snapshot.isDragging ? 'currently-dragging' : ''}
23
+ style={provided.draggableProps.style}
24
+ ref={provided.innerRef}
25
+ {...provided.draggableProps}
26
+ {...provided.dragHandleProps}
27
+ >
28
+ {value}
29
+ </div>
30
+ </li>
31
+ )}
32
+ </Draggable>
33
+ )
34
+ })}
35
+ {provided.placeholder}
36
+ </ul>
37
+ )}
38
+ </Droppable>
39
+ </DragDropContext>
40
+ </div>
37
41
  )
38
42
  }
39
43
 
@@ -626,9 +626,9 @@
626
626
 
627
627
  .sort-list {
628
628
  list-style: none;
629
+ padding: 0;
629
630
 
630
631
  > li {
631
- margin-right: 0.3em;
632
632
  margin-bottom: 0.3em;
633
633
  }
634
634
  }
@@ -641,8 +641,8 @@
641
641
  background: #f1f1f1;
642
642
  padding: 0.4em 0.6em;
643
643
  font-size: 0.8em;
644
- margin-bottom: 0.3em;
645
644
  cursor: move;
645
+ width: 100%;
646
646
  }
647
647
 
648
648
  .info {
@@ -71,6 +71,10 @@ const Visualization = forwardRef<HTMLDivElement, VisualizationWrapper>((props, r
71
71
  if (config?.runtime?.editorErrorMessage.length !== 0) classes.push('type-map--has-error')
72
72
  }
73
73
 
74
+ if (config.type === 'table') {
75
+ classes.push('type-data-table')
76
+ }
77
+
74
78
  if (config.type === 'data-bite') {
75
79
  classes.push('cdc-open-viz-module', 'type-data-bite', currentViewport, config.theme, `font-${config.fontSize}`)
76
80
  if (isEditor) {
@@ -104,6 +108,13 @@ const Visualization = forwardRef<HTMLDivElement, VisualizationWrapper>((props, r
104
108
  }
105
109
  }
106
110
 
111
+ if (config.visualizationType === 'TP5 Gauge') {
112
+ classes.push('gauge__style--tp5')
113
+ if (config.visual?.whiteBackground) {
114
+ classes.push('white-background-style')
115
+ }
116
+ }
117
+
107
118
  classes.push('cove-component', 'waffle-chart')
108
119
  }
109
120
  return classes
@@ -1,5 +1,4 @@
1
1
  import React from 'react'
2
- // import html2pdf from 'html2pdf.js'
3
2
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
4
3
  import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
5
4
  import { prepareScreenshotContainer } from '@cdc/core/helpers/prepareScreenshot'
@@ -0,0 +1,37 @@
1
+ import React, { useState } from 'react'
2
+ import type { Meta, StoryObj } from '@storybook/react-vite'
3
+ import CustomColorsEditor from '../CustomColorsEditor/CustomColorsEditor'
4
+
5
+ const meta: Meta<typeof CustomColorsEditor> = {
6
+ title: 'Components/Atoms/CustomColorsEditor',
7
+ component: CustomColorsEditor
8
+ }
9
+
10
+ export default meta
11
+ type Story = StoryObj<typeof CustomColorsEditor>
12
+
13
+ const fiftyColors = [
14
+ '#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231',
15
+ '#911eb4', '#42d4f4', '#f032e6', '#bfef45', '#fabed4',
16
+ '#469990', '#dcbeff', '#9a6324', '#fffac8', '#800000',
17
+ '#aaffc3', '#808000', '#ffd8b1', '#000075', '#a9a9a9',
18
+ '#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231',
19
+ '#911eb4', '#42d4f4', '#f032e6', '#bfef45', '#fabed4',
20
+ '#469990', '#dcbeff', '#9a6324', '#fffac8', '#800000',
21
+ '#aaffc3', '#808000', '#ffd8b1', '#000075', '#a9a9a9',
22
+ '#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00',
23
+ '#ffff33', '#a65628', '#f781bf', '#999999', '#66c2a5'
24
+ ]
25
+
26
+ const Wrapper = ({ initialColors }: { initialColors: string[] }) => {
27
+ const [colors, setColors] = useState(initialColors)
28
+ return <CustomColorsEditor colors={colors} onChange={setColors} label='Custom Color Order' />
29
+ }
30
+
31
+ export const Default: Story = {
32
+ render: () => <Wrapper initialColors={['#3366cc', '#dc3912', '#ff9900']} />
33
+ }
34
+
35
+ export const FiftyColors: Story = {
36
+ render: () => <Wrapper initialColors={fiftyColors} />
37
+ }
@@ -45,6 +45,7 @@ export const CityState: Story = {
45
45
  tabbingId: '#asdf',
46
46
  columns: CityStateExample.columns,
47
47
  applyLegendToRow: () => ['#000'],
48
+ getPatternForRow: () => null,
48
49
  displayGeoName
49
50
  },
50
51
  decorators: [
@@ -1,9 +1,22 @@
1
- import { mapColorPalettes, mapColorPalettesV1, mapColorPalettesV2 } from './mapColorPalettes'
2
- import { chartColorPalettes, sequentialPalettes, colorPalettesChart, colorPalettesChartV1, colorPalettesChartV2, twoColorPalette } from './chartColorPalettes'
3
-
1
+ import { mapColorPalettes, mapColorPalettesV1, mapColorPalettesV2, sequentialZeroColors } from './mapColorPalettes'
2
+ import {
3
+ chartColorPalettes,
4
+ sequentialPalettes,
5
+ colorPalettesChart,
6
+ colorPalettesChartV1,
7
+ colorPalettesChartV2,
8
+ twoColorPalette
9
+ } from './chartColorPalettes'
4
10
 
5
11
  // Re-export map palettes (already processed in mapColorPalettes.ts)
6
- export { mapColorPalettes, mapColorPalettesV1, mapColorPalettesV2 }
12
+ export { mapColorPalettes, mapColorPalettesV1, mapColorPalettesV2, sequentialZeroColors }
7
13
 
8
14
  // Re-export chart palettes (already processed in chartColorPalettes.ts)
9
- export { chartColorPalettes, sequentialPalettes, colorPalettesChart, colorPalettesChartV1, colorPalettesChartV2, twoColorPalette }
15
+ export {
16
+ chartColorPalettes,
17
+ sequentialPalettes,
18
+ colorPalettesChart,
19
+ colorPalettesChartV1,
20
+ colorPalettesChartV2,
21
+ twoColorPalette
22
+ }
@@ -43,3 +43,13 @@ export const mapColorPalettes = {
43
43
 
44
44
  export const mapColorPalettesV1 = mapColorPalettes.v1
45
45
  export const mapColorPalettesV2 = mapColorPalettes.v2
46
+
47
+ // Lighter colors (first palette color 75% toward white) for zero-value categories in v2 sequential palettes
48
+ // Used when: categorical map, gradient legend, first category value is "0", palette not reversed
49
+ export const sequentialZeroColors: Record<string, string> = {
50
+ sequential_blue: '#F6F9FD',
51
+ sequential_teal: '#F9FEFE',
52
+ sequential_purple: '#F8F4F7',
53
+ sequential_orange: '#FFF8F5',
54
+ sequential_green: '#F5FEFD'
55
+ }
@@ -0,0 +1,235 @@
1
+ // Dev template JavaScript
2
+ // Handles config URL params, visualization reloading, and sidebar
3
+
4
+ // Apply config override from ?config= URL parameter (must happen before React loads)
5
+ const params = new URLSearchParams(window.location.search)
6
+ const configParam = params.get('config')
7
+ let editorEnabled = params.get('editor') === 'true'
8
+ const previewEnabled = params.get('preview') === 'true'
9
+
10
+ if (configParam) {
11
+ document.querySelector('.react-container').setAttribute('data-config', configParam)
12
+ }
13
+ if (editorEnabled) {
14
+ document.querySelector('.react-container').setAttribute('data-editor', 'true')
15
+ }
16
+
17
+ // Load the visualization component
18
+ await import('./src/index')
19
+
20
+ // Reload visualization without page refresh
21
+ window.reloadVisualization = async configUrl => {
22
+ const wrapper = document.getElementById('viz-wrapper')
23
+ const editorAttr = editorEnabled ? ' data-editor="true"' : ''
24
+ wrapper.innerHTML = `<div class="react-container" data-config="${configUrl}"${editorAttr}></div>`
25
+ await import(/* @vite-ignore */ `./src/index?t=${Date.now()}`)
26
+ }
27
+
28
+ // Initialize sidebar by default (hide with ?sidebar=false, or for editor package)
29
+ // __COVE_PACKAGE_NAME__ is injected by Vite's define option in generateViteConfig.js
30
+ const sidebarDisabled = params.get('sidebar') === 'false' || __COVE_PACKAGE_NAME__ === 'CdcEditor'
31
+ if (sidebarDisabled) {
32
+ // Remove sidebar margin (the inline script in the HTML template pre-allocates it for ?sidebar!=false,
33
+ // but CdcEditor also disables the sidebar without that param)
34
+ document.body.style.marginLeft = ''
35
+ }
36
+ if (!sidebarDisabled) {
37
+ document.body.classList.add('has-sidebar')
38
+
39
+ // Fetch examples list
40
+ const response = await fetch('/__examples')
41
+ const examples = await response.json()
42
+
43
+ // Get current config
44
+ const currentConfig = configParam || '/examples/default.json'
45
+
46
+ // Build sidebar HTML
47
+ const sidebarRoot = document.getElementById('dev-sidebar-root')
48
+
49
+ // Build a recursive tree structure for arbitrary nesting depth
50
+ const buildTree = files => {
51
+ const tree = { files: [], dirs: {} }
52
+ files.forEach(file => {
53
+ const parts = file.split('/')
54
+ if (parts.length === 1) {
55
+ tree.files.push(file)
56
+ } else {
57
+ const dir = parts[0]
58
+ if (!tree.dirs[dir]) tree.dirs[dir] = []
59
+ tree.dirs[dir].push(parts.slice(1).join('/'))
60
+ }
61
+ })
62
+ // Recursively build subtrees for each directory
63
+ Object.keys(tree.dirs).forEach(dir => {
64
+ tree.dirs[dir] = buildTree(tree.dirs[dir])
65
+ })
66
+ return tree
67
+ }
68
+
69
+ const tree = buildTree(examples)
70
+
71
+ const caseInsensitiveSort = (a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })
72
+
73
+ // Recursive function to render tree at any depth
74
+ const renderTree = (node, pathPrefix) => {
75
+ let html = ''
76
+
77
+ // Render files at this level
78
+ node.files.sort(caseInsensitiveSort).forEach(file => {
79
+ const configPath = pathPrefix + file
80
+ const isActive = configPath === currentConfig ? ' active' : ''
81
+ html += `<button class="dev-sidebar-item${isActive}" data-config="${configPath}">${file}</button>`
82
+ })
83
+
84
+ // Render subdirectories recursively
85
+ Object.keys(node.dirs)
86
+ .sort(caseInsensitiveSort)
87
+ .forEach(dir => {
88
+ const dirPath = pathPrefix + dir + '/'
89
+ const isOpen = currentConfig.startsWith(dirPath) ? ' open' : ''
90
+ html += `<div class="dev-sidebar-folder${isOpen}" data-folder-path="${dirPath}">${dir}</div>`
91
+ html += '<div class="dev-sidebar-folder-contents">'
92
+ html += renderTree(node.dirs[dir], dirPath)
93
+ html += '</div>'
94
+ })
95
+
96
+ return html
97
+ }
98
+
99
+ // Format package name for display (e.g., "CdcChart" -> "Chart", "CdcMap" -> "Map")
100
+ const formatPackageName = name => {
101
+ if (!name) return ''
102
+ return name
103
+ .replace(/^Cdc/, '')
104
+ .replace(/([A-Z])/g, ' $1')
105
+ .trim()
106
+ }
107
+ const packageDisplayName = formatPackageName(__COVE_PACKAGE_NAME__)
108
+
109
+ const editorToggleClass = editorEnabled ? ' active' : ''
110
+ const previewToggleClass = previewEnabled ? ' active' : ''
111
+ let html = '<nav class="dev-sidebar">'
112
+ html += `<div class="dev-sidebar-header"><span>${packageDisplayName} Examples</span><div class="dev-sidebar-toggles"><button class="dev-sidebar-toggle${previewToggleClass}" id="dev-preview-toggle" title="Toggle CDC Page Preview"><svg width="20" height="20" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 1.5h7l3.5 3.5V14a.5.5 0 0 1-.5.5H3a.5.5 0 0 1-.5-.5V2a.5.5 0 0 1 .5-.5z"/><path d="M10 1.5V5h3.5"/></svg></button><button class="dev-sidebar-toggle dev-sidebar-editor-toggle${editorToggleClass}" id="dev-editor-toggle" title="Toggle Editor">⚙</button></div></div>`
113
+ html +=
114
+ '<div class="dev-sidebar-search"><input type="text" id="dev-sidebar-search-input" placeholder="Search examples..." /></div>'
115
+ html += '<div class="dev-sidebar-tree">'
116
+ html += renderTree(tree, '/examples/')
117
+ html += '</div></nav>'
118
+ sidebarRoot.innerHTML = html
119
+
120
+ // Search functionality
121
+ const searchInput = document.getElementById('dev-sidebar-search-input')
122
+ searchInput.addEventListener('input', e => {
123
+ const query = e.target.value.toLowerCase()
124
+ const items = sidebarRoot.querySelectorAll('.dev-sidebar-item')
125
+ const folders = sidebarRoot.querySelectorAll('.dev-sidebar-folder')
126
+
127
+ if (!query) {
128
+ // Reset: show all items, collapse folders (except those with active item)
129
+ items.forEach(item => (item.style.display = ''))
130
+ folders.forEach(folder => {
131
+ folder.style.display = ''
132
+ if (!folder.nextElementSibling?.querySelector('.active')) {
133
+ folder.classList.remove('open')
134
+ }
135
+ })
136
+ return
137
+ }
138
+
139
+ // Split query into tokens - all must match (in any order)
140
+ const tokens = query.split(/\s+/).filter(t => t)
141
+ const matchesAllTokens = text => tokens.every(token => text.includes(token))
142
+
143
+ // First pass: find folders that match the query
144
+ const matchingFolderPaths = new Set()
145
+ folders.forEach(folder => {
146
+ const folderName = folder.textContent.toLowerCase()
147
+ if (matchesAllTokens(folderName)) {
148
+ matchingFolderPaths.add(folder.dataset.folderPath)
149
+ }
150
+ })
151
+
152
+ // Filter items: show if item matches OR is inside a matching folder
153
+ items.forEach(item => {
154
+ const configPath = item.dataset.config
155
+ const itemMatches = matchesAllTokens(item.textContent.toLowerCase())
156
+ const inMatchingFolder = [...matchingFolderPaths].some(folderPath => configPath.startsWith(folderPath))
157
+ item.style.display = itemMatches || inMatchingFolder ? '' : 'none'
158
+ })
159
+
160
+ // Show/hide folders based on whether they match or have visible children
161
+ folders.forEach(folder => {
162
+ const folderPath = folder.dataset.folderPath
163
+ const folderMatches = matchingFolderPaths.has(folderPath)
164
+ const contents = folder.nextElementSibling
165
+ const hasVisibleChildren = contents?.querySelector('.dev-sidebar-item:not([style*="display: none"])')
166
+ folder.style.display = folderMatches || hasVisibleChildren ? '' : 'none'
167
+ if (folderMatches || hasVisibleChildren) {
168
+ folder.classList.add('open')
169
+ }
170
+ })
171
+ })
172
+
173
+ // Click handlers for files
174
+ sidebarRoot.querySelectorAll('.dev-sidebar-item').forEach(btn => {
175
+ btn.addEventListener('click', async () => {
176
+ const configPath = btn.dataset.config
177
+
178
+ // Update active state
179
+ sidebarRoot.querySelectorAll('.dev-sidebar-item').forEach(b => b.classList.remove('active'))
180
+ btn.classList.add('active')
181
+
182
+ // Update URL without reload - keep clean if selecting default
183
+ const url = new URL(window.location)
184
+ if (configPath === '/examples/default.json') {
185
+ url.searchParams.delete('config')
186
+ } else {
187
+ url.searchParams.set('config', configPath)
188
+ }
189
+ history.pushState({}, '', url.toString().replace(/%2F/g, '/'))
190
+
191
+ // Reload visualization
192
+ await window.reloadVisualization(configPath)
193
+ })
194
+ })
195
+
196
+ // Click handlers for folders
197
+ sidebarRoot.querySelectorAll('.dev-sidebar-folder').forEach(folder => {
198
+ folder.addEventListener('click', () => {
199
+ folder.classList.toggle('open')
200
+ })
201
+ })
202
+
203
+ // Editor toggle handler
204
+ const editorToggle = document.getElementById('dev-editor-toggle')
205
+ editorToggle.addEventListener('click', async () => {
206
+ editorEnabled = !editorEnabled
207
+ editorToggle.classList.toggle('active', editorEnabled)
208
+
209
+ // Update URL
210
+ const url = new URL(window.location)
211
+ if (editorEnabled) {
212
+ url.searchParams.set('editor', 'true')
213
+ } else {
214
+ url.searchParams.delete('editor')
215
+ }
216
+ history.pushState({}, '', url.toString().replace(/%2F/g, '/'))
217
+
218
+ // Reload visualization with new editor state
219
+ const currentConfig =
220
+ document.querySelector('.react-container')?.getAttribute('data-config') || '/examples/default.json'
221
+ await window.reloadVisualization(currentConfig)
222
+ })
223
+
224
+ // Preview toggle handler - full page reload since server needs to serve different HTML
225
+ const previewToggle = document.getElementById('dev-preview-toggle')
226
+ previewToggle.addEventListener('click', () => {
227
+ const url = new URL(window.location)
228
+ if (previewEnabled) {
229
+ url.searchParams.delete('preview')
230
+ } else {
231
+ url.searchParams.set('preview', 'true')
232
+ }
233
+ window.location.href = url.toString().replace(/%2F/g, '/')
234
+ })
235
+ }
@@ -0,0 +1,30 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
6
+ <style>
7
+ body {
8
+ margin: 0;
9
+ border-top: none !important;
10
+ min-height: calc(100vh + 1px);
11
+ }
12
+ /* {{PACKAGE_CSS}} */
13
+ /* {{SIDEBAR_CSS}} */
14
+ </style>
15
+ <link rel="stylesheet prefetch" href="https://www.cdc.gov/TemplatePackage/5.0/css/app.min.css?_=71669" />
16
+ </head>
17
+ <body>
18
+ <script>
19
+ if (new URLSearchParams(location.search).get('sidebar') !== 'false') document.body.style.marginLeft = '240px'
20
+ </script>
21
+ <div id="dev-sidebar-root"></div>
22
+ <div id="viz-wrapper">
23
+ <div class="react-container" data-config="/examples/default.json"></div>
24
+ </div>
25
+ <noscript>You need to enable JavaScript to run this app.</noscript>
26
+ <script type="module">
27
+ // {{DEV_JS}}
28
+ </script>
29
+ </body>
30
+ </html>