@cdc/core 4.25.10 → 4.26.1

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 (134) hide show
  1. package/_stories/Gallery.Charts.stories.tsx +307 -0
  2. package/_stories/Gallery.DataBite.stories.tsx +72 -0
  3. package/_stories/Gallery.Maps.stories.tsx +230 -0
  4. package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
  5. package/_stories/PageART.stories.tsx +192 -0
  6. package/_stories/PageBRFSS.stories.tsx +289 -0
  7. package/_stories/PageCancerRegistries.stories.tsx +199 -0
  8. package/_stories/PageEasternEquineEncephalitis.stories.tsx +202 -0
  9. package/_stories/PageExcessiveAlcoholUse.stories.tsx +196 -0
  10. package/_stories/PageMaternalMortality.stories.tsx +192 -0
  11. package/_stories/PageOralHealth.stories.tsx +196 -0
  12. package/_stories/PageRespiratory.stories.tsx +332 -0
  13. package/_stories/PageSmokingTobacco.stories.tsx +195 -0
  14. package/_stories/PageStateDiabetesProfiles.stories.tsx +196 -0
  15. package/_stories/PageWastewater.stories.tsx +463 -0
  16. package/_stories/StoryRenderingTests.stories.tsx +164 -0
  17. package/assets/icon-magnifying-glass.svg +5 -0
  18. package/assets/icon-warming-stripes.svg +13 -0
  19. package/components/AdvancedEditor/AdvancedEditor.tsx +7 -1
  20. package/components/AdvancedEditor/EmbedEditor.tsx +281 -0
  21. package/components/ComboBox/ComboBox.tsx +345 -0
  22. package/components/ComboBox/combobox.styles.css +185 -0
  23. package/components/ComboBox/index.ts +1 -0
  24. package/components/CustomColorsEditor/CustomColorsEditor.css +299 -0
  25. package/components/CustomColorsEditor/CustomColorsEditor.tsx +209 -0
  26. package/components/CustomColorsEditor/index.ts +1 -0
  27. package/components/DataTable/DataTable.tsx +132 -58
  28. package/components/DataTable/DataTableStandAlone.tsx +8 -3
  29. package/components/DataTable/components/DataTableEditorPanel.tsx +12 -2
  30. package/components/DataTable/data-table.css +217 -210
  31. package/components/DataTable/helpers/mapCellMatrix.tsx +28 -9
  32. package/components/DataTable/helpers/standardizeState.js +2 -2
  33. package/components/DataTable/helpers/tests/standardizeState.test.js +54 -0
  34. package/components/EditorPanel/ColumnsEditor.tsx +37 -19
  35. package/components/EditorPanel/DataTableEditor.tsx +54 -28
  36. package/components/EditorPanel/EditorPanel.styles.css +439 -0
  37. package/components/EditorPanel/EditorPanel.tsx +144 -0
  38. package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
  39. package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
  40. package/components/EditorPanel/FootnotesEditor.tsx +44 -37
  41. package/components/EditorPanel/Inputs.tsx +44 -8
  42. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +35 -62
  43. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +246 -175
  44. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +61 -22
  45. package/components/EditorPanel/sections/VisualSection.tsx +169 -0
  46. package/components/Filters/Filters.tsx +57 -10
  47. package/components/Filters/components/Dropdown.tsx +6 -1
  48. package/components/Filters/helpers/getNestedOptions.ts +2 -1
  49. package/components/Filters/helpers/handleSorting.ts +1 -1
  50. package/components/Footnotes/Footnotes.tsx +35 -25
  51. package/components/Footnotes/FootnotesStandAlone.tsx +42 -6
  52. package/components/HeaderThemeSelector/HeaderThemeSelector.css +43 -0
  53. package/components/HeaderThemeSelector/HeaderThemeSelector.stories.tsx +74 -0
  54. package/components/HeaderThemeSelector/HeaderThemeSelector.tsx +61 -0
  55. package/components/HeaderThemeSelector/index.ts +2 -0
  56. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +82 -0
  57. package/components/Layout/components/Visualization/index.tsx +16 -1
  58. package/components/Layout/components/Visualization/visualizations.scss +7 -0
  59. package/components/Layout/styles/editor.scss +2 -1
  60. package/components/Legend/Legend.Gradient.tsx +1 -1
  61. package/components/Loader/Loader.tsx +1 -1
  62. package/components/MediaControls.tsx +63 -34
  63. package/components/PaletteConversionModal.tsx +7 -4
  64. package/components/PaletteSelector/PaletteSelector.css +49 -6
  65. package/components/Table/components/Cell.tsx +23 -2
  66. package/components/Table/components/Row.tsx +5 -3
  67. package/components/_stories/Filters.stories.tsx +20 -1
  68. package/components/_stories/Footnotes.CSV.stories.tsx +247 -0
  69. package/components/_stories/Footnotes.stories.tsx +768 -3
  70. package/components/_stories/Inputs.stories.tsx +2 -2
  71. package/components/_stories/styles.scss +0 -1
  72. package/components/ui/Accordion.jsx +1 -1
  73. package/components/ui/Icon.tsx +3 -1
  74. package/components/ui/Title/index.tsx +30 -2
  75. package/components/ui/Title/title.styles.css +42 -0
  76. package/components/ui/accordion.styles.css +57 -0
  77. package/data/chartColorPalettes.ts +1 -1
  78. package/dist/cove-main.css +75 -6
  79. package/dist/cove-main.css.map +1 -1
  80. package/generateViteConfig.js +8 -1
  81. package/helpers/addValuesToFilters.ts +11 -1
  82. package/helpers/constants.ts +37 -0
  83. package/helpers/cove/number.ts +33 -12
  84. package/helpers/coveUpdateWorker.ts +20 -11
  85. package/helpers/embedCodeGenerator.ts +109 -0
  86. package/helpers/fetchRemoteData.ts +3 -15
  87. package/helpers/getUniqueValues.ts +19 -0
  88. package/helpers/hashObj.ts +25 -0
  89. package/helpers/isRightAlignedTableValue.js +5 -0
  90. package/helpers/markupProcessor.ts +27 -12
  91. package/helpers/mergeCustomOrderValues.ts +37 -0
  92. package/helpers/metrics/helpers.ts +1 -0
  93. package/helpers/parseCsvWithQuotes.ts +65 -0
  94. package/helpers/pivotData.ts +2 -2
  95. package/helpers/prepareScreenshot.ts +268 -0
  96. package/helpers/queryStringUtils.ts +29 -0
  97. package/helpers/testing.ts +17 -4
  98. package/helpers/tests/prepareScreenshot.test.ts +414 -0
  99. package/helpers/tests/queryStringUtils.test.ts +381 -0
  100. package/helpers/tests/testStandaloneBuild.ts +23 -5
  101. package/helpers/useDataVizClasses.ts +0 -1
  102. package/helpers/ver/4.25.11.ts +13 -0
  103. package/helpers/ver/4.26.1.ts +80 -0
  104. package/helpers/viewports.ts +2 -0
  105. package/hooks/useDataColumns.ts +63 -0
  106. package/hooks/useFilterManagement.ts +94 -0
  107. package/hooks/useLegendSeparators.ts +26 -0
  108. package/hooks/useListManagement.ts +192 -0
  109. package/package.json +6 -4
  110. package/styles/_button-section.scss +0 -3
  111. package/styles/_common-components.css +73 -0
  112. package/styles/_global.scss +25 -5
  113. package/styles/base.scss +0 -50
  114. package/styles/cove-main.scss +3 -1
  115. package/styles/filters.scss +10 -3
  116. package/styles/v2/base/index.scss +0 -1
  117. package/styles/v2/components/editor.scss +14 -6
  118. package/styles/v2/utils/_breakpoints.scss +1 -1
  119. package/styles/v2/utils/index.scss +0 -1
  120. package/styles/waiting.scss +1 -1
  121. package/types/Axis.ts +1 -0
  122. package/types/ForecastingSeriesKey.ts +1 -0
  123. package/types/MarkupInclude.ts +5 -3
  124. package/types/MarkupVariable.ts +1 -1
  125. package/types/Series.ts +3 -0
  126. package/types/Table.ts +1 -0
  127. package/types/Visualization.ts +1 -0
  128. package/types/VizFilter.ts +2 -0
  129. package/LICENSE +0 -201
  130. package/styles/_mixins.scss +0 -13
  131. package/styles/_typography.scss +0 -0
  132. package/styles/v2/base/_typography.scss +0 -0
  133. package/styles/v2/components/guidance-block.scss +0 -74
  134. 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
