@explorer02/cfm-survey-sdk 0.2.2 → 0.2.4

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 (113) hide show
  1. package/dist/cli/index.js +140 -33
  2. package/dist/cli/index.mjs +140 -33
  3. package/dist/index.js +1 -1
  4. package/dist/index.mjs +1 -1
  5. package/package.json +8 -1
  6. package/postinstall.js +8 -5
  7. package/templates/AGENT.md +62 -17
  8. package/templates/deploy-wizard-to-s3.sh +52 -0
  9. package/templates/docs/00-integration/agent-execution-flow.md +281 -0
  10. package/templates/docs/00-integration/agent-operating-contract.md +260 -0
  11. package/templates/docs/00-integration/analytics-events-catalog.md +54 -0
  12. package/templates/docs/00-integration/apply-ui-config.md +365 -0
  13. package/templates/docs/00-integration/canonical-survey-fixtures.md +94 -0
  14. package/templates/docs/00-integration/client-integration-guide.md +10 -8
  15. package/templates/docs/00-integration/client-lib-folder.md +115 -0
  16. package/templates/docs/00-integration/component-checklist.md +90 -54
  17. package/templates/docs/00-integration/constraints.md +13 -5
  18. package/templates/docs/00-integration/custom-field-logic-and-navigation.md +56 -0
  19. package/templates/docs/00-integration/display-logic-and-navigation.md +16 -1
  20. package/templates/docs/00-integration/file-upload-aws.md +116 -0
  21. package/templates/docs/00-integration/logic-fields-catalog.md +105 -0
  22. package/templates/docs/00-integration/partial-save-and-recovery.md +24 -0
  23. package/templates/docs/00-integration/progress.md +4 -2
  24. package/templates/docs/00-integration/question-type-sdk-matrix.md +11 -9
  25. package/templates/docs/00-integration/setup.md +15 -3
  26. package/templates/docs/00-integration/skip-logic-and-navigation.md +1 -1
  27. package/templates/docs/00-integration/survey-lifecycle-analytics.md +1 -1
  28. package/templates/docs/00-integration/ui-customization-wizard.md +144 -0
  29. package/templates/docs/00-integration/useSurveySDK.md +35 -1
  30. package/templates/docs/00-integration/wizard-api.md +134 -0
  31. package/templates/docs/00-integration/wizard-config-handoff.md +246 -0
  32. package/templates/docs/00-integration/wizard-preview-build-guide.md +400 -0
  33. package/templates/docs/00-integration/wizard-question-type-styling.md +195 -0
  34. package/templates/docs/01-components/01-survey-page.md +18 -8
  35. package/templates/docs/01-components/02-question.md +2 -2
  36. package/templates/docs/01-components/03-rating-scale.md +12 -18
  37. package/templates/docs/01-components/05-csat-matrix-scale.md +18 -26
  38. package/templates/docs/01-components/06-likert-matrix-scale.md +19 -38
  39. package/templates/docs/01-components/07-slider-matrix-scale.md +48 -33
  40. package/templates/docs/01-components/08-file-upload-scale.md +15 -52
  41. package/templates/docs/01-components/09-custom-slider-track.md +15 -25
  42. package/templates/docs/01-components/10-header-footer.md +23 -4
  43. package/templates/docs/01-components/11-progress-bar.md +14 -6
  44. package/templates/docs/01-components/13-matrix-dropdown.md +28 -33
  45. package/templates/docs/01-components/17-heatmap-scale.md +4 -4
  46. package/templates/docs/01-components/18-rank-order-scale.md +24 -31
  47. package/templates/docs/01-components/19-survey-sticky-chrome.md +79 -0
  48. package/templates/docs/01-components/README.md +2 -2
  49. package/templates/docs/02-reference/question-types/10-slider-matrix.md +38 -26
  50. package/templates/docs/02-reference/question-types/11-file-upload.md +36 -21
  51. package/templates/docs/02-reference/question-types/README.md +11 -1
  52. package/templates/docs/02-reference/routing-table.md +1 -0
  53. package/templates/docs/02-reference/value-derivation.md +1 -1
  54. package/templates/docs/03-ui-specs/00-question-shell.md +10 -0
  55. package/templates/docs/03-ui-specs/01-rating.md +28 -2
  56. package/templates/docs/03-ui-specs/02-radio.md +94 -24
  57. package/templates/docs/03-ui-specs/03-text.md +12 -1
  58. package/templates/docs/03-ui-specs/04-csat.md +90 -26
  59. package/templates/docs/03-ui-specs/06-slider.md +3 -1
  60. package/templates/docs/03-ui-specs/07-matrix-cfm.md +74 -35
  61. package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +50 -16
  62. package/templates/docs/03-ui-specs/09-slider-matrix.md +70 -26
  63. package/templates/docs/03-ui-specs/10-file-upload.md +52 -20
  64. package/templates/docs/03-ui-specs/11-text-and-media.md +4 -0
  65. package/templates/docs/03-ui-specs/12-survey-chrome.md +92 -5
  66. package/templates/docs/03-ui-specs/13-heatmap.md +21 -3
  67. package/templates/docs/03-ui-specs/14-rank-order.md +108 -28
  68. package/templates/docs/03-ui-specs/README.md +11 -7
  69. package/templates/docs/03-ui-specs/shared/custom-slider-track.md +28 -35
  70. package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +1 -1
  71. package/templates/docs/MANIFEST.json +187 -6
  72. package/templates/docs/index.md +116 -116
  73. package/templates/docs/templates/CsatMatrixScale.tsx +637 -0
  74. package/templates/docs/templates/CustomSliderTrack.tsx +144 -0
  75. package/templates/docs/templates/FileUploadScale.tsx +262 -0
  76. package/templates/docs/templates/HeatmapScale.tsx +114 -0
  77. package/templates/docs/templates/LikertMatrixScale.tsx +414 -0
  78. package/templates/docs/templates/MatrixDropdown.tsx +216 -0
  79. package/templates/docs/templates/Question.tsx +3 -0
  80. package/templates/docs/templates/RankOrderScale.tsx +353 -0
  81. package/templates/docs/templates/RatingScale.tsx +129 -0
  82. package/templates/docs/templates/SliderMatrixScale.tsx +248 -0
  83. package/templates/docs/templates/SurveyStickyChrome.tsx +24 -0
  84. package/templates/docs/templates/customFieldValues.ts +36 -0
  85. package/templates/docs/templates/implementation_plan.md +75 -8
  86. package/templates/docs/templates/labelStyles.ts +33 -0
  87. package/templates/docs/templates/selectionStyles.ts +100 -0
  88. package/templates/docs/templates/survey-inventory.schema.json +28 -23
  89. package/templates/docs/templates/surveyUiIcons.tsx +63 -0
  90. package/templates/docs/templates/surveyUiScaleUtils.ts +51 -0
  91. package/templates/docs/templates/verify-agent-build.sh +151 -0
  92. package/templates/preview-harness/preview-bridge.inline.js +201 -0
  93. package/templates/preview-harness/previewPages.js +108 -0
  94. package/templates/preview-harness/previewPages.ts +298 -0
  95. package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +67 -0
  96. package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +122 -0
  97. package/templates/preview-harness/vite-app/src/SurveyPagePreview.tsx +75 -0
  98. package/templates/preview-harness/vite-app/src/fixtures/questions.ts +311 -0
  99. package/templates/preview-harness/vite-app/src/globals.css +16 -0
  100. package/templates/preview-harness/vite-app/src/mount.tsx +9 -0
  101. package/templates/preview-harness/vite-app/src/preview-live-overrides.css +101 -0
  102. package/templates/preview-harness/vite-app/stubs/next-dynamic.tsx +25 -0
  103. package/templates/preview-harness/vite-app/stubs/next-image.tsx +29 -0
  104. package/templates/previewBridge.ts +153 -0
  105. package/templates/survey-theme.css +295 -0
  106. package/templates/survey-ui-config.schema.json +213 -0
  107. package/templates/wizard-dist/assets/PreviewMock-DgHfrVeb.js +1 -0
  108. package/templates/wizard-dist/assets/TypePanel-CFVC3Ptn.js +1 -0
  109. package/templates/wizard-dist/assets/index-BhWM50Yu.css +1 -0
  110. package/templates/wizard-dist/assets/index-DYK3X1e5.js +34 -0
  111. package/templates/wizard-dist/assets/vendor-BwkXDkd3.js +17 -0
  112. package/templates/wizard-dist/index.html +20 -0
  113. package/templates/wizard.html +13 -1129
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import {
5
+ FaFaceAngry,
6
+ FaFaceSadTear,
7
+ FaFaceFrown,
8
+ FaFaceFrownOpen,
9
+ FaFaceMeh,
10
+ FaFaceSmile,
11
+ FaFaceSmileBeam,
12
+ FaFaceGrin,
13
+ FaFaceGrinWide,
14
+ FaFaceGrinStars,
15
+ FaStar,
16
+ } from 'react-icons/fa6';
17
+ import { FaRegStar } from 'react-icons/fa';
18
+
19
+ const EMOJI_COLORS = [
20
+ '#dc2626', '#e04832', '#ea580c', '#f59e0b', '#eab308',
21
+ '#a3a323', '#84cc16', '#65a30d', '#22c55e', '#16a34a', '#059669',
22
+ ] as const;
23
+
24
+ const FACE_ICONS = [
25
+ FaFaceAngry, FaFaceSadTear, FaFaceFrown, FaFaceFrownOpen, FaFaceMeh, FaFaceMeh,
26
+ FaFaceSmile, FaFaceSmileBeam, FaFaceGrin, FaFaceGrinWide, FaFaceGrinStars,
27
+ ] as const;
28
+
29
+ const CsatEmojiSet: Record<number, React.ReactNode> = {};
30
+ for (let i = 0; i < 11; i++) {
31
+ const Icon = FACE_ICONS[i];
32
+ CsatEmojiSet[i + 1] = React.createElement(Icon as React.ElementType, {
33
+ className: 'transition-transform',
34
+ style: { color: EMOJI_COLORS[i], fontSize: '28px' },
35
+ });
36
+ }
37
+
38
+ const EMOJI_SCALES: Record<number, number[]> = {
39
+ 3: [3, 6, 8],
40
+ 4: [3, 4, 7, 9],
41
+ 5: [3, 4, 6, 7, 9],
42
+ 6: [3, 4, 6, 7, 9, 11],
43
+ };
44
+
45
+ export function getEmojiForIndex(index: number, total: number): React.ReactNode {
46
+ if (total <= 1) return CsatEmojiSet[6];
47
+ if (EMOJI_SCALES[total]) {
48
+ const key = EMOJI_SCALES[total][index];
49
+ if (key) return CsatEmojiSet[key];
50
+ }
51
+ const percent = index / (total - 1);
52
+ const key = Math.min(11, Math.max(1, Math.round(percent * 10) + 1));
53
+ return CsatEmojiSet[key];
54
+ }
55
+
56
+ export const CsatStarIcons: { filled: React.ReactNode; empty: React.ReactNode } = {
57
+ filled: React.createElement(FaStar as React.ElementType, {
58
+ style: { color: '#e20074', fontSize: '28px' },
59
+ }),
60
+ empty: React.createElement(FaRegStar as React.ElementType, {
61
+ style: { color: '#d1d5db', fontSize: '28px', stroke: '#d1d5db', strokeWidth: '16px' },
62
+ }),
63
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Portable scale utilities — copy to src/lib/surveyUiScaleUtils.ts or co-locate with components.
3
+ */
4
+ import type { NpsScalePoint, ScaleColumn } from '@explorer02/cfm-survey-sdk';
5
+
6
+ export const SCALE_COLORS: readonly string[] = [
7
+ '#e2001a', '#e4251b', '#ec610a', '#f18b00', '#f7b200', '#fcd900',
8
+ '#d2dc00', '#9fca00', '#6bb300', '#339a00', '#008000',
9
+ ] as const;
10
+
11
+ export function getAccentColor(value: number): string {
12
+ return SCALE_COLORS[value] ?? '#e20074';
13
+ }
14
+
15
+ export function tickColorFromIndex(index: number, total: number): string {
16
+ if (total <= 1) return '#e20074';
17
+ const hue = Math.round((index / (total - 1)) * 120);
18
+ return `hsl(${hue}, 90%, 40%)`;
19
+ }
20
+
21
+ export function nonNaScaleColumns(columns: ScaleColumn[]): ScaleColumn[] {
22
+ return columns.filter(col => col.id !== 'na');
23
+ }
24
+
25
+ export function starValueFromOption(option: NpsScalePoint, index: number): number {
26
+ const numeric = Number(option.optionLabel);
27
+ return Number.isFinite(numeric) && option.optionLabel.trim() !== '' ? numeric : index;
28
+ }
29
+
30
+ export function npsValueFromOption(option: NpsScalePoint, index: number): number {
31
+ const numeric = Number(option.optionLabel);
32
+ return Number.isFinite(numeric) && option.optionLabel.trim() !== '' ? numeric : index;
33
+ }
34
+
35
+ /** Matches SDK matrixColumnStoredValue — scale-matrix answers store column ids. */
36
+ export function matrixColumnStoredValue(column: { id: string; label: string }): string | null {
37
+ return column.id === 'na' ? null : column.id;
38
+ }
39
+
40
+ /** @deprecated Use matrixColumnStoredValue */
41
+ export function columnSubmitValue(column: { id: string; label: string }): string | null {
42
+ return matrixColumnStoredValue(column);
43
+ }
44
+
45
+ export function columnAccentColor(column: { id: string; label: string }): string | undefined {
46
+ const numeric = Number(column.label);
47
+ if (Number.isFinite(numeric) && column.label.trim() !== '') {
48
+ return getAccentColor(numeric);
49
+ }
50
+ return undefined;
51
+ }
@@ -20,6 +20,58 @@ if [ -n "$INTERNAL_IMPORTS" ]; then
20
20
  fi
21
21
  echo "OK: No internal SDK imports"
22
22
 
23
+ # Wizard-ready artifacts (Phase 5 — mandatory)
24
+ THEME_CSS=""
25
+ for candidate in src/styles/survey-theme.css styles/survey-theme.css app/globals.css; do
26
+ if [ -f "$candidate" ] && grep -q "cfm-" "$candidate" 2>/dev/null; then
27
+ THEME_CSS="$candidate"
28
+ break
29
+ fi
30
+ done
31
+ if [ -z "$THEME_CSS" ]; then
32
+ for candidate in src/styles/survey-theme.css styles/survey-theme.css; do
33
+ if [ -f "$candidate" ]; then THEME_CSS="$candidate"; break; fi
34
+ done
35
+ fi
36
+ if [ -z "$THEME_CSS" ]; then
37
+ echo "FAIL: src/styles/survey-theme.css missing — copy from docs/templates/survey-theme.css"
38
+ exit 1
39
+ fi
40
+ echo "OK: survey-theme.css present ($THEME_CSS)"
41
+
42
+ SEL_STYLES=""
43
+ for candidate in src/lib/surveyUi/selectionStyles.ts lib/surveyUi/selectionStyles.ts; do
44
+ if [ -f "$candidate" ]; then SEL_STYLES="$candidate"; break; fi
45
+ done
46
+ if [ -z "$SEL_STYLES" ]; then
47
+ echo "FAIL: selectionStyles.ts missing — copy from docs/templates/selectionStyles.ts"
48
+ exit 1
49
+ fi
50
+ echo "OK: selectionStyles.ts present ($SEL_STYLES)"
51
+
52
+ LBL_STYLES=""
53
+ for candidate in src/lib/surveyUi/labelStyles.ts lib/surveyUi/labelStyles.ts; do
54
+ if [ -f "$candidate" ]; then LBL_STYLES="$candidate"; break; fi
55
+ done
56
+ if [ -z "$LBL_STYLES" ]; then
57
+ echo "FAIL: labelStyles.ts missing — copy from docs/templates/labelStyles.ts"
58
+ exit 1
59
+ fi
60
+ echo "OK: labelStyles.ts present ($LBL_STYLES)"
61
+
62
+ if [ ! -f "./survey-ui-config.json" ]; then
63
+ echo "FAIL: ./survey-ui-config.json missing — draft config required for wizard seed"
64
+ exit 1
65
+ fi
66
+ echo "OK: survey-ui-config.json present"
67
+
68
+ CHROME_HOOKS=$(grep -rE "data-cfm-(logo|btn-next|survey-title)" src/ components/ app/ 2>/dev/null || true)
69
+ if [ -z "$CHROME_HOOKS" ]; then
70
+ echo "FAIL: No data-cfm-* chrome hooks found — see wizard-preview-build-guide.md"
71
+ exit 1
72
+ fi
73
+ echo "OK: data-cfm-* chrome hooks present"
74
+
23
75
  REQUIRED_TYPES=(MCQ TEXTFIELD NPS_SCALE CFM_MATRIX CSAT_MATRIX RATING_MATRIX SLIDER_MATRIX FILE_UPLOAD TEXT_AND_MEDIA HEATMAP RANK_ORDER)
24
76
  QUESTION_FILE=""
25
77
  for candidate in src/components/Question.tsx components/Question.tsx app/components/Question.tsx; do
@@ -46,6 +98,105 @@ else
46
98
  exit 1
47
99
  fi
48
100
  echo "OK: All 11 question.type branches present"
101
+
102
+ if grep -q "RANK_ORDER" "$QUESTION_FILE" || grep -q "RankOrderScale" "$QUESTION_FILE"; then
103
+ RANK_FILE=""
104
+ for candidate in src/components/RankOrderScale.tsx components/RankOrderScale.tsx app/components/RankOrderScale.tsx; do
105
+ if [ -f "$candidate" ]; then RANK_FILE="$candidate"; break; fi
106
+ done
107
+ if [ -z "$RANK_FILE" ]; then
108
+ echo "FAIL: RankOrderScale.tsx required when RANK_ORDER is wired in Question.tsx"
109
+ exit 1
110
+ fi
111
+ echo "OK: RankOrderScale.tsx exists ($RANK_FILE)"
112
+ if ! grep -q "@dnd-kit/core" package.json 2>/dev/null; then
113
+ echo "FAIL: @dnd-kit/core missing from package.json (required for RANK_ORDER)"
114
+ exit 1
115
+ fi
116
+ echo "OK: @dnd-kit in package.json"
117
+ if ! grep -q "getVisibleRankOrderOptionsForAnswers" "$QUESTION_FILE"; then
118
+ echo "WARN: getVisibleRankOrderOptionsForAnswers not found in Question.tsx"
119
+ fi
120
+ fi
121
+
122
+ if grep -q "FILE_UPLOAD" "$QUESTION_FILE" || grep -q "FileUploadScale" "$QUESTION_FILE"; then
123
+ UPLOAD_FILE=""
124
+ for candidate in src/components/FileUploadScale.tsx components/FileUploadScale.tsx app/components/FileUploadScale.tsx; do
125
+ if [ -f "$candidate" ]; then UPLOAD_FILE="$candidate"; break; fi
126
+ done
127
+ if [ -z "$UPLOAD_FILE" ]; then
128
+ echo "FAIL: FileUploadScale.tsx required when FILE_UPLOAD is wired"
129
+ exit 1
130
+ fi
131
+ echo "OK: FileUploadScale.tsx exists ($UPLOAD_FILE)"
132
+ fi
133
+
134
+ require_scale_file() {
135
+ local type_pattern="$1"
136
+ local scale_name="$2"
137
+ if grep -q "$type_pattern" "$QUESTION_FILE" || grep -q "$scale_name" "$QUESTION_FILE"; then
138
+ local SCALE_FILE=""
139
+ for candidate in "src/components/${scale_name}.tsx" "components/${scale_name}.tsx" "app/components/${scale_name}.tsx"; do
140
+ if [ -f "$candidate" ]; then SCALE_FILE="$candidate"; break; fi
141
+ done
142
+ if [ -z "$SCALE_FILE" ]; then
143
+ echo "FAIL: ${scale_name}.tsx required when ${type_pattern} is wired in Question.tsx"
144
+ exit 1
145
+ fi
146
+ echo "OK: ${scale_name}.tsx exists ($SCALE_FILE)"
147
+ fi
148
+ }
149
+
150
+ require_scale_file "SLIDER_MATRIX" "SliderMatrixScale"
151
+ require_scale_file "NPS_SCALE" "RatingScale"
152
+ require_scale_file "CFM_MATRIX" "LikertMatrixScale"
153
+ require_scale_file "CSAT_MATRIX" "CsatMatrixScale"
154
+ require_scale_file "RATING_MATRIX" "CsatMatrixScale"
155
+ require_scale_file "HEATMAP" "HeatmapScale"
156
+
157
+ if grep -qE "SLIDER_MATRIX|displayStyle.*graphics|RATING_MATRIX" "$QUESTION_FILE" 2>/dev/null; then
158
+ TRACK_FILE=""
159
+ for candidate in src/components/CustomSliderTrack.tsx components/CustomSliderTrack.tsx app/components/CustomSliderTrack.tsx; do
160
+ if [ -f "$candidate" ]; then TRACK_FILE="$candidate"; break; fi
161
+ done
162
+ if [ -z "$TRACK_FILE" ]; then
163
+ echo "FAIL: CustomSliderTrack.tsx required for SLIDER_MATRIX or graphics matrix"
164
+ exit 1
165
+ fi
166
+ echo "OK: CustomSliderTrack.tsx exists ($TRACK_FILE)"
167
+ fi
168
+
169
+ if grep -q "getVisibleMcqOptionsForAnswers\|getVisibleMatrixItemsForAnswers" "$QUESTION_FILE"; then
170
+ if ! grep -q "customFieldValues" "$QUESTION_FILE"; then
171
+ echo "WARN: Question.tsx uses visibility helpers but may not pass customFieldValues 4th arg"
172
+ else
173
+ echo "OK: Question.tsx references customFieldValues for visibility helpers"
174
+ fi
175
+ fi
176
+ fi
177
+
178
+ SURVEY_PAGE=""
179
+ for candidate in src/components/SurveyPage.tsx components/SurveyPage.tsx app/components/SurveyPage.tsx; do
180
+ if [ -f "$candidate" ]; then SURVEY_PAGE="$candidate"; break; fi
181
+ done
182
+
183
+ if [ -n "$SURVEY_PAGE" ]; then
184
+ if grep -q "customFieldValues" "$SURVEY_PAGE" || grep -q "CUSTOM_FIELD_VALUES" "$SURVEY_PAGE"; then
185
+ echo "OK: SurveyPage wires customFieldValues or CUSTOM_FIELD_VALUES"
186
+ if grep -q "CUSTOM_FIELD_VALUES" "$SURVEY_PAGE"; then
187
+ LIB_FILE=""
188
+ for candidate in src/lib/customFieldValues.ts lib/customFieldValues.ts; do
189
+ if [ -f "$candidate" ]; then LIB_FILE="$candidate"; break; fi
190
+ done
191
+ if [ -z "$LIB_FILE" ]; then
192
+ echo "WARN: SurveyPage imports CUSTOM_FIELD_VALUES but src/lib/customFieldValues.ts not found"
193
+ else
194
+ echo "OK: customFieldValues lib file exists ($LIB_FILE)"
195
+ fi
196
+ fi
197
+ else
198
+ echo "WARN: SurveyPage found but no customFieldValues wiring — required when survey uses CUSTOM_FIELD logic"
199
+ fi
49
200
  fi
50
201
 
51
202
  echo "Running npm run build..."
@@ -0,0 +1,201 @@
1
+ (function () {
2
+ var SNAPSHOT = 'CFM_UI_CONFIG_SNAPSHOT';
3
+ var DELTA = 'CFM_UI_CONFIG_DELTA';
4
+ var LEGACY = 'CFM_UI_CONFIG';
5
+ var rafId = null;
6
+ var pending = {};
7
+ var liveEdit = false;
8
+
9
+ function flush() {
10
+ rafId = null;
11
+ var root = document.documentElement;
12
+ for (var k in pending) {
13
+ if (Object.prototype.hasOwnProperty.call(pending, k)) {
14
+ if (root.style.getPropertyValue(k) !== pending[k]) {
15
+ root.style.setProperty(k, pending[k]);
16
+ }
17
+ delete pending[k];
18
+ }
19
+ }
20
+ }
21
+
22
+ function queue(vars) {
23
+ for (var k in vars) {
24
+ if (Object.prototype.hasOwnProperty.call(vars, k)) pending[k] = vars[k];
25
+ }
26
+ if (rafId == null) rafId = requestAnimationFrame(flush);
27
+ }
28
+
29
+ function applyImmediate(vars) {
30
+ var root = document.documentElement;
31
+ for (var k in vars) {
32
+ if (Object.prototype.hasOwnProperty.call(vars, k)) {
33
+ root.style.setProperty(k, vars[k]);
34
+ }
35
+ }
36
+ }
37
+
38
+ function setDisplay(selector, visible) {
39
+ document.querySelectorAll(selector).forEach(function (el) {
40
+ el.style.display = visible ? '' : 'none';
41
+ });
42
+ }
43
+
44
+ function applyContent(patch) {
45
+ if (!patch) return;
46
+ if (patch.logoUrl !== undefined) {
47
+ document.querySelectorAll('[data-cfm-logo], header img, footer img').forEach(function (el) {
48
+ var img = el;
49
+ if (patch.logoUrl) {
50
+ img.src = patch.logoUrl;
51
+ img.style.display = '';
52
+ } else if (el.hasAttribute('data-cfm-logo')) {
53
+ img.style.display = 'none';
54
+ }
55
+ });
56
+ document.querySelectorAll('[data-cfm-logo-text]').forEach(function (el) {
57
+ if (patch.logoUrl) {
58
+ el.style.display = 'none';
59
+ } else {
60
+ el.style.display = '';
61
+ el.classList.remove('hidden');
62
+ }
63
+ });
64
+ }
65
+ if (patch.companyName !== undefined) {
66
+ document.querySelectorAll('[data-cfm-company]').forEach(function (el) {
67
+ el.textContent = patch.companyName || '';
68
+ });
69
+ document.querySelectorAll('[data-cfm-logo-text]').forEach(function (el) {
70
+ var logoImg = document.querySelector('[data-cfm-logo]');
71
+ var logoHidden = !logoImg || logoImg.style.display === 'none';
72
+ if (logoHidden) el.textContent = patch.companyName || el.textContent;
73
+ });
74
+ }
75
+ if (patch.surveyTitle !== undefined) {
76
+ document.querySelectorAll('[data-cfm-survey-title]').forEach(function (el) {
77
+ el.textContent = patch.surveyTitle || '';
78
+ });
79
+ }
80
+ if (patch.browserTabTitle !== undefined && patch.browserTabTitle) {
81
+ document.title = patch.browserTabTitle;
82
+ }
83
+ if (patch.thankYouMessage !== undefined) {
84
+ document.querySelectorAll('[data-cfm-thank-you]').forEach(function (el) {
85
+ el.textContent = patch.thankYouMessage || '';
86
+ });
87
+ }
88
+ if (patch.buttonLabels && patch.buttonLabels.next) {
89
+ document.querySelectorAll('[data-cfm-btn-next]').forEach(function (el) {
90
+ el.textContent = patch.buttonLabels.next;
91
+ });
92
+ }
93
+ if (patch.buttonLabels && patch.buttonLabels.back) {
94
+ document.querySelectorAll('[data-cfm-btn-back]').forEach(function (el) {
95
+ el.textContent = patch.buttonLabels.back;
96
+ });
97
+ }
98
+ if (patch.copyrightText !== undefined) {
99
+ document.querySelectorAll('[data-cfm-copyright]').forEach(function (el) {
100
+ el.textContent = patch.copyrightText || '';
101
+ });
102
+ }
103
+ if (patch.footerLinks) {
104
+ var nav = document.querySelector('[data-cfm-footer-links]');
105
+ if (nav) {
106
+ nav.innerHTML = patch.footerLinks
107
+ .map(function (l) {
108
+ return (
109
+ '<a href="' +
110
+ l.url +
111
+ '" class="cfm-footer-link" target="_blank" rel="noopener noreferrer">' +
112
+ l.label +
113
+ '</a>'
114
+ );
115
+ })
116
+ .join('');
117
+ }
118
+ }
119
+ if (patch.layoutFlags) {
120
+ var flags = patch.layoutFlags;
121
+ if (flags.showCompanyName !== undefined) {
122
+ setDisplay('[data-cfm-company]', flags.showCompanyName);
123
+ }
124
+ if (flags.showHeader !== undefined) {
125
+ setDisplay('.cfm-header, header.cfm-header, .sticky.top-0', flags.showHeader);
126
+ }
127
+ if (flags.showFooter !== undefined) {
128
+ setDisplay('.cfm-footer, footer.cfm-footer', flags.showFooter);
129
+ }
130
+ if (flags.showProgressBar !== undefined) {
131
+ setDisplay('.cfm-progress, [data-cfm-progress]', flags.showProgressBar);
132
+ }
133
+ if (flags.showLanguageSwitcher !== undefined) {
134
+ setDisplay('[data-cfm-language]', flags.showLanguageSwitcher);
135
+ }
136
+ if (flags.showNextButton !== undefined) {
137
+ setDisplay('[data-cfm-btn-next]', flags.showNextButton);
138
+ }
139
+ if (flags.showBackButton !== undefined) {
140
+ setDisplay('[data-cfm-btn-back]', flags.showBackButton);
141
+ }
142
+ if (flags.showQuestionNumbers !== undefined) {
143
+ setDisplay('[data-cfm-question-number]', flags.showQuestionNumbers);
144
+ }
145
+ if (flags.showRequiredAsterisk !== undefined) {
146
+ setDisplay('[data-cfm-required]', flags.showRequiredAsterisk);
147
+ }
148
+ if (flags.showFooterLogo !== undefined) {
149
+ setDisplay('[data-cfm-footer-logo]', flags.showFooterLogo);
150
+ }
151
+ }
152
+ if (patch.domAttributes) {
153
+ var root = document.documentElement;
154
+ for (var attr in patch.domAttributes) {
155
+ if (Object.prototype.hasOwnProperty.call(patch.domAttributes, attr)) {
156
+ root.setAttribute(attr, patch.domAttributes[attr]);
157
+ }
158
+ }
159
+ }
160
+ if (patch.previewState && patch.previewState.textfieldPlaceholder !== undefined) {
161
+ document.querySelectorAll('[data-cfm-textfield]').forEach(function (el) {
162
+ el.setAttribute('placeholder', patch.previewState.textfieldPlaceholder || '');
163
+ });
164
+ }
165
+ }
166
+
167
+ function handleMessage(event) {
168
+ var data = event.data;
169
+ if (!data || !data.type) return;
170
+ if (data.type !== SNAPSHOT && data.type !== DELTA && data.type !== LEGACY) return;
171
+
172
+ if (!liveEdit) {
173
+ liveEdit = true;
174
+ document.documentElement.classList.add('cfm-live-edit');
175
+ }
176
+
177
+ if (data.cssVars) {
178
+ if (data.type === DELTA) queue(data.cssVars);
179
+ else applyImmediate(data.cssVars);
180
+ }
181
+
182
+ if (data.contentPatch) {
183
+ applyContent(data.contentPatch);
184
+ }
185
+
186
+ if (data.type === LEGACY && data.cssVars) {
187
+ applyImmediate(data.cssVars);
188
+ }
189
+ }
190
+
191
+ window.addEventListener('message', handleMessage);
192
+
193
+ try {
194
+ var channel = new BroadcastChannel('cfm-wizard-preview');
195
+ channel.addEventListener('message', function (event) {
196
+ handleMessage(event);
197
+ });
198
+ } catch (e) {
199
+ /* BroadcastChannel unavailable */
200
+ }
201
+ })();
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ const sharedStyles = `
4
+ .cfm-header { height:var(--cfm-header-height); background:var(--cfm-header-bg); border-bottom:1px solid var(--cfm-header-border); display:flex; align-items:center; padding:0 var(--cfm-header-padding-x); box-shadow:0 2px 8px rgba(0,0,0,0.06); }
5
+ .cfm-header-logo { width:var(--cfm-header-logo-width); height:var(--cfm-header-logo-height); object-fit:contain; padding:var(--cfm-header-logo-padding); background:var(--cfm-primary); }
6
+ .cfm-header-company { font-size:var(--cfm-header-company-size); font-weight:var(--cfm-header-company-weight); color:var(--cfm-header-company-color); margin-left:16px; }
7
+ .cfm-footer { background:var(--cfm-footer-bg); color:var(--cfm-footer-text); padding:var(--cfm-footer-padding-y) var(--cfm-footer-padding-x); display:flex; flex-wrap:wrap; gap:16px; justify-content:space-between; align-items:flex-end; }
8
+ .cfm-footer-links { display:flex; gap:24px; flex-wrap:wrap; }
9
+ .cfm-footer-links a { color:var(--cfm-footer-link); text-decoration:none; font-size:12px; }
10
+ .cfm-progress { height:var(--cfm-progress-height); background:var(--cfm-progress-track); border-radius:999px; overflow:hidden; margin:var(--cfm-sticky-pt) 0 var(--cfm-sticky-pb); }
11
+ .cfm-progress-fill { height:100%; width:45%; background:var(--cfm-progress-fill); border-radius:999px; }
12
+ .cfm-mcq-option { border:2px solid #e5e7eb; border-radius:var(--cfm-mcq-border-radius); padding:var(--cfm-mcq-card-padding); margin-bottom:var(--cfm-mcq-option-gap); display:flex; align-items:center; gap:8px; }
13
+ .cfm-mcq-option.selected { border-color:var(--cfm-mcq-selected-accent); background:var(--cfm-mcq-selected-bg); }
14
+ .cfm-nps-row { display:flex; gap:var(--cfm-nps-cell-gap); flex-wrap:wrap; }
15
+ .cfm-nps-cell { width:var(--cfm-nps-cell-size); height:var(--cfm-nps-cell-size); border-radius:var(--cfm-border-radius); background:var(--cfm-nps-track-bar); display:flex; align-items:center; justify-content:center; font-size:14px; }
16
+ .cfm-nps-cell.selected { background:var(--cfm-nps-selected-fill); border:2px solid var(--cfm-accent); }
17
+ .cfm-matrix-row { display:flex; align-items:center; padding:8px 0; background:var(--cfm-row-band); margin-bottom:4px; }
18
+ .cfm-matrix-label { width:var(--cfm-matrix-row-width); flex-shrink:0; padding:0 16px; font-size:14px; font-weight:500; }
19
+ .cfm-matrix-cells { display:flex; gap:8px; flex:1; justify-content:center; }
20
+ .cfm-matrix-cell { width:var(--cfm-csat-cell-size); height:var(--cfm-csat-cell-size); border-radius:50%; border:2px solid #d1d5db; }
21
+ .cfm-matrix-cell.selected { border-color:var(--cfm-csat-ring-color); background:var(--cfm-selected-bg); }
22
+ .cfm-emoji { font-size:var(--cfm-csat-emoji-size); opacity:var(--cfm-csat-unselected-opacity); }
23
+ .cfm-emoji.selected { opacity:1; outline:2px solid var(--cfm-csat-ring-color); border-radius:8px; }
24
+ .cfm-star { color:var(--cfm-star-color); font-size:24px; opacity:var(--cfm-star-unselected-opacity); }
25
+ .cfm-star.selected { opacity:1; }
26
+ .cfm-badge { width:40px; height:40px; border-radius:8px; background:var(--cfm-badge-color); color:white; display:flex; align-items:center; justify-content:center; font-weight:600; }
27
+ .cfm-slider-track { height:8px; background:var(--cfm-slider-track); border-radius:4px; position:relative; margin:24px 0; }
28
+ .cfm-slider-thumb { width:20px; height:20px; background:var(--cfm-slider-thumb); border-radius:50%; position:absolute; top:-6px; left:60%; }
29
+ .cfm-tick { width:var(--cfm-slider-tick-size); height:var(--cfm-slider-tick-size); background:var(--cfm-slider-tick); border-radius:4px; font-size:11px; display:flex; align-items:center; justify-content:center; color:white; }
30
+ .cfm-dropzone { border:2px dashed var(--cfm-upload-accent); border-radius:var(--cfm-border-radius); padding:var(--cfm-upload-padding); text-align:center; color:var(--cfm-text); }
31
+ .cfm-input { width:100%; height:var(--cfm-input-height); border:1px solid var(--cfm-input-border); border-radius:var(--cfm-input-radius); padding:0 var(--cfm-input-padding); font-family:inherit; box-sizing:border-box; }
32
+ .cfm-rank-item { background:var(--cfm-rank-item-bg); padding:var(--cfm-rank-item-padding); margin-bottom:var(--cfm-rank-item-gap); border-radius:var(--cfm-border-radius); display:flex; align-items:center; gap:12px; }
33
+ .cfm-rank-badge { background:var(--cfm-rank-badge); color:white; width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:12px; }
34
+ .cfm-handle { color:var(--cfm-rank-handle); cursor:grab; }
35
+ .cfm-pin { width:var(--cfm-heatmap-pin-size); height:var(--cfm-heatmap-pin-size); background:var(--cfm-heatmap-pin); border:2px solid white; border-radius:50%; position:absolute; }
36
+ .cfm-media { max-width:var(--cfm-media-max-width); border-radius:var(--cfm-media-radius); background:#e5e7eb; height:200px; display:flex; align-items:center; justify-content:center; }
37
+ .cfm-btn-primary { background:var(--cfm-primary); color:var(--cfm-button-text); border:none; border-radius:var(--cfm-border-radius); padding:10px 20px; }
38
+ .cfm-btn-back { background:transparent; color:var(--cfm-primary); border:1px solid var(--cfm-primary); border-radius:var(--cfm-border-radius); padding:10px 20px; }
39
+ `;
40
+
41
+ function cells(n) {
42
+ return Array.from({ length: n }, (_, i) =>
43
+ `<div class="cfm-matrix-cell${i === 2 ? ' selected' : ''}"></div>`,
44
+ ).join('');
45
+ }
46
+
47
+ function matrixPreview(label) {
48
+ return `<div class="cfm-matrix-row"><div class="cfm-matrix-label">${label}</div><div class="cfm-matrix-cells">${cells(5)}</div></div>`;
49
+ }
50
+
51
+ const npsCells = Array.from({ length: 11 }, (_, i) =>
52
+ `<div class="cfm-nps-cell${i === 8 ? ' selected' : ''}">${i}</div>`,
53
+ ).join('');
54
+
55
+ const PREVIEW_PAGE_DEFS = [
56
+ { key: 'header', title: 'Header', body: `<header class="cfm-header"><img data-cfm-logo class="cfm-header-logo" src="" alt="Logo" style="display:none" /><span data-cfm-company class="cfm-header-company">Company Name</span></header>` },
57
+ { key: 'footer', title: 'Footer', body: `<footer class="cfm-footer"><div><img data-cfm-logo style="width:var(--cfm-footer-logo-width);height:var(--cfm-footer-logo-height);object-fit:contain;display:none" src="" alt="" /><p data-cfm-copyright style="font-size:12px;margin-top:8px">© Company</p></div><nav data-cfm-footer-links class="cfm-footer-links"></nav></footer>` },
58
+ { key: 'chrome', title: 'Chrome', body: `<header class="cfm-header"><img data-cfm-logo class="cfm-header-logo" src="" alt="" style="display:none" /><span data-cfm-company class="cfm-header-company">Company</span></header><div class="cfm-progress"><div class="cfm-progress-fill"></div></div><p data-cfm-survey-title style="font-size:20px;font-weight:var(--cfm-heading-weight);margin:16px 0">Survey Title</p><div style="display:flex;gap:12px;margin-top:24px"><button data-cfm-btn-back class="cfm-btn-back">Back</button><button data-cfm-btn-next class="cfm-btn-primary">Next</button></div>` },
59
+ { key: 'survey_page', title: 'Survey Page', body: `<header class="cfm-header"><img data-cfm-logo class="cfm-header-logo" src="" alt="" style="display:none" /><span data-cfm-company class="cfm-header-company">Company</span></header><div data-cfm-progress class="cfm-progress"><div class="cfm-progress-fill" style="width:35%"></div></div><div data-cfm-language style="text-align:right;margin:16px 0"><select class="cfm-input" style="width:auto"><option>English</option></select></div><p style="font-weight:600"><span data-cfm-question-number>1.</span> Sample question <span data-cfm-required style="color:red">*</span></p><div class="cfm-mcq-option selected"><span>◉</span> Option A</div><p style="font-weight:600;margin-top:24px"><span data-cfm-question-number>2.</span> NPS question</p><div class="cfm-nps-row">${npsCells}</div><div style="display:flex;gap:12px;margin-top:24px"><button data-cfm-btn-back class="cfm-btn-back">Back</button><button data-cfm-btn-next class="cfm-btn-primary">Next</button></div><footer class="cfm-footer" style="margin-top:32px"><p data-cfm-copyright>© Company</p><nav data-cfm-footer-links class="cfm-footer-links"></nav></footer>` },
60
+ { key: 'intro', title: 'Intro', body: `<h1 data-cfm-survey-title style="font-size:28px;font-weight:var(--cfm-heading-weight)">Welcome</h1><p style="margin:16px 0;color:#6b7280">Please share your feedback.</p><button class="cfm-btn-primary">Start Survey</button>` },
61
+ { key: 'end', title: 'End', body: `<h1 style="font-size:28px;font-weight:var(--cfm-heading-weight)">Thank you!</h1><p data-cfm-thank-you style="margin:16px 0">Thank you for your feedback.</p>` },
62
+ { key: 'MCQ_single', title: 'MCQ Single', body: `<p style="font-weight:600;margin-bottom:16px">1. Select one option</p><div data-cfm-mcq-style="outlined"><div class="cfm-mcq-option selected"><span>◉</span> Option A</div><div class="cfm-mcq-option"><span>○</span> Option B</div></div>` },
63
+ { key: 'MCQ_multiple', title: 'MCQ Multiple', body: `<p style="font-weight:600;margin-bottom:16px">Select all that apply</p><div class="cfm-mcq-option selected"><span>☑</span> Option A</div><div class="cfm-mcq-option"><span>☐</span> Option B</div>` },
64
+ { key: 'TEXTFIELD_short', title: 'Text Short', body: `<p style="font-weight:600;margin-bottom:8px">Your name</p><input class="cfm-input" placeholder="Type here..." />` },
65
+ { key: 'TEXTFIELD_long', title: 'Text Long', body: `<p style="font-weight:600;margin-bottom:8px">Comments</p><textarea class="cfm-input" style="height:120px;padding:12px" placeholder="Type your response..."></textarea>` },
66
+ { key: 'NPS_SCALE', title: 'NPS', body: `<p style="font-weight:600;margin-bottom:8px">How likely are you to recommend us?</p><div class="cfm-nps-row">${npsCells}</div>` },
67
+ { key: 'CFM_likert', title: 'CFM Likert', body: matrixPreview('Statement item 1') },
68
+ { key: 'CFM_bipolar', title: 'CFM Bipolar', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width)">Left</div><div class="cfm-matrix-cells">${cells(5)}</div><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width);text-align:right">Right</div></div>` },
69
+ { key: 'CFM_transpose', title: 'CFM Transpose', body: matrixPreview('Scale item (transposed)') },
70
+ { key: 'CFM_carousel', title: 'CFM Carousel', body: `${matrixPreview('Row 1 of 3')}<div style="text-align:center;margin-top:12px"><span style="color:var(--cfm-matrix-carousel-dot)">●</span> ○ ○</div>` },
71
+ { key: 'CFM_dropdown', title: 'CFM Dropdown', body: `<div class="cfm-matrix-label">Statement 1</div><select class="cfm-input" style="margin-top:8px"><option>Select...</option></select>` },
72
+ { key: 'CSAT_emoji', title: 'CSAT Emoji', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">How satisfied?</div><div class="cfm-matrix-cells"><span class="cfm-emoji selected">😞</span><span class="cfm-emoji">🙂</span><span class="cfm-emoji">😊</span></div></div>` },
73
+ { key: 'CSAT_star', title: 'CSAT Star', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">Rate us</div><div class="cfm-matrix-cells"><span class="cfm-star selected">★</span><span class="cfm-star selected">★</span><span class="cfm-star">★</span></div></div>` },
74
+ { key: 'CSAT_numbered', title: 'CSAT Numbered', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">Score</div><div class="cfm-matrix-cells"><div class="cfm-badge">1</div><div class="cfm-badge">2</div><div class="cfm-badge">3</div></div></div>` },
75
+ { key: 'CSAT_graphics', title: 'CSAT Graphics', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">Slider row</div><div style="flex:1;padding:0 16px"><div class="cfm-slider-track"><div class="cfm-slider-thumb"></div></div></div></div>` },
76
+ { key: 'CSAT_carousel', title: 'CSAT Carousel', body: `${matrixPreview('Carousel row')}<div style="text-align:center"><span style="color:var(--cfm-csat-accent)">●</span> ○</div>` },
77
+ { key: 'CSAT_dropdown', title: 'CSAT Dropdown', body: `<div class="cfm-matrix-label">Statement</div><select class="cfm-input" style="margin-top:8px"><option>Choose rating...</option></select>` },
78
+ { key: 'RATING_star', title: 'Rating Star', body: `<div class="cfm-matrix-cells" style="justify-content:flex-start;padding:16px"><span class="cfm-star selected">★</span><span class="cfm-star selected">★</span><span class="cfm-star">★</span></div>` },
79
+ { key: 'RATING_numbered', title: 'Rating Numbered', body: `<div class="cfm-matrix-cells" style="padding:16px"><div class="cfm-badge">1</div><div class="cfm-badge">2</div><div class="cfm-badge">3</div></div>` },
80
+ { key: 'RATING_radio', title: 'Rating Radio', body: matrixPreview('Recommend to a friend') },
81
+ { key: 'RATING_emoji', title: 'Rating Emoji', body: `<div class="cfm-matrix-cells" style="padding:16px"><span class="cfm-emoji">🙂</span><span class="cfm-emoji selected">😊</span><span class="cfm-emoji">🤩</span></div>` },
82
+ { key: 'RATING_graphics', title: 'Rating Graphics', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">Experience</div><div style="flex:1;padding:0 16px"><div class="cfm-slider-track"><div class="cfm-slider-thumb"></div></div></div></div>` },
83
+ { key: 'RATING_slider', title: 'Rating Slider', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">Rate on slider</div><div style="flex:1;padding:0 16px"><div class="cfm-slider-track"><div class="cfm-slider-thumb"></div></div></div></div>` },
84
+ { key: 'SLIDER_matrix', title: 'Slider Matrix', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">Statement 1</div><div style="flex:1;padding:0 16px"><div class="cfm-slider-track"><div class="cfm-slider-thumb"></div></div></div></div>` },
85
+ { key: 'FILE_UPLOAD', title: 'File Upload', body: `<div data-cfm-dropzone-style="dashed" class="cfm-dropzone">Drop files here or click to upload</div>` },
86
+ { key: 'TEXT_AND_MEDIA', title: 'Text & Media', body: `<div class="cfm-media">Media preview</div>` },
87
+ { key: 'HEATMAP', title: 'Heatmap', body: `<div style="position:relative;background:#e5e7eb;height:240px;border-radius:var(--cfm-border-radius)"><div class="cfm-pin" style="top:40%;left:55%"></div></div>` },
88
+ { key: 'RANK_drag', title: 'Rank Drag', body: `<div class="cfm-rank-item"><span class="cfm-handle">⠿</span><span class="cfm-rank-badge">1</span> Item A</div><div class="cfm-rank-item"><span class="cfm-handle">⠿</span><span class="cfm-rank-badge">2</span> Item B</div>` },
89
+ { key: 'RANK_dropdown', title: 'Rank Dropdown', body: `<div style="display:flex;align-items:center;gap:12px"><span>Item A</span><select class="cfm-input" style="width:80px"><option>1</option></select></div>` },
90
+ ];
91
+
92
+ function buildPreviewHtml(def, inlineCss, inlineJs) {
93
+ return `<!DOCTYPE html>
94
+ <html lang="en">
95
+ <head>
96
+ <meta charset="UTF-8" />
97
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
98
+ <title>${def.title} — CFM Preview</title>
99
+ <style>${inlineCss}\n${sharedStyles}</style>
100
+ </head>
101
+ <body>
102
+ <div class="cfm-preview-root">${def.body}</div>
103
+ <script>${inlineJs}</script>
104
+ </body>
105
+ </html>`;
106
+ }
107
+
108
+ module.exports = { PREVIEW_PAGE_DEFS, buildPreviewHtml, sharedStyles, PREVIEW_KEYS: PREVIEW_PAGE_DEFS.map((d) => d.key) };