@explorer02/cfm-survey-sdk 0.2.3 → 0.2.5

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 (100) hide show
  1. package/dist/cli/index.js +141 -33
  2. package/dist/cli/index.mjs +141 -33
  3. package/dist/index.js +1 -1
  4. package/dist/index.mjs +1 -1
  5. package/package.json +3 -1
  6. package/postinstall.js +9 -5
  7. package/templates/AGENT.md +60 -19
  8. package/templates/deploy-wizard-to-s3.sh +52 -0
  9. package/templates/docs/00-integration/agent-execution-flow.md +293 -0
  10. package/templates/docs/00-integration/agent-operating-contract.md +268 -0
  11. package/templates/docs/00-integration/apply-ui-config.md +390 -0
  12. package/templates/docs/00-integration/aws-deploy.md +117 -3
  13. package/templates/docs/00-integration/canonical-survey-fixtures.md +94 -0
  14. package/templates/docs/00-integration/client-integration-guide.md +9 -7
  15. package/templates/docs/00-integration/client-lib-folder.md +115 -0
  16. package/templates/docs/00-integration/component-checklist.md +90 -62
  17. package/templates/docs/00-integration/constraints.md +14 -5
  18. package/templates/docs/00-integration/custom-field-logic-and-navigation.md +56 -0
  19. package/templates/docs/00-integration/mockup-ui-extraction.md +113 -0
  20. package/templates/docs/00-integration/placeholders-and-custom-fields-wiring.md +192 -0
  21. package/templates/docs/00-integration/placeholders-and-tokens.md +2 -1
  22. package/templates/docs/00-integration/progress.md +4 -2
  23. package/templates/docs/00-integration/setup.md +12 -0
  24. package/templates/docs/00-integration/ui-customization-wizard.md +123 -0
  25. package/templates/docs/00-integration/useSurveySDK.md +35 -1
  26. package/templates/docs/00-integration/wizard-api.md +126 -0
  27. package/templates/docs/00-integration/wizard-config-handoff.md +292 -0
  28. package/templates/docs/00-integration/wizard-preview-build-guide.md +405 -0
  29. package/templates/docs/00-integration/wizard-question-type-styling.md +197 -0
  30. package/templates/docs/00-integration/wizard-troubleshooting.md +80 -0
  31. package/templates/docs/01-components/01-survey-page.md +18 -8
  32. package/templates/docs/01-components/02-question.md +2 -2
  33. package/templates/docs/01-components/03-rating-scale.md +12 -18
  34. package/templates/docs/01-components/05-csat-matrix-scale.md +18 -26
  35. package/templates/docs/01-components/06-likert-matrix-scale.md +19 -39
  36. package/templates/docs/01-components/07-slider-matrix-scale.md +48 -33
  37. package/templates/docs/01-components/09-custom-slider-track.md +15 -25
  38. package/templates/docs/01-components/10-header-footer.md +23 -4
  39. package/templates/docs/01-components/11-progress-bar.md +14 -6
  40. package/templates/docs/01-components/19-survey-sticky-chrome.md +79 -0
  41. package/templates/docs/01-components/README.md +2 -2
  42. package/templates/docs/02-reference/question-types/10-slider-matrix.md +38 -26
  43. package/templates/docs/02-reference/routing-table.md +1 -0
  44. package/templates/docs/02-reference/value-derivation.md +1 -1
  45. package/templates/docs/03-ui-specs/00-question-shell.md +10 -0
  46. package/templates/docs/03-ui-specs/01-rating.md +18 -0
  47. package/templates/docs/03-ui-specs/02-radio.md +16 -1
  48. package/templates/docs/03-ui-specs/03-text.md +12 -1
  49. package/templates/docs/03-ui-specs/04-csat.md +84 -50
  50. package/templates/docs/03-ui-specs/06-slider.md +3 -1
  51. package/templates/docs/03-ui-specs/07-matrix-cfm.md +71 -48
  52. package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +48 -18
  53. package/templates/docs/03-ui-specs/09-slider-matrix.md +59 -32
  54. package/templates/docs/03-ui-specs/10-file-upload.md +11 -0
  55. package/templates/docs/03-ui-specs/11-text-and-media.md +4 -0
  56. package/templates/docs/03-ui-specs/12-survey-chrome.md +54 -5
  57. package/templates/docs/03-ui-specs/13-heatmap.md +12 -1
  58. package/templates/docs/03-ui-specs/14-rank-order.md +11 -0
  59. package/templates/docs/03-ui-specs/README.md +4 -2
  60. package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +1 -1
  61. package/templates/docs/MANIFEST.json +180 -7
  62. package/templates/docs/index.md +114 -117
  63. package/templates/docs/templates/CsatMatrixScale.tsx +637 -0
  64. package/templates/docs/templates/FileUploadScale.tsx +262 -0
  65. package/templates/docs/templates/HeatmapScale.tsx +114 -0
  66. package/templates/docs/templates/LikertMatrixScale.tsx +414 -0
  67. package/templates/docs/templates/Question.tsx +3 -0
  68. package/templates/docs/templates/RatingScale.tsx +129 -0
  69. package/templates/docs/templates/SliderMatrixScale.tsx +248 -0
  70. package/templates/docs/templates/SurveyStickyChrome.tsx +24 -0
  71. package/templates/docs/templates/customFieldValues.ts +36 -0
  72. package/templates/docs/templates/deploy-to-aws.sh +74 -0
  73. package/templates/docs/templates/implementation_plan.md +64 -10
  74. package/templates/docs/templates/labelStyles.ts +33 -0
  75. package/templates/docs/templates/selectionStyles.ts +100 -0
  76. package/templates/docs/templates/surveyUiIcons.tsx +11 -0
  77. package/templates/docs/templates/surveyUiScaleUtils.ts +51 -0
  78. package/templates/docs/templates/verify-agent-build.sh +119 -0
  79. package/templates/preview-harness/preview-bridge.inline.js +201 -0
  80. package/templates/preview-harness/previewPages.js +108 -0
  81. package/templates/preview-harness/previewPages.ts +298 -0
  82. package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +67 -0
  83. package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +122 -0
  84. package/templates/preview-harness/vite-app/src/SurveyPagePreview.tsx +75 -0
  85. package/templates/preview-harness/vite-app/src/fixtures/questions.ts +311 -0
  86. package/templates/preview-harness/vite-app/src/globals.css +16 -0
  87. package/templates/preview-harness/vite-app/src/mount.tsx +9 -0
  88. package/templates/preview-harness/vite-app/src/preview-live-overrides.css +101 -0
  89. package/templates/preview-harness/vite-app/stubs/next-dynamic.tsx +25 -0
  90. package/templates/preview-harness/vite-app/stubs/next-image.tsx +29 -0
  91. package/templates/previewBridge.ts +153 -0
  92. package/templates/survey-theme.css +295 -0
  93. package/templates/survey-ui-config.schema.json +213 -0
  94. package/templates/wizard-dist/assets/PreviewMock-AoZdAEPZ.js +1 -0
  95. package/templates/wizard-dist/assets/TypePanel-39FnbtvG.js +1 -0
  96. package/templates/wizard-dist/assets/index-BhWM50Yu.css +1 -0
  97. package/templates/wizard-dist/assets/index-CWKdsJOp.js +34 -0
  98. package/templates/wizard-dist/assets/vendor-BwkXDkd3.js +17 -0
  99. package/templates/wizard-dist/index.html +20 -0
  100. package/templates/wizard.html +13 -1129