+ }
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 512 512" fill="currentColor">
2
+ <!-- Outlined/Regular style magnifying glass -->
3
+ <path d="M208 48a160 160 0 1 1 0 320 160 160 0 1 1 0-320zm0 368c48.8 0 93.7-16.8 129.1-44.9L471 505c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9L371.1 337.1C399.2 301.7 416 256.8 416 208C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208z"/>
4
+ </svg>
5
+
@@ -0,0 +1,13 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
2
+ <rect x="32" y="64" width="32" height="384" fill="#000000"/>
3
+ <rect x="72" y="64" width="32" height="384" fill="#1a1a1a"/>
4
+ <rect x="112" y="64" width="32" height="384" fill="#333333"/>
5
+ <rect x="152" y="64" width="32" height="384" fill="#666666"/>
6
+ <rect x="192" y="64" width="32" height="384" fill="#999999"/>
7
+ <rect x="232" y="64" width="32" height="384" fill="#b3b3b3"/>
8
+ <rect x="272" y="64" width="32" height="384" fill="#cccccc"/>
9
+ <rect x="312" y="64" width="32" height="384" fill="#e6e6e6"/>
10
+ <rect x="352" y="64" width="32" height="384" fill="#f2f2f2"/>
11
+ <rect x="392" y="64" width="32" height="384" fill="#fafafa"/>
12
+ <rect x="432" y="64" width="32" height="384" fill="#ffffff"/>
13
+ </svg>
@@ -6,6 +6,7 @@ import { FilterFunction, JsonEditor, UpdateFunction } from 'json-edit-react'
6
6
  import './advanced-editor-styles.css'
7
7
  import _ from 'lodash'
8
8
  import Tooltip from '../ui/Tooltip'
9
+ import EmbedEditor from './EmbedEditor'
9
10
 
10
11
  export const AdvancedEditor = ({
11
12
  loadConfig,
@@ -59,7 +60,9 @@ export const AdvancedEditor = ({
59
60
  chart: ['Charts', 'https://www.cdc.gov/cove/index.html', <ChartIcon />],
60
61
  dashboard: ['Dashboard', 'https://www.cdc.gov/cove/index.html', <ChartIcon />],
61
62
  map: ['Maps', 'https://www.cdc.gov/cove/index.html', <MapIcon />],
62
- 'markup-include': ['Markup Include', 'https://www.cdc.gov/cove/index.html', <MarkupIncludeIcon />]
63
+ 'markup-include': ['Markup Include', 'https://www.cdc.gov/cove/index.html', <MarkupIncludeIcon />],
64
+ 'data-bite': ['Data Bite', 'https://www.cdc.gov/cove/index.html', <ChartIcon />],
65
+ 'waffle-chart': ['Waffle Chart', 'https://www.cdc.gov/cove/index.html', <ChartIcon />]
63
66
  }
64
67
 
65
68
  if (!config.type) return <></>
@@ -113,6 +116,9 @@ export const AdvancedEditor = ({
113
116
  </React.Fragment>
114
117
  )}
115
118
  </div>
119
+
120
+ {/* Share with Partners Section */}
121
+ <EmbedEditor config={config} />
116
122
  </>
117
123
  )
118
124
  }
@@ -0,0 +1,281 @@
1
+ import React, { useState, useEffect, useMemo } from 'react'
2
+ import { generateEmbedCode } from '../../helpers/embedCodeGenerator'
3
+
4
+ type EmbedEditorProps = {
5
+ config?: any // Current visualization config
6
+ }
7
+
8
+ /**
9
+ * EmbedEditor - Provides "Share with Partners" functionality
10
+ * Generates embed codes for iframe embedding of visualizations
11
+ */
12
+ export const EmbedEditor: React.FC<EmbedEditorProps> = ({ config }) => {
13
+ const [configUrl, setConfigUrl] = useState<string | null>(null)
14
+ const [showEmbedModal, setShowEmbedModal] = useState(false)
15
+ const [embedCode, setEmbedCode] = useState('')
16
+ const [embedCodeCopied, setEmbedCodeCopied] = useState(false)
17
+ const [isExpanded, setIsExpanded] = useState(false)
18
+
19
+ // Check if all filters have setByQueryParameter
20
+ const filtersAreValid = useMemo(() => {
21
+ if (!config) return true
22
+
23
+ // Check regular filters
24
+ const filters = config.filters || []
25
+ // Check dashboard shared filters
26
+ const sharedFilters = config.dashboard?.sharedFilters || []
27
+
28
+ const allFilters = [...filters, ...sharedFilters]
29
+
30
+ // If no filters, valid
31
+ if (allFilters.length === 0) return true
32
+
33
+ // All filters must have setByQueryParameter
34
+ return allFilters.every((filter: any) => !!filter.setByQueryParameter)
35
+ }, [config])
36
+
37
+ // Detect configUrl from WCMS permalink or use dev fallback
38
+ useEffect(() => {
39
+ // Try to get config URL from WCMS permalink element
40
+ const permalinkElement = document.querySelector('#sample-permalink') as HTMLAnchorElement
41
+
42
+ if (permalinkElement?.href) {
43
+ try {
44
+ // Parse the URL and extract just the pathname (strip host)
45
+ const url = new URL(permalinkElement.href)
46
+ const pathname = url.pathname
47
+ setConfigUrl(pathname)
48
+ } catch (err) {
49
+ console.warn('[EmbedEditor] Failed to parse permalink URL:', err)
50
+ }
51
+ } else {
52
+ // Check if we're in development mode
53
+ const isDevelopment =
54
+ process.env.NODE_ENV === 'development' ||
55
+ window.location.hostname === 'localhost' ||
56
+ window.location.hostname === '127.0.0.1'
57
+
58
+ if (isDevelopment) {
59
+ // Use fallback only in development
60
+ const fallbackUrl = '/examples/line-chart-states.json'
61
+ setConfigUrl(fallbackUrl)
62
+ } else {
63
+ // In production without permalink, don't show embed section
64
+ console.warn('[EmbedEditor] No permalink found and not in development mode')
65
+ setConfigUrl(null)
66
+ }
67
+ }
68
+ }, [])
69
+
70
+ // Handle showing embed code modal
71
+ const handleShowEmbedCode = () => {
72
+ if (!configUrl) {
73
+ alert('This visualization must be published before generating embed code.')
74
+ return
75
+ }
76
+
77
+ const code = generateEmbedCode({ configUrl })
78
+ setEmbedCode(code)
79
+ setShowEmbedModal(true)
80
+ setEmbedCodeCopied(false)
81
+ }
82
+
83
+ // Handle copying embed code from modal
84
+ const handleCopyFromModal = async () => {
85
+ try {
86
+ await navigator.clipboard.writeText(embedCode)
87
+ setEmbedCodeCopied(true)
88
+ setTimeout(() => setEmbedCodeCopied(false), 3000)
89
+ } catch (err) {
90
+ console.error('Failed to copy embed code:', err)
91
+ alert('Failed to copy to clipboard. Please copy manually.')
92
+ }
93
+ }
94
+
95
+ // Handle closing modal
96
+ const handleCloseModal = () => {
97
+ setShowEmbedModal(false)
98
+ setEmbedCodeCopied(false)
99
+ }
100
+
101
+ // Hide embed section until released
102
+ return null
103
+
104
+ return (
105
+ <>
106
+ {/* Collapsible Share with Partners Section */}
107
+ <div className='share-partners' style={{ padding: '0 1em 1em', textAlign: 'left' }}>
108
+ <span
109
+ className='advanced-toggle-link'
110
+ onClick={() => setIsExpanded(!isExpanded)}
111
+ style={{ paddingTop: '1em', display: 'block', cursor: 'pointer', textDecoration: 'underline' }}
112
+ >
113
+ <span
114
+ style={{ textDecoration: 'none', display: 'inline-block', fontFamily: 'monospace', paddingRight: '5px' }}
115
+ >
116
+ {isExpanded ? `— ` : `+ `}
117
+ </span>
118
+ Share with Partners
119
+ </span>
120
+
121
+ {isExpanded && (
122
+ <div style={{ paddingTop: '1em' }}>
123
+ {!configUrl ? (
124
+ <div
125
+ style={{
126
+ padding: '0.75em',
127
+ background: '#fff3cd',
128
+ border: '1px solid #ffc107',
129
+ borderRadius: '4px',
130
+ marginBottom: '0.5em'
131
+ }}
132
+ >
133
+ <p style={{ fontSize: '0.85em', margin: 0, color: '#856404' }}>
134
+ ⚠️ An embed code cannot be generated until this visualization has been saved.
135
+ </p>
136
+ </div>
137
+ ) : !filtersAreValid ? (
138
+ <div
139
+ style={{
140
+ padding: '0.75em',
141
+ background: '#fff3cd',
142
+ border: '1px solid #ffc107',
143
+ borderRadius: '4px',
144
+ marginBottom: '0.5em'
145
+ }}
146
+ >
147
+ <p style={{ fontSize: '0.85em', margin: '0 0 0.5em 0', fontWeight: 'bold', color: '#856404' }}>
148
+ ⚠️ Embed Code Not Available
149
+ </p>
150
+ <p style={{ fontSize: '0.85em', margin: 0, color: '#856404' }}>
151
+ To enable embedding, all filters must have the "Query String Parameter" field set. Some filters are
152
+ missing this field. After setting the field, make sure to save your visualization.
153
+ </p>
154
+ </div>
155
+ ) : (
156
+ <>
157
+ <p style={{ fontSize: '0.85em', marginBottom: '1em', color: '#666' }}>
158
+ Generate embed codes for partners to add this visualization to their websites. Your visualization will
159
+ need to be published to Link (www.cdc.gov) before it can be embedded by a partner.
160
+ </p>
161
+
162
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5em' }}>
163
+ <button
164
+ className='btn btn-primary'
165
+ onClick={handleShowEmbedCode}
166
+ style={{ width: '100%', textAlign: 'left' }}
167
+ >
168
+ Get Embed Code
169
+ </button>
170
+ </div>
171
+ </>
172
+ )}
173
+ </div>
174
+ )}
175
+ </div>
176
+
177
+ {/* Embed Code Modal */}
178
+ {showEmbedModal && (
179
+ <div
180
+ className='modal-overlay'
181
+ style={{
182
+ position: 'fixed',
183
+ top: 0,
184
+ left: 0,
185
+ right: 0,
186
+ bottom: 0,
187
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
188
+ display: 'flex',
189
+ alignItems: 'center',
190
+ justifyContent: 'center',
191
+ zIndex: 9999
192
+ }}
193
+ onClick={handleCloseModal}
194
+ >
195
+ <div
196
+ className='modal-content'
197
+ style={{
198
+ backgroundColor: 'white',
199
+ borderRadius: '8px',
200
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
201
+ maxWidth: '600px',
202
+ width: '90%',
203
+ margin: '20px'
204
+ }}
205
+ onClick={e => e.stopPropagation()}
206
+ >
207
+ <div
208
+ className='modal-header'
209
+ style={{
210
+ padding: '15px 20px',
211
+ borderBottom: '1px solid #e0e0e0',
212
+ backgroundColor: '#005eaa',
213
+ display: 'flex',
214
+ justifyContent: 'space-between',
215
+ alignItems: 'center',
216
+ borderRadius: '8px 8px 0 0'
217
+ }}
218
+ >
219
+ <h3 style={{ color: 'white', margin: 0 }}>Embed Code</h3>
220
+ <button
221
+ onClick={handleCloseModal}
222
+ style={{
223
+ background: 'transparent',
224
+ border: 'none',
225
+ color: 'white',
226
+ fontSize: '1.5em',
227
+ cursor: 'pointer',
228
+ padding: '0 5px',
229
+ lineHeight: 1
230
+ }}
231
+ aria-label='Close'
232
+ >
233
+ ×
234
+ </button>
235
+ </div>
236
+
237
+ <div className='modal-body' style={{ padding: '20px' }}>
238
+ <p style={{ marginBottom: '10px', color: '#666' }}>Copy this code and paste it into your website:</p>
239
+ <textarea
240
+ readOnly
241
+ value={embedCode}
242
+ style={{
243
+ width: '100%',
244
+ height: '180px',
245
+ fontFamily: 'monospace',
246
+ fontSize: '0.85em',
247
+ padding: '10px',
248
+ border: '1px solid #ddd',
249
+ borderRadius: '4px',
250
+ resize: 'vertical',
251
+ boxSizing: 'border-box'
252
+ }}
253
+ onFocus={e => e.target.select()}
254
+ />
255
+ </div>
256
+
257
+ <div
258
+ className='modal-footer'
259
+ style={{
260
+ padding: '15px 20px',
261
+ borderTop: '1px solid #e0e0e0',
262
+ display: 'flex',
263
+ justifyContent: 'flex-end',
264
+ gap: '10px'
265
+ }}
266
+ >
267
+ <button className='btn btn-secondary' onClick={handleCloseModal}>
268
+ Close
269
+ </button>
270
+ <button className='btn btn-primary' onClick={handleCopyFromModal} style={{ minWidth: '120px' }}>
271
+ {embedCodeCopied ? '✓ Copied!' : 'Copy to Clipboard'}
272
+ </button>
273
+ </div>
274
+ </div>
275
+ </div>
276
+ )}
277
+ </>
278
+ )
279
+ }
280
+
281
+ export default EmbedEditor