@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.
- package/.claude/agents/qa-test-developer.md +126 -0
- package/CLAUDE.local.md +67 -0
- package/_stories/Gallery.Charts.stories.tsx +34 -41
- package/_stories/Gallery.DataBite.stories.tsx +14 -7
- package/_stories/Gallery.Maps.stories.tsx +36 -27
- package/_stories/Gallery.WaffleChart.stories.tsx +1 -1
- package/_stories/PageART.stories.tsx +4 -3
- package/_stories/PageBRFSS.stories.tsx +20 -15
- package/_stories/PageCancerRegistries.stories.tsx +14 -14
- package/_stories/PageEasternEquineEncephalitis.stories.tsx +30 -16
- package/_stories/PageExcessiveAlcoholUse.stories.tsx +148 -143
- package/_stories/PageMaternalMortality.stories.tsx +4 -3
- package/_stories/PageOralHealth.stories.tsx +14 -9
- package/_stories/PageSmokingTobacco.stories.tsx +14 -9
- package/_stories/PageStateDiabetesProfiles.stories.tsx +14 -9
- package/_stories/PageWastewater.stories.tsx +40 -26
- package/_stories/VegaImport.stories.tsx +401 -0
- package/_stories/vega-fixtures/bars-with-line.json +444 -0
- package/_stories/vega-fixtures/bars.json +58 -0
- package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
- package/_stories/vega-fixtures/combo.json +68 -0
- package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
- package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
- package/_stories/vega-fixtures/horizontal-bar.json +427 -0
- package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
- package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
- package/_stories/vega-fixtures/lines.json +227 -0
- package/_stories/vega-fixtures/measles-bars.json +348 -0
- package/_stories/vega-fixtures/measles-map.json +11101 -0
- package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
- package/_stories/vega-fixtures/multi-dataset.json +255 -0
- package/_stories/vega-fixtures/no-data.json +14 -0
- package/_stories/vega-fixtures/pie-chart.json +94 -0
- package/_stories/vega-fixtures/repeat-spec.json +47 -0
- package/_stories/vega-fixtures/stacked-area.json +222 -0
- package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
- package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
- package/_stories/vega-fixtures/stacked-bars.json +212 -0
- package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
- package/_stories/vega-fixtures/warning-combo.json +59 -0
- package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
- package/assets/icon-chart-area.svg +1 -0
- package/assets/icon-chart-radar.svg +23 -0
- package/assets/logo2.svg +31 -0
- package/components/AdvancedEditor/EmbedEditor.tsx +270 -38
- package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
- package/components/DataTable/helpers/getSeriesName.ts +6 -0
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +14 -6
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +4 -0
- package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +2 -2
- package/components/Layout/components/Visualization/index.tsx +11 -0
- package/components/MediaControls.tsx +0 -1
- package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
- package/components/_stories/DataTable.stories.tsx +1 -0
- package/data/colorPalettes.ts +18 -5
- package/data/mapColorPalettes.ts +10 -0
- package/devTemplate/dev.js +235 -0
- package/devTemplate/index.html +30 -0
- package/devTemplate/preview.html +1503 -0
- package/devTemplate/sidebar.css +151 -0
- package/dist/cove-main.css +2803 -4471
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +111 -2
- package/helpers/DataTransform.ts +1 -5
- package/helpers/cove/date.ts +33 -1
- package/helpers/cove/string.ts +29 -0
- package/helpers/coveUpdateWorker.ts +3 -1
- package/helpers/embed/embedCodeGenerator.ts +80 -0
- package/helpers/embed/embedHelper.js +158 -0
- package/helpers/embed/filterUtils.ts +121 -0
- package/helpers/embed/index.ts +21 -0
- package/helpers/embed/urlValidation.ts +119 -0
- package/helpers/filterVizData.ts +6 -1
- package/helpers/getFileExtension.ts +0 -6
- package/helpers/metrics/types.ts +3 -0
- package/helpers/palettes/colorDistributions.ts +1 -1
- package/helpers/palettes/utils.ts +12 -12
- package/helpers/parseCsvWithQuotes.ts +15 -14
- package/helpers/prepareScreenshot.ts +27 -7
- package/helpers/testing.ts +44 -0
- package/helpers/tests/DataTransform.test.ts +125 -0
- package/helpers/tests/date.test.ts +64 -0
- package/helpers/vegaConfig.ts +1 -1
- package/helpers/vegaConfigImport.ts +160 -0
- package/helpers/ver/4.26.1.ts +1 -1
- package/helpers/ver/4.26.2.ts +84 -0
- package/helpers/ver/tests/4.26.1.test.ts +105 -0
- package/helpers/ver/tests/4.26.2.test.ts +298 -0
- package/helpers/viewports.ts +2 -0
- package/package.json +27 -32
- package/styles/v2/components/editor.scss +9 -9
- package/styles/v2/utils/_grid.scss +8 -3
- package/types/Annotation.ts +10 -11
- package/types/General.ts +2 -0
- package/types/Palette.ts +21 -0
- package/types/Visualization.ts +6 -0
- package/_stories/StoryRenderingTests.stories.tsx +0 -164
- 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
|
-
|
|
8
|
+
|
|
9
|
+
const FilterOrder: React.FC<FilterOrderProps> = ({ orderedValues, handleFilterOrder, onNestedDragAreaHover }) => {
|
|
8
10
|
return (
|
|
9
|
-
<
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
{
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
}
|
package/data/colorPalettes.ts
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
import { mapColorPalettes, mapColorPalettesV1, mapColorPalettesV2 } from './mapColorPalettes'
|
|
2
|
-
import {
|
|
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 {
|
|
15
|
+
export {
|
|
16
|
+
chartColorPalettes,
|
|
17
|
+
sequentialPalettes,
|
|
18
|
+
colorPalettesChart,
|
|
19
|
+
colorPalettesChartV1,
|
|
20
|
+
colorPalettesChartV2,
|
|
21
|
+
twoColorPalette
|
|
22
|
+
}
|
package/data/mapColorPalettes.ts
CHANGED
|
@@ -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>
|