@@ -0,0 +1,311 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import type { SpecialPage, SurveyQuestion } from '@explorer02/cfm-survey-sdk';
3
+
4
+ const base = (partial: Partial<SurveyQuestion> & { type: SurveyQuestion['type'] }): SurveyQuestion =>
5
+ ({
6
+ id: 'preview-q',
7
+ questionText: 'How satisfied are you with our service?',
8
+ required: true,
9
+ questionNumber: 1,
10
+ options: [],
11
+ ...partial,
12
+ }) as SurveyQuestion;
13
+
14
+ const MATRIX_ROWS_MULTI = [
15
+ { id: 'r1', statementText: 'The product is easy to use' },
16
+ { id: 'r2', statementText: 'Support was helpful' },
17
+ ];
18
+
19
+ export const FIXTURE_INTRO: SpecialPage = {
20
+ id: 'preview-intro',
21
+ header: 'Welcome to our survey',
22
+ description: '<p>Please take a few minutes to share your feedback.</p>',
23
+ ctaLabel: 'Start Survey',
24
+ };
25
+
26
+ export const FIXTURE_END: SpecialPage = {
27
+ id: 'preview-end',
28
+ header: 'Thank you!',
29
+ description: '<p>We appreciate your time.</p>',
30
+ };
31
+
32
+ export const FIXTURE_MCQ_SINGLE = base({
33
+ type: 'MCQ',
34
+ selectionMode: 'single',
35
+ options: [
36
+ { id: 'a', optionLabel: 'Very satisfied', value: 'a' },
37
+ { id: 'b', optionLabel: 'Satisfied', value: 'b' },
38
+ { id: 'c', optionLabel: 'Neutral', value: 'c' },
39
+ ],
40
+ defaultOptionIds: ['a'],
41
+ });
42
+
43
+ export const FIXTURE_MCQ_MULTIPLE = base({
44
+ type: 'MCQ',
45
+ selectionMode: 'multiple',
46
+ minSelections: 1,
47
+ maxSelections: 2,
48
+ options: [
49
+ { id: 'a', optionLabel: 'Fast delivery', value: 'a' },
50
+ { id: 'b', optionLabel: 'Great support', value: 'b' },
51
+ { id: 'c', optionLabel: 'Easy to use', value: 'c' },
52
+ ],
53
+ });
54
+
55
+ export const FIXTURE_NPS = base({
56
+ type: 'NPS_SCALE',
57
+ buttonStyle: 'standard',
58
+ minLabel: 'Not at all likely',
59
+ maxLabel: 'Extremely likely',
60
+ options: Array.from({ length: 11 }, (_, i) => ({
61
+ id: `nps-${i}`,
62
+ optionLabel: String(i),
63
+ value: i,
64
+ })),
65
+ });
66
+
67
+ export const FIXTURE_SURVEY_PAGE_NPS = base({
68
+ id: 'preview-survey-nps',
69
+ type: 'NPS_SCALE',
70
+ questionNumber: 6,
71
+ buttonStyle: 'standard',
72
+ questionText:
73
+ 'Based on your experience, how likely are you to recommend [Company/Product/Service] to your friends and family?',
74
+ minLabel: 'Not Likely At All',
75
+ maxLabel: 'Extremely Likely',
76
+ options: Array.from({ length: 11 }, (_, i) => ({
77
+ id: `nps-${i}`,
78
+ optionLabel: String(i),
79
+ value: i,
80
+ })),
81
+ });
82
+
83
+ export const PREVIEW_LANGUAGES = [
84
+ { code: 'en', name: 'English' },
85
+ { code: 'de', name: 'Deutsch' },
86
+ ];
87
+
88
+ export const FIXTURE_TEXT_SHORT = base({
89
+ type: 'TEXTFIELD',
90
+ textFieldType: 'short',
91
+ placeholder: 'Your answer',
92
+ });
93
+
94
+ export const FIXTURE_TEXT_LONG = base({
95
+ type: 'TEXTFIELD',
96
+ textFieldType: 'long',
97
+ placeholder: 'Tell us more…',
98
+ });
99
+
100
+ export const FIXTURE_CFM_LIKERT = base({
101
+ type: 'CFM_MATRIX',
102
+ statementRows: MATRIX_ROWS_MULTI,
103
+ scaleColumns: [
104
+ { id: 'c1', label: 'Strongly disagree' },
105
+ { id: 'c2', label: 'Disagree' },
106
+ { id: 'c3', label: 'Neutral' },
107
+ { id: 'c4', label: 'Agree' },
108
+ { id: 'c5', label: 'Strongly agree' },
109
+ ],
110
+ });
111
+
112
+ export const FIXTURE_CFM_BIPOLAR = base({
113
+ type: 'CFM_MATRIX',
114
+ statementLayout: 'bipolar',
115
+ showColumnHeaders: false,
116
+ statementRows: [
117
+ {
118
+ id: 'r1',
119
+ statementText: 'Difficult',
120
+ oppositeStatementText: 'Easy',
121
+ },
122
+ ],
123
+ scaleColumns: Array.from({ length: 5 }, (_, i) => ({ id: `c${i}`, label: '' })),
124
+ scaleAnchorLabels: ['Hard', '', '', '', 'Easy'],
125
+ });
126
+
127
+ export const FIXTURE_CSAT_EMOJI = base({
128
+ type: 'CSAT_MATRIX',
129
+ displayStyle: 'emoji',
130
+ statementRows: [{ id: 'r1', statementText: 'How do you feel?' }],
131
+ scaleColumns: [
132
+ { id: 'c1', label: '😞' },
133
+ { id: 'c2', label: '😐' },
134
+ { id: 'c3', label: '🙂' },
135
+ { id: 'c4', label: '😊' },
136
+ { id: 'c5', label: '🤩' },
137
+ ],
138
+ scaleAnchorLabels: ['Very unhappy', '', '', '', 'Very happy'],
139
+ });
140
+
141
+ export const FIXTURE_CSAT_STAR = base({
142
+ type: 'CSAT_MATRIX',
143
+ displayStyle: 'star',
144
+ statementRows: [{ id: 'r1', statementText: 'Rate our app' }],
145
+ scaleColumns: Array.from({ length: 5 }, (_, i) => ({ id: `c${i}`, label: String(i + 1) })),
146
+ });
147
+
148
+ export const FIXTURE_CSAT_NUMBERED = base({
149
+ type: 'CSAT_MATRIX',
150
+ displayStyle: 'numbered',
151
+ statementRows: [{ id: 'r1', statementText: 'Rate support' }],
152
+ scaleColumns: Array.from({ length: 10 }, (_, i) => ({ id: `c${i}`, label: String(i + 1) })),
153
+ });
154
+
155
+ export const FIXTURE_CSAT_GRAPHICS = base({
156
+ type: 'CSAT_MATRIX',
157
+ displayStyle: 'graphics',
158
+ statementRows: [{ id: 'r1', statementText: 'Experience quality' }],
159
+ scaleColumns: [
160
+ { id: 'c1', label: 'Low' },
161
+ { id: 'c2', label: 'Mid' },
162
+ { id: 'c3', label: 'High' },
163
+ ],
164
+ });
165
+
166
+ export const FIXTURE_CSAT_CAROUSEL = base({
167
+ type: 'CSAT_MATRIX',
168
+ displayStyle: 'star',
169
+ gridLayout: 'carousel',
170
+ statementRows: [{ id: 'r1', statementText: 'Overall experience' }],
171
+ scaleColumns: Array.from({ length: 5 }, (_, i) => ({ id: `c${i}`, label: String(i + 1) })),
172
+ });
173
+
174
+ export const FIXTURE_CSAT_DROPDOWN = base({
175
+ type: 'CSAT_MATRIX',
176
+ displayStyle: 'dropdown',
177
+ statementRows: [{ id: 'r1', statementText: 'Select your rating' }],
178
+ scaleColumns: [
179
+ { id: 'c1', label: 'Poor' },
180
+ { id: 'c2', label: 'Fair' },
181
+ { id: 'c3', label: 'Good' },
182
+ { id: 'c4', label: 'Excellent' },
183
+ ],
184
+ });
185
+
186
+ export const FIXTURE_RATING_STAR = base({
187
+ type: 'RATING_MATRIX',
188
+ displayStyle: 'star',
189
+ statementRows: [{ id: 'r1', statementText: 'Overall rating' }],
190
+ scaleColumns: Array.from({ length: 5 }, (_, i) => ({ id: `c${i}`, label: String(i + 1) })),
191
+ });
192
+
193
+ export const FIXTURE_RATING_NUMBERED = base({
194
+ type: 'RATING_MATRIX',
195
+ displayStyle: 'numbered',
196
+ statementRows: [{ id: 'r1', statementText: 'Value for money' }],
197
+ scaleColumns: Array.from({ length: 10 }, (_, i) => ({ id: `c${i}`, label: String(i + 1) })),
198
+ });
199
+
200
+ export const FIXTURE_RATING_RADIO = base({
201
+ type: 'RATING_MATRIX',
202
+ displayStyle: 'standard',
203
+ statementRows: [{ id: 'r1', statementText: 'Recommend to a friend' }],
204
+ scaleColumns: Array.from({ length: 5 }, (_, i) => ({ id: `c${i}`, label: String(i + 1) })),
205
+ });
206
+
207
+ export const FIXTURE_RATING_EMOJI = base({
208
+ type: 'RATING_MATRIX',
209
+ displayStyle: 'emoji',
210
+ statementRows: [{ id: 'r1', statementText: 'How do you feel?' }],
211
+ scaleColumns: Array.from({ length: 5 }, (_, i) => ({ id: `c${i}`, label: String(i + 1) })),
212
+ });
213
+
214
+ export const FIXTURE_RATING_GRAPHICS = base({
215
+ type: 'RATING_MATRIX',
216
+ displayStyle: 'graphics',
217
+ statementRows: [{ id: 'r1', statementText: 'Experience quality' }],
218
+ scaleColumns: [
219
+ { id: 'c1', label: 'Low' },
220
+ { id: 'c2', label: 'Mid' },
221
+ { id: 'c3', label: 'High' },
222
+ ],
223
+ });
224
+
225
+ export const FIXTURE_RATING_SLIDER = base({
226
+ type: 'RATING_MATRIX',
227
+ displayStyle: 'graphics',
228
+ statementRows: [{ id: 'r1', statementText: 'Rate on slider' }],
229
+ scaleColumns: Array.from({ length: 5 }, (_, i) => ({ id: `c${i}`, label: String(i + 1) })),
230
+ scaleAnchorLabels: ['Low', 'High'],
231
+ });
232
+
233
+ export const FIXTURE_SLIDER = base({
234
+ type: 'SLIDER_MATRIX',
235
+ statementRows: [
236
+ { id: 'r1', statementText: 'Ease of use', min: 0, max: 10, step: 1 },
237
+ { id: 'r2', statementText: 'Speed', min: 0, max: 10, step: 1 },
238
+ ],
239
+ scaleAnchorLabels: ['Not at all', 'Extremely'],
240
+ ticks: {
241
+ count: 11,
242
+ marks: Array.from({ length: 11 }, (_, i) => ({
243
+ tickPosition: i,
244
+ tickLabel: String(i),
245
+ })),
246
+ },
247
+ enableInputBox: false,
248
+ displayValues: false,
249
+ hasNotApplicableOption: false,
250
+ sliderStyle: 'standard',
251
+ });
252
+
253
+ export const FIXTURE_FILE_UPLOAD = base({
254
+ type: 'FILE_UPLOAD',
255
+ maxFileCount: 1,
256
+ });
257
+
258
+ export const FIXTURE_HEATMAP = base({
259
+ type: 'HEATMAP',
260
+ maxClicksAllowed: 3,
261
+ imageUrl: 'https://placehold.co/600x400/e5e7eb/6b7280?text=Heatmap+Image',
262
+ });
263
+
264
+ export const FIXTURE_RANK_DRAG = base({
265
+ type: 'RANK_ORDER',
266
+ interactionMode: 'dragAndDrop',
267
+ optionDisplay: 'textOnly',
268
+ options: [
269
+ { id: 'o1', optionLabel: 'Speed' },
270
+ { id: 'o2', optionLabel: 'Price' },
271
+ { id: 'o3', optionLabel: 'Quality' },
272
+ ],
273
+ });
274
+
275
+ export const FIXTURE_RANK_DROPDOWN = base({
276
+ type: 'RANK_ORDER',
277
+ interactionMode: 'dropdown',
278
+ optionDisplay: 'textOnly',
279
+ options: [
280
+ { id: 'o1', optionLabel: 'Feature A' },
281
+ { id: 'o2', optionLabel: 'Feature B' },
282
+ { id: 'o3', optionLabel: 'Feature C' },
283
+ ],
284
+ });
285
+
286
+ export const PREVIEW_FIXTURES: Record<string, SurveyQuestion | null> = {
287
+ MCQ_single: FIXTURE_MCQ_SINGLE,
288
+ MCQ_multiple: FIXTURE_MCQ_MULTIPLE,
289
+ TEXTFIELD_short: FIXTURE_TEXT_SHORT,
290
+ TEXTFIELD_long: FIXTURE_TEXT_LONG,
291
+ NPS_SCALE: FIXTURE_NPS,
292
+ CFM_likert: FIXTURE_CFM_LIKERT,
293
+ CFM_bipolar: FIXTURE_CFM_BIPOLAR,
294
+ CSAT_emoji: FIXTURE_CSAT_EMOJI,
295
+ CSAT_star: FIXTURE_CSAT_STAR,
296
+ CSAT_numbered: FIXTURE_CSAT_NUMBERED,
297
+ CSAT_graphics: FIXTURE_CSAT_GRAPHICS,
298
+ CSAT_carousel: FIXTURE_CSAT_CAROUSEL,
299
+ CSAT_dropdown: FIXTURE_CSAT_DROPDOWN,
300
+ RATING_star: FIXTURE_RATING_STAR,
301
+ RATING_numbered: FIXTURE_RATING_NUMBERED,
302
+ RATING_radio: FIXTURE_RATING_RADIO,
303
+ RATING_emoji: FIXTURE_RATING_EMOJI,
304
+ RATING_graphics: FIXTURE_RATING_GRAPHICS,
305
+ RATING_slider: FIXTURE_RATING_SLIDER,
306
+ SLIDER_matrix: FIXTURE_SLIDER,
307
+ FILE_UPLOAD: FIXTURE_FILE_UPLOAD,
308
+ HEATMAP: FIXTURE_HEATMAP,
309
+ RANK_drag: FIXTURE_RANK_DRAG,
310
+ RANK_dropdown: FIXTURE_RANK_DROPDOWN,
311
+ };
@@ -0,0 +1,16 @@
1
+ @import "tailwindcss";
2
+ @import "./survey-theme.css";
3
+ @import "./preview-live-overrides.css";
4
+
5
+ @source "../agent/src/**/*.{ts,tsx}";
6
+
7
+ body {
8
+ margin: 0;
9
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
10
+ color: #262626;
11
+ background-color: #ffffff;
12
+ }
13
+
14
+ #root {
15
+ min-height: 100vh;
16
+ }
@@ -0,0 +1,9 @@
1
+ import React, { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import './globals.css';
4
+
5
+ export function mountPreview(ui: React.ReactNode): void {
6
+ const el = document.getElementById('root');
7
+ if (!el) throw new Error('Missing #root');
8
+ createRoot(el).render(<StrictMode>{ui}</StrictMode>);
9
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Maps hardcoded Tailwind utility classes (from agent builds) to wizard CSS vars
3
+ * when html.cfm-live-edit is active. Vars are updated via postMessage bridge.
4
+ */
5
+ html.cfm-live-edit {
6
+ font-family: var(--cfm-font-family, system-ui, sans-serif);
7
+ color: var(--cfm-text, #262626);
8
+ background-color: var(--cfm-background, #fff);
9
+ }
10
+
11
+ html.cfm-live-edit header {
12
+ background-color: var(--cfm-header-bg, #fff) !important;
13
+ height: var(--cfm-header-height, 120px) !important;
14
+ }
15
+
16
+ html.cfm-live-edit header .h-\[120px\].w-\[120px\] {
17
+ width: var(--cfm-header-logo-width, 120px) !important;
18
+ height: var(--cfm-header-logo-height, 120px) !important;
19
+ }
20
+
21
+ html.cfm-live-edit .bg-\[\#e20074\] {
22
+ background-color: var(--cfm-primary, #e20074) !important;
23
+ }
24
+
25
+ html.cfm-live-edit .text-\[\#e20074\] {
26
+ color: var(--cfm-primary, #e20074) !important;
27
+ }
28
+
29
+ html.cfm-live-edit .border-\[\#e20074\] {
30
+ border-color: var(--cfm-mcq-selected-accent, var(--cfm-primary, #e20074)) !important;
31
+ }
32
+
33
+ html.cfm-live-edit .bg-\[\#fdf2f8\] {
34
+ background-color: var(--cfm-secondary, #fdf2f8) !important;
35
+ }
36
+
37
+ html.cfm-live-edit .bg-\[\#fbe8f3\] {
38
+ background-color: var(--cfm-mcq-selected-bg, var(--cfm-secondary, #fbe8f3)) !important;
39
+ }
40
+
41
+ html.cfm-live-edit footer.bg-\[\#262626\],
42
+ html.cfm-live-edit footer.cfm-footer,
43
+ html.cfm-live-edit .cfm-footer {
44
+ background-color: var(--cfm-footer-bg, #262626) !important;
45
+ color: var(--cfm-footer-text, #fff) !important;
46
+ }
47
+
48
+ html.cfm-live-edit footer .text-gray-400,
49
+ html.cfm-live-edit footer.cfm-footer [data-cfm-copyright] {
50
+ color: var(--cfm-footer-text, #9ca3af) !important;
51
+ }
52
+
53
+ html.cfm-live-edit footer.cfm-footer a,
54
+ html.cfm-live-edit .cfm-footer a {
55
+ color: var(--cfm-footer-link, #9ca3af) !important;
56
+ }
57
+
58
+ html.cfm-live-edit header.cfm-header {
59
+ background-color: var(--cfm-header-bg, #fff) !important;
60
+ height: var(--cfm-header-height, 120px) !important;
61
+ border-bottom-color: var(--cfm-header-border, #e5e7eb) !important;
62
+ }
63
+
64
+ html.cfm-live-edit .h-3.rounded-full.bg-gray-100 {
65
+ background-color: var(--cfm-progress-track, #f3f4f6) !important;
66
+ height: var(--cfm-progress-height, 12px) !important;
67
+ }
68
+
69
+ html.cfm-live-edit .h-3.rounded-full.bg-\[\#e20074\] {
70
+ background-color: var(--cfm-progress-fill, var(--cfm-primary, #e20074)) !important;
71
+ }
72
+
73
+ html.cfm-live-edit .max-w-3xl,
74
+ html.cfm-live-edit .max-w-4xl {
75
+ max-width: var(--cfm-max-width, 48rem) !important;
76
+ }
77
+
78
+ html.cfm-live-edit .cfm-header-inner {
79
+ flex-direction: var(--cfm-header-flex-direction, column) !important;
80
+ justify-content: var(--cfm-header-justify, flex-start) !important;
81
+ align-items: var(--cfm-header-brand-align, flex-start) !important;
82
+ }
83
+
84
+ html.cfm-live-edit [data-cfm-company] {
85
+ margin-top: var(--cfm-header-company-margin-top, 8px) !important;
86
+ margin-left: var(--cfm-header-company-margin-left, 0) !important;
87
+ }
88
+
89
+ html.cfm-live-edit .cfm-footer-inner {
90
+ flex-direction: var(--cfm-footer-flex-direction, row) !important;
91
+ }
92
+
93
+ html.cfm-live-edit .rounded-xl,
94
+ html.cfm-live-edit .rounded-md,
95
+ html.cfm-live-edit .rounded-lg {
96
+ border-radius: var(--cfm-border-radius, 12px);
97
+ }
98
+
99
+ html.cfm-live-edit [data-cfm-survey-title] {
100
+ font-weight: var(--cfm-heading-weight, 600);
101
+ }
@@ -0,0 +1,25 @@
1
+ import { lazy, Suspense, type ComponentType } from 'react';
2
+
3
+ type DynamicOptions = {
4
+ loading?: () => React.ReactNode;
5
+ ssr?: boolean;
6
+ };
7
+
8
+ /** Vite preview stub for `next/dynamic`. */
9
+ export default function dynamic<P extends object>(
10
+ loader: () => Promise<{ default: ComponentType<P> } | ComponentType<P>>,
11
+ options?: DynamicOptions,
12
+ ) {
13
+ const Lazy = lazy(async () => {
14
+ const mod = await loader();
15
+ return { default: ('default' in (mod as object) ? (mod as { default: ComponentType<P> }).default : mod) as ComponentType<P> };
16
+ });
17
+
18
+ return function DynamicWrapper(props: P) {
19
+ return (
20
+ <Suspense fallback={options?.loading?.() ?? null}>
21
+ <Lazy {...props} />
22
+ </Suspense>
23
+ );
24
+ };
25
+ }
@@ -0,0 +1,29 @@
1
+ import type { ImgHTMLAttributes } from 'react';
2
+
3
+ type ImageProps = ImgHTMLAttributes<HTMLImageElement> & {
4
+ src: string;
5
+ alt: string;
6
+ width?: number;
7
+ height?: number;
8
+ priority?: boolean;
9
+ fill?: boolean;
10
+ };
11
+
12
+ /** Vite preview stub for `next/image`. */
13
+ export default function Image({ src, alt, width, height, className, style, fill, priority, ...rest }: ImageProps) {
14
+ const resolved =
15
+ /^https?:\/\//.test(src) || src.startsWith('data:')
16
+ ? src
17
+ : `../${src.replace(/^\//, '')}`;
18
+ return (
19
+ <img
20
+ src={resolved}
21
+ alt={alt}
22
+ width={width}
23
+ height={height}
24
+ className={className}
25
+ style={fill ? { ...style, width: '100%', height: '100%', objectFit: 'contain' } : style}
26
+ {...rest}
27
+ />
28
+ );
29
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Live preview bridge for cfm-sdk customize wizard.
3
+ * Copy to src/lib/previewBridge.ts and import once in layout.tsx.
4
+ *
5
+ * Supports SNAPSHOT (full state on tab load) and DELTA (rAF-batched partial updates).
6
+ */
7
+ export const PREVIEW_BRIDGE_SNAPSHOT = 'CFM_UI_CONFIG_SNAPSHOT';
8
+ export const PREVIEW_BRIDGE_DELTA = 'CFM_UI_CONFIG_DELTA';
9
+ /** @deprecated use SNAPSHOT/DELTA */
10
+ export const PREVIEW_BRIDGE_MESSAGE = PREVIEW_BRIDGE_SNAPSHOT;
11
+
12
+ export interface ContentPatch {
13
+ logoUrl?: string | null;
14
+ surveyTitle?: string;
15
+ companyName?: string;
16
+ thankYouMessage?: string;
17
+ buttonLabels?: { next?: string; back?: string; submit?: string };
18
+ layoutFlags?: Record<string, boolean | string>;
19
+ footerLinks?: { label: string; url: string }[];
20
+ copyrightText?: string;
21
+ }
22
+
23
+ interface PreviewBridgePayload {
24
+ type: string;
25
+ cssVars?: Record<string, string>;
26
+ contentPatch?: ContentPatch;
27
+ }
28
+
29
+ let rafId: number | null = null;
30
+ const pendingVars: Record<string, string> = {};
31
+ let liveEditClass = false;
32
+
33
+ function flushCssVars(): void {
34
+ rafId = null;
35
+ const root = document.documentElement;
36
+ for (const [key, value] of Object.entries(pendingVars)) {
37
+ if (root.style.getPropertyValue(key) !== value) {
38
+ root.style.setProperty(key, value);
39
+ }
40
+ delete pendingVars[key];
41
+ }
42
+ }
43
+
44
+ function queueCssVars(vars: Record<string, string>): void {
45
+ Object.assign(pendingVars, vars);
46
+ if (rafId == null) {
47
+ rafId = requestAnimationFrame(flushCssVars);
48
+ }
49
+ }
50
+
51
+ function applyContentPatch(patch: ContentPatch): void {
52
+ const root = document.documentElement;
53
+ if (patch.logoUrl !== undefined) {
54
+ root.dataset.cfmLogoUrl = patch.logoUrl ?? '';
55
+ document.querySelectorAll('[data-cfm-logo]').forEach((el) => {
56
+ const img = el as HTMLImageElement;
57
+ if (patch.logoUrl) {
58
+ img.src = patch.logoUrl;
59
+ img.style.display = '';
60
+ } else {
61
+ img.style.display = 'none';
62
+ }
63
+ });
64
+ }
65
+ if (patch.companyName !== undefined) {
66
+ document.querySelectorAll('[data-cfm-company]').forEach((el) => {
67
+ el.textContent = patch.companyName ?? '';
68
+ });
69
+ }
70
+ if (patch.surveyTitle !== undefined) {
71
+ document.querySelectorAll('[data-cfm-survey-title]').forEach((el) => {
72
+ el.textContent = patch.surveyTitle ?? '';
73
+ });
74
+ }
75
+ if (patch.thankYouMessage !== undefined) {
76
+ document.querySelectorAll('[data-cfm-thank-you]').forEach((el) => {
77
+ el.textContent = patch.thankYouMessage ?? '';
78
+ });
79
+ }
80
+ if (patch.buttonLabels?.next) {
81
+ document.querySelectorAll('[data-cfm-btn-next]').forEach((el) => {
82
+ el.textContent = patch.buttonLabels!.next!;
83
+ });
84
+ }
85
+ if (patch.buttonLabels?.back) {
86
+ document.querySelectorAll('[data-cfm-btn-back]').forEach((el) => {
87
+ el.textContent = patch.buttonLabels!.back!;
88
+ });
89
+ }
90
+ if (patch.copyrightText !== undefined) {
91
+ document.querySelectorAll('[data-cfm-copyright]').forEach((el) => {
92
+ el.textContent = patch.copyrightText ?? '';
93
+ });
94
+ }
95
+ if (patch.footerLinks) {
96
+ const nav = document.querySelector('[data-cfm-footer-links]');
97
+ if (nav) {
98
+ nav.innerHTML = patch.footerLinks
99
+ .map(
100
+ (l) =>
101
+ `<a href="${l.url}" target="_blank" rel="noopener noreferrer" style="color:var(--cfm-footer-link)">${l.label}</a>`,
102
+ )
103
+ .join('');
104
+ }
105
+ }
106
+ }
107
+
108
+ function setLiveEditMode(on: boolean): void {
109
+ if (liveEditClass === on) return;
110
+ liveEditClass = on;
111
+ document.documentElement.classList.toggle('cfm-live-edit', on);
112
+ }
113
+
114
+ export function initPreviewBridge(): () => void {
115
+ if (typeof window === 'undefined') return () => {};
116
+
117
+ const handler = (event: MessageEvent<PreviewBridgePayload>) => {
118
+ const data = event.data;
119
+ if (!data?.type) return;
120
+
121
+ if (
122
+ data.type !== PREVIEW_BRIDGE_SNAPSHOT &&
123
+ data.type !== PREVIEW_BRIDGE_DELTA &&
124
+ data.type !== PREVIEW_BRIDGE_MESSAGE
125
+ ) {
126
+ return;
127
+ }
128
+
129
+ setLiveEditMode(true);
130
+
131
+ if (data.cssVars) {
132
+ if (data.type === PREVIEW_BRIDGE_DELTA) {
133
+ queueCssVars(data.cssVars);
134
+ } else {
135
+ applyCssVarsImmediate(data.cssVars);
136
+ }
137
+ }
138
+
139
+ if (data.contentPatch && data.type !== PREVIEW_BRIDGE_DELTA) {
140
+ applyContentPatch(data.contentPatch);
141
+ }
142
+ };
143
+
144
+ window.addEventListener('message', handler);
145
+ return () => window.removeEventListener('message', handler);
146
+ }
147
+
148
+ function applyCssVarsImmediate(vars: Record<string, string>): void {
149
+ const root = document.documentElement;
150
+ for (const [key, value] of Object.entries(vars)) {
151
+ root.style.setProperty(key, value);
152
+ }
153
+ }