@explorer02/cfm-survey-sdk 0.3.9 → 0.4.0

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 (29) hide show
  1. package/dist/cli/index.js +35 -35
  2. package/dist/cli/index.mjs +35 -35
  3. package/package.json +1 -1
  4. package/templates/docs/templates/CsatMatrixScale.tsx +36 -23
  5. package/templates/docs/templates/CustomSliderTrack.tsx +2 -2
  6. package/templates/docs/templates/Header.tsx +14 -14
  7. package/templates/docs/templates/LikertMatrixScale.tsx +2 -2
  8. package/templates/docs/templates/Question.tsx +30 -23
  9. package/templates/docs/templates/RankOrderScale.tsx +10 -2
  10. package/templates/docs/templates/SliderMatrixScale.tsx +1 -1
  11. package/templates/docs/templates/uiConfig.ts +85 -0
  12. package/templates/preview-harness/preview-bridge.inline.js +55 -17
  13. package/templates/preview-harness/previewPages.js +2 -2
  14. package/templates/preview-harness/previewPages.ts +1 -1
  15. package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +8 -1
  16. package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +17 -96
  17. package/templates/preview-harness/vite-app/src/SurveyPagePreview.tsx +32 -8
  18. package/templates/preview-harness/vite-app/src/fixtures/questions.ts +47 -16
  19. package/templates/preview-harness/vite-app/src/globals.css +6 -0
  20. package/templates/preview-harness/vite-app/src/mergePreviewQuestion.ts +173 -0
  21. package/templates/preview-harness/vite-app/src/preview-live-overrides.css +22 -0
  22. package/templates/previewBridge.ts +61 -18
  23. package/templates/survey-theme.css +3 -0
  24. package/templates/wizard-dist/assets/{PreviewMock-CS4WqhDB.js → PreviewMock-BlOAvUCN.js} +1 -1
  25. package/templates/wizard-dist/assets/TypePanel-DcbMaw1b.js +1 -0
  26. package/templates/wizard-dist/assets/index-2Bi_7Fdh.js +34 -0
  27. package/templates/wizard-dist/index.html +1 -1
  28. package/templates/wizard-dist/assets/TypePanel-BgTW2c74.js +0 -1
  29. package/templates/wizard-dist/assets/index-hjm-fxWO.js +0 -34
@@ -36,6 +36,15 @@ function graphicsColumnLabelStyle(question: CsatMatrixQuestion) {
36
36
  return { fontSize: '12px', fontWeight: 500, lineHeight: 1.2, wordBreak: 'break-word' as const, ...pillStyle };
37
37
  }
38
38
 
39
+ /** Graphics uses anchor labels (bold top row) — per-column tick badges duplicate them for CSAT + RATING. */
40
+ function hasGraphicsAnchorLabels(labels: string[] | undefined): boolean {
41
+ return Boolean(labels?.some((label) => label.trim().length > 0));
42
+ }
43
+
44
+ function shouldShowGraphicsColumnLabelRow(isGraphics: boolean, labels: string[] | undefined): boolean {
45
+ return isGraphics && !hasGraphicsAnchorLabels(labels);
46
+ }
47
+
39
48
  function GraphicsColumnLabelsRow({
40
49
  scaleColumns,
41
50
  question,
@@ -47,8 +56,8 @@ function GraphicsColumnLabelsRow({
47
56
  const labelStyle = graphicsColumnLabelStyle(question);
48
57
 
49
58
  return (
50
- <div style={{ flex: 1, padding: '0 24px' }}>
51
- <div style={{ position: 'relative', margin: '0 16px', height: '16px' }}>
59
+ <div style={{ flex: 1, minWidth: 0, padding: '0 8px' }}>
60
+ <div style={{ position: 'relative', margin: '0 8px', height: '16px' }}>
52
61
  {scaleColumns.map((col, i) => {
53
62
  const percent = scaleColumns.length === 1 ? 0 : (i / (scaleColumns.length - 1)) * 100;
54
63
  return (
@@ -60,7 +69,7 @@ function GraphicsColumnLabelsRow({
60
69
  transform: 'translateX(-50%)',
61
70
  bottom: 0,
62
71
  textAlign: 'center',
63
- width: '64px',
72
+ maxWidth: '72px',
64
73
  }}
65
74
  >
66
75
  <span style={labelStyle} dangerouslySetInnerHTML={{ __html: col.label }} />
@@ -212,13 +221,13 @@ function CsatMatrixGrid({
212
221
  const n = labels?.length || 0;
213
222
 
214
223
  return (
215
- <div style={{ width: '100%' }}>
224
+ <div style={{ width: '100%', maxWidth: '100%', minWidth: 0, overflowX: 'auto', boxSizing: 'border-box' }}>
216
225
  {/* Labels row */}
217
226
  {showLabels && m > 0 && (
218
- <div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: '12px' }}>
219
- <div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0 }} />
220
- <div style={{ flex: 1, display: 'flex' }}>
221
- <div style={{ flex: 1, position: 'relative', height: '24px', ...(isGraphics ? { margin: '0 40px' } : {}) }}>
227
+ <div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: '12px', minWidth: 0 }}>
228
+ <div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0, minWidth: 0 }} />
229
+ <div style={{ flex: 1, display: 'flex', minWidth: 0 }}>
230
+ <div style={{ flex: 1, position: 'relative', minHeight: '24px', minWidth: 0, ...(isGraphics ? { margin: '0 8px' } : {}) }}>
222
231
  {labels!.map((label, i) => {
223
232
  const startPercent = isGraphics ? 0 : 0.5 / m;
224
233
  const rangePercent = isGraphics ? 1 : (m - 1) / m;
@@ -229,8 +238,10 @@ function CsatMatrixGrid({
229
238
  position: 'absolute',
230
239
  left: `${percent}%`,
231
240
  transform: 'translateX(-50%)',
232
- textAlign: 'center', fontSize: '13px', fontWeight: 600,
233
- lineHeight: 1.3, whiteSpace: 'nowrap',
241
+ textAlign: 'center', fontSize: isGraphics ? '12px' : '13px', fontWeight: 600,
242
+ lineHeight: 1.3,
243
+ whiteSpace: isGraphics ? 'normal' : 'nowrap',
244
+ maxWidth: isGraphics ? '72px' : undefined,
234
245
  ...anchorLabelStyle(question),
235
246
  }}>
236
247
  {label}
@@ -243,9 +254,9 @@ function CsatMatrixGrid({
243
254
  </div>
244
255
  )}
245
256
 
246
- {/* Column tick labels for graphics sliders (shown with or without anchor labels) */}
247
- {isGraphics && m > 0 && (
248
- <div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: showLabels ? '8px' : '16px', minHeight: '24px' }}>
257
+ {/* Column tick labels for graphics only when no anchor label row above */}
258
+ {shouldShowGraphicsColumnLabelRow(isGraphics, labels) && m > 0 && (
259
+ <div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: '16px', minHeight: '24px' }}>
249
260
  <div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0 }} />
250
261
  <div style={{ flex: 1, display: 'flex' }}>
251
262
  <GraphicsColumnLabelsRow scaleColumns={scaleColumns} question={question} />
@@ -285,15 +296,15 @@ function CsatMatrixGrid({
285
296
  {statementRows.map((row, rowIdx) => (
286
297
  <div key={row.id} style={{
287
298
  display: 'flex', alignItems: 'center', borderRadius: '8px',
288
- padding: '12px 0', ...matrixRowBackgroundStyle(rowIdx),
299
+ padding: '12px 0', minWidth: 0, ...matrixRowBackgroundStyle(rowIdx),
289
300
  }}>
290
- <div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0, padding: '0 16px' }}>
301
+ <div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0, minWidth: 0, padding: '0 12px' }}>
291
302
  <span style={{ fontSize: '14px', fontWeight: 500, color: '#111827', lineHeight: 1.4 }}
292
303
  dangerouslySetInnerHTML={{ __html: row.statementText }} />
293
304
  </div>
294
305
 
295
306
  {isGraphics ? (
296
- <div style={{ flex: 1, padding: '0 24px', position: 'relative', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
307
+ <div style={{ flex: 1, minWidth: 0, padding: '0 8px', position: 'relative', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
297
308
  <CustomSliderTrack
298
309
  min={0}
299
310
  max={scaleColumns.length - 1}
@@ -450,11 +461,11 @@ function CsatMatrixCarousel({
450
461
  const showLabels = labels && labels.length > 0;
451
462
 
452
463
  return (
453
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', backgroundColor: '#fff', borderRadius: '12px', border: '1px solid #e5e5e5', padding: '32px', boxShadow: '0 4px 6px -1px rgba(0,0,0,0.05)', width: '100%', boxSizing: 'border-box' }}>
464
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', backgroundColor: '#fff', borderRadius: '12px', border: '1px solid #e5e5e5', padding: '24px 16px', boxShadow: '0 4px 6px -1px rgba(0,0,0,0.05)', width: '100%', maxWidth: '100%', minWidth: 0, overflowX: 'auto', boxSizing: 'border-box' }}>
454
465
  <h3 style={{ fontSize: '18px', fontWeight: 500, color: '#111827', marginBottom: '32px', textAlign: 'center' }} dangerouslySetInnerHTML={{ __html: row.statementText }} />
455
466
 
456
467
  {showLabels && scaleColumns.length > 0 && (
457
- <div style={{ width: '100%', maxWidth: '800px', display: 'flex', position: 'relative', height: '24px', marginBottom: '16px' }}>
468
+ <div style={{ width: '100%', maxWidth: '100%', minWidth: 0, position: 'relative', height: '24px', marginBottom: '16px' }}>
458
469
  {labels!.map((label, i) => {
459
470
  const m = scaleColumns.length;
460
471
  const n = labels!.length;
@@ -465,8 +476,10 @@ function CsatMatrixCarousel({
465
476
  return (
466
477
  <div key={i} style={{
467
478
  position: 'absolute', left: `${percent}%`, transform: 'translateX(-50%)',
468
- textAlign: 'center', fontSize: '13px', fontWeight: 600,
469
- lineHeight: 1.3, whiteSpace: 'nowrap',
479
+ textAlign: 'center', fontSize: isGraphics ? '12px' : '13px', fontWeight: 600,
480
+ lineHeight: 1.3,
481
+ whiteSpace: isGraphics ? 'normal' : 'nowrap',
482
+ maxWidth: isGraphics ? '72px' : undefined,
470
483
  ...anchorLabelStyle(question),
471
484
  }}>
472
485
  {label}
@@ -476,14 +489,14 @@ function CsatMatrixCarousel({
476
489
  </div>
477
490
  )}
478
491
 
479
- {isGraphics && scaleColumns.length > 0 && (
480
- <div style={{ width: '100%', maxWidth: '800px', marginBottom: showLabels ? '8px' : '16px' }}>
492
+ {shouldShowGraphicsColumnLabelRow(isGraphics, labels) && scaleColumns.length > 0 && (
493
+ <div style={{ width: '100%', maxWidth: '100%', marginBottom: '16px' }}>
481
494
  <GraphicsColumnLabelsRow scaleColumns={scaleColumns} question={question} />
482
495
  </div>
483
496
  )}
484
497
 
485
498
  {isGraphics ? (
486
- <div style={{ width: '100%', maxWidth: '800px', marginBottom: '40px', padding: '0 24px' }}>
499
+ <div style={{ width: '100%', maxWidth: '100%', minWidth: 0, marginBottom: '40px', padding: '0 8px' }}>
487
500
  <CustomSliderTrack
488
501
  min={0} max={scaleColumns.length - 1} htmlStep={1}
489
502
  value={
@@ -56,7 +56,7 @@ export function CustomSliderTrack({
56
56
 
57
57
  return (
58
58
  <div
59
- style={{ position: 'relative', height: '32px', display: 'flex', alignItems: 'center', margin: '0 16px', opacity: disabled ? 0.5 : 1 }}
59
+ style={{ position: 'relative', height: '32px', display: 'flex', alignItems: 'center', margin: '0 8px', opacity: disabled ? 0.5 : 1 }}
60
60
  onMouseEnter={() => !disabled && setIsHovered(true)}
61
61
  onMouseLeave={() => { setIsHovered(false); setIsDragging(false); }}
62
62
  >
@@ -94,7 +94,7 @@ export function CustomSliderTrack({
94
94
  onTouchStart={() => !disabled && setIsDragging(true)}
95
95
  onTouchEnd={() => setIsDragging(false)}
96
96
  style={{
97
- position: 'absolute', top: '50%', left: '-16px', right: '-16px', width: 'calc(100% + 32px)', height: '100%',
97
+ position: 'absolute', top: '50%', left: '-8px', right: '-8px', width: 'calc(100% + 16px)', height: '100%',
98
98
  transform: 'translateY(-50%)', opacity: 0, cursor: disabled ? 'not-allowed' : 'pointer', zIndex: 10, margin: 0,
99
99
  }}
100
100
  />
@@ -76,20 +76,20 @@ export default function Header({ embedded = false }: HeaderProps) {
76
76
  {seedCompany}
77
77
  </span>
78
78
 
79
- {seedCompany ? (
80
- <span
81
- data-cfm-company
82
- className="font-semibold"
83
- style={{
84
- display: hasLogoFile && seedShowCompany ? undefined : 'none',
85
- color: 'var(--cfm-header-company-color, var(--cfm-text))',
86
- fontSize: 'var(--cfm-header-company-size, 18px)',
87
- fontWeight: 'var(--cfm-header-company-weight, 600)',
88
- }}
89
- >
90
- {seedCompany}
91
- </span>
92
- ) : null}
79
+ {/* Always in DOM for wizard bridge — visibility toggled via layoutFlags.showCompanyName */}
80
+ <span
81
+ data-cfm-company
82
+ className="font-semibold"
83
+ style={{
84
+ display:
85
+ seedCompany && hasLogoFile && seedShowCompany ? undefined : 'none',
86
+ color: 'var(--cfm-header-company-color, var(--cfm-text))',
87
+ fontSize: 'var(--cfm-header-company-size, 18px)',
88
+ fontWeight: 'var(--cfm-header-company-weight, 600)',
89
+ }}
90
+ >
91
+ {seedCompany}
92
+ </span>
93
93
  </div>
94
94
  </div>
95
95
  </header>
@@ -248,7 +248,7 @@ function LikertMatrixGridLayout({
248
248
  };
249
249
 
250
250
  return (
251
- <div style={{ width: '100%' }}>
251
+ <div style={{ width: '100%', maxWidth: '100%', minWidth: 0, overflowX: 'auto', boxSizing: 'border-box' }}>
252
252
  {!repeatColumnHeaders && renderHeader()}
253
253
 
254
254
  <div style={{ display: 'flex', flexDirection: 'column', gap: repeatColumnHeaders ? '24px' : '0' }}>
@@ -326,7 +326,7 @@ function LikertMatrixGridLayout({
326
326
  </div>
327
327
 
328
328
  {isBipolar && (
329
- <div style={{ flex: '0 0 25%', padding: '0 16px', textAlign: 'right', boxSizing: 'border-box' }}>
329
+ <div style={{ flex: `0 0 ${BIPOLAR_COL_WIDTH}`, padding: '0 16px', textAlign: 'right', boxSizing: 'border-box' }}>
330
330
  <span style={{ fontSize: '14px', fontWeight: 500, color: '#111827', lineHeight: 1.4, wordWrap: 'break-word', display: 'block' }}
331
331
  dangerouslySetInnerHTML={{ __html: transposeTable ? '' : ('oppositeStatementText' in rowItem ? rowItem.oppositeStatementText ?? '' : '') }} />
332
332
  </div>
@@ -19,7 +19,7 @@ import { FileUploadScale } from './FileUploadScale';
19
19
  import { HeatmapScale } from './HeatmapScale';
20
20
  import { RankOrderScale } from './RankOrderScale';
21
21
  import RatingScale from './RatingScale';
22
- import { getLayoutFlags } from '@/lib/uiConfig';
22
+ import { showQuestionNumbersInChrome, showRequiredAsteriskInChrome, resolveNpsButtonStyle, resolveNpsHintMinLabel, resolveNpsHintMaxLabel } from '@/lib/uiConfig';
23
23
  import { hintLabelStyle } from '@/lib/surveyUi/labelStyles';
24
24
  import {
25
25
  mcqCheckboxControlStyle,
@@ -48,7 +48,8 @@ export default function Question({
48
48
  customFieldValues = {},
49
49
  onSelect,
50
50
  }: QuestionProps) {
51
- const { showQuestionNumbers, showRequiredAsterisk } = getLayoutFlags();
51
+ const showNumbers = showQuestionNumbersInChrome();
52
+ const showAsterisk = showRequiredAsteriskInChrome();
52
53
 
53
54
  const questionsById = useMemo(
54
55
  () => new Map(allQuestions.map(q => [q.id, q])),
@@ -125,31 +126,35 @@ export default function Question({
125
126
  borderRadius: 'var(--cfm-border-radius, 12px)',
126
127
  fontFamily: 'var(--cfm-font-family)',
127
128
  marginBottom: 'var(--cfm-card-gap, 32px)',
129
+ maxWidth: '100%',
130
+ overflowX: 'auto',
131
+ boxSizing: 'border-box',
128
132
  }}
129
133
  >
130
134
  <h2
131
135
  className="text-lg font-medium leading-relaxed"
132
136
  style={{ color: 'var(--cfm-text, #111827)' }}
133
137
  >
134
- {question.questionNumber != null && (
135
- <span
136
- data-cfm-question-number
137
- className="mr-1 font-semibold"
138
- style={{ display: showQuestionNumbers ? undefined : 'none' }}
139
- >
140
- {question.questionNumber}.
141
- </span>
142
- )}
138
+ <span
139
+ data-cfm-question-number
140
+ className="mr-1 font-semibold"
141
+ style={{
142
+ display:
143
+ question.questionNumber != null && showNumbers ? undefined : 'none',
144
+ }}
145
+ >
146
+ {question.questionNumber != null ? `${question.questionNumber}.` : ''}
147
+ </span>
143
148
  <span dangerouslySetInnerHTML={{ __html: displayQuestionText }} />
144
- {question.required && (
145
- <span
146
- data-cfm-required
147
- className="ml-1 text-red-500"
148
- style={{ display: showRequiredAsterisk ? undefined : 'none' }}
149
- >
150
- *
151
- </span>
152
- )}
149
+ <span
150
+ data-cfm-required
151
+ className="ml-1 text-red-500"
152
+ style={{
153
+ display: question.required && showAsterisk ? undefined : 'none',
154
+ }}
155
+ >
156
+ *
157
+ </span>
153
158
  </h2>
154
159
  {displayQuestionDescription && (
155
160
  <div
@@ -165,7 +170,8 @@ export default function Question({
165
170
  <span
166
171
  className="absolute left-0 top-0 max-w-[30%] text-left leading-tight"
167
172
  style={{ ...hintLabelStyle(), padding: '2px 6px', borderRadius: '4px' }}
168
- dangerouslySetInnerHTML={{ __html: question.minLabel ?? '' }}
173
+ data-cfm-hint-min
174
+ dangerouslySetInnerHTML={{ __html: resolveNpsHintMinLabel(question.minLabel ?? '') }}
169
175
  />
170
176
  {question.midLabel && (
171
177
  <span
@@ -184,7 +190,8 @@ export default function Question({
184
190
  <span
185
191
  className="absolute right-0 top-0 max-w-[30%] text-right leading-tight"
186
192
  style={{ ...hintLabelStyle(), padding: '2px 6px', borderRadius: '4px' }}
187
- dangerouslySetInnerHTML={{ __html: question.maxLabel ?? '' }}
193
+ data-cfm-hint-max
194
+ dangerouslySetInnerHTML={{ __html: resolveNpsHintMaxLabel(question.maxLabel ?? '') }}
188
195
  />
189
196
  </div>
190
197
  )}
@@ -194,7 +201,7 @@ export default function Question({
194
201
  selectedValue={selectedValue}
195
202
  onSelect={onSelect}
196
203
  variant="nps"
197
- buttonStyle={question.buttonStyle}
204
+ buttonStyle={resolveNpsButtonStyle(question.buttonStyle)}
198
205
  />
199
206
  </div>
200
207
  )}
@@ -182,8 +182,16 @@ function DropdownRankLayout({
182
182
  aria-label={`Rank for ${option.optionLabel || option.id}`}
183
183
  value={currentRank ?? ''}
184
184
  onChange={event => handleRankChange(option.id, event.target.value)}
185
- className="h-10 w-16 shrink-0 rounded border bg-white px-2 text-sm outline-none"
186
- style={{ borderColor: 'var(--cfm-rank-select-border, #d1d5db)' }}
185
+ className="h-10 w-16 shrink-0 rounded border px-2 text-sm font-semibold outline-none"
186
+ style={{
187
+ borderColor: 'var(--cfm-rank-select-border, #d1d5db)',
188
+ backgroundColor: currentRank
189
+ ? 'var(--cfm-rank-badge-bg, var(--cfm-mcq-selected-bg, #fdf2f8))'
190
+ : '#ffffff',
191
+ color: currentRank
192
+ ? 'var(--cfm-rank-badge-text, var(--cfm-mcq-selected-accent, var(--cfm-primary, #e20074)))'
193
+ : '#374151',
194
+ }}
187
195
  onFocus={e => {
188
196
  e.currentTarget.style.borderColor = focusRingVar;
189
197
  e.currentTarget.style.boxShadow = `0 0 0 1px ${focusRingVar}`;
@@ -168,7 +168,7 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
168
168
  };
169
169
 
170
170
  return (
171
- <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
171
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', width: '100%', maxWidth: '100%', minWidth: 0, overflowX: 'auto', boxSizing: 'border-box' }}>
172
172
  {renderHeaderRow()}
173
173
 
174
174
  {statementRows.map(row => {
@@ -82,6 +82,29 @@ export function getLayoutFlags() {
82
82
  };
83
83
  }
84
84
 
85
+ function readLiveChromeFlag(cssVar: string, configFallback: boolean): boolean {
86
+ if (typeof window === 'undefined') return configFallback;
87
+ const root = document.documentElement;
88
+ const live =
89
+ root.style.getPropertyValue(cssVar).trim() ||
90
+ getComputedStyle(root).getPropertyValue(cssVar).trim();
91
+ if (live === '0') return false;
92
+ if (live === '1') return true;
93
+ return configFallback;
94
+ }
95
+
96
+ /** Wizard live preview reads --cfm-show-question-numbers; production uses survey-ui-config.json. */
97
+ export function showQuestionNumbersInChrome(): boolean {
98
+ const flags = getLayoutFlags();
99
+ return readLiveChromeFlag('--cfm-show-question-numbers', flags.showQuestionNumbers);
100
+ }
101
+
102
+ /** Wizard live preview reads --cfm-show-required-asterisk; production uses survey-ui-config.json. */
103
+ export function showRequiredAsteriskInChrome(): boolean {
104
+ const flags = getLayoutFlags();
105
+ return readLiveChromeFlag('--cfm-show-required-asterisk', flags.showRequiredAsterisk);
106
+ }
107
+
85
108
  export function getPlaceholders(): Record<string, string> {
86
109
  return { ...(surveyUiConfig.placeholders ?? {}) };
87
110
  }
@@ -89,3 +112,65 @@ export function getPlaceholders(): Record<string, string> {
89
112
  export function isDebugEnabled(): boolean {
90
113
  return surveyUiConfig.debug?.enabled === true;
91
114
  }
115
+
116
+ export type NpsButtonStyle = 'standard' | 'numbered' | 'emoji' | 'pill';
117
+
118
+ function readLiveNpsButtonStyle(): NpsButtonStyle | null {
119
+ if (typeof window === 'undefined') return null;
120
+ const root = document.documentElement;
121
+ const attr = root.getAttribute('data-cfm-nps-button-style');
122
+ if (attr === 'standard' || attr === 'numbered' || attr === 'emoji' || attr === 'pill') {
123
+ return attr;
124
+ }
125
+ const live =
126
+ root.style.getPropertyValue('--cfm-nps-button-style').trim() ||
127
+ getComputedStyle(root).getPropertyValue('--cfm-nps-button-style').trim();
128
+ if (live === 'standard' || live === 'numbered' || live === 'emoji' || live === 'pill') {
129
+ return live;
130
+ }
131
+ return null;
132
+ }
133
+
134
+ /** Wizard bridge + survey-ui-config.json override API `buttonStyle` for NPS cells. */
135
+ export function resolveNpsButtonStyle(questionStyle?: string): NpsButtonStyle {
136
+ const live = readLiveNpsButtonStyle();
137
+ if (live) return live;
138
+
139
+ const fromJson = (
140
+ surveyUiConfig.questionTypes as { NPS_SCALE?: { buttonStyle?: string } } | undefined
141
+ )?.NPS_SCALE?.buttonStyle;
142
+ if (
143
+ fromJson === 'standard' ||
144
+ fromJson === 'numbered' ||
145
+ fromJson === 'emoji' ||
146
+ fromJson === 'pill'
147
+ ) {
148
+ return fromJson;
149
+ }
150
+ if (
151
+ questionStyle === 'standard' ||
152
+ questionStyle === 'numbered' ||
153
+ questionStyle === 'emoji' ||
154
+ questionStyle === 'pill'
155
+ ) {
156
+ return questionStyle;
157
+ }
158
+ return 'numbered';
159
+ }
160
+
161
+ function resolveNpsHintText(kind: 'min' | 'max', questionFallback: string): string {
162
+ const nps = (
163
+ surveyUiConfig.questionTypes as { NPS_SCALE?: { hintMinText?: string; hintMaxText?: string } } | undefined
164
+ )?.NPS_SCALE;
165
+ const fromJson = kind === 'min' ? nps?.hintMinText : nps?.hintMaxText;
166
+ const trimmed = fromJson?.trim();
167
+ return trimmed || questionFallback;
168
+ }
169
+
170
+ export function resolveNpsHintMinLabel(questionFallback = ''): string {
171
+ return resolveNpsHintText('min', questionFallback);
172
+ }
173
+
174
+ export function resolveNpsHintMaxLabel(questionFallback = ''): string {
175
+ return resolveNpsHintText('max', questionFallback);
176
+ }
@@ -17,10 +17,12 @@
17
17
  delete pending[k];
18
18
  }
19
19
  }
20
- applyThemeSideEffects();
21
- }
20
+ applyThemeSideEffects();
21
+ syncCompanyNameVisibility();
22
+ syncQuestionChromeVisibility();
23
+ }
22
24
 
23
- function queue(vars) {
25
+ function queue(vars) {
24
26
  for (var k in vars) {
25
27
  if (Object.prototype.hasOwnProperty.call(vars, k)) pending[k] = vars[k];
26
28
  }
@@ -34,10 +36,12 @@
34
36
  root.style.setProperty(k, vars[k]);
35
37
  }
36
38
  }
37
- applyThemeSideEffects();
38
- }
39
+ applyThemeSideEffects();
40
+ syncCompanyNameVisibility();
41
+ syncQuestionChromeVisibility();
42
+ }
39
43
 
40
- function setDisplay(selector, visible) {
44
+ function setDisplay(selector, visible) {
41
45
  document.querySelectorAll(selector).forEach(function (el) {
42
46
  el.style.display = visible ? '' : 'none';
43
47
  });
@@ -81,6 +85,46 @@
81
85
  applyDropzoneStyle();
82
86
  }
83
87
 
88
+ function isHeaderLogoVisible() {
89
+ var logoImg = document.querySelector('header [data-cfm-logo], .cfm-header [data-cfm-logo]');
90
+ return Boolean(
91
+ logoImg &&
92
+ logoImg.style.display !== 'none' &&
93
+ logoImg.getAttribute('src'),
94
+ );
95
+ }
96
+
97
+ function syncCompanyNameVisibility(layoutShowCompany) {
98
+ var showFromCss = readVar('--cfm-header-show-company', '1') !== '0';
99
+ var showToggle =
100
+ layoutShowCompany !== undefined ? !!layoutShowCompany : showFromCss;
101
+
102
+ document.querySelectorAll('[data-cfm-company]').forEach(function (el) {
103
+ var hasName = Boolean((el.textContent || '').trim());
104
+ var show = showToggle && hasName && isHeaderLogoVisible();
105
+ el.style.display = show ? '' : 'none';
106
+ });
107
+ }
108
+
109
+ function syncQuestionChromeVisibility(layoutShowNumbers, layoutShowRequired) {
110
+ var showNumbers =
111
+ layoutShowNumbers !== undefined
112
+ ? !!layoutShowNumbers
113
+ : readVar('--cfm-show-question-numbers', '1') !== '0';
114
+ var showRequired =
115
+ layoutShowRequired !== undefined
116
+ ? !!layoutShowRequired
117
+ : readVar('--cfm-show-required-asterisk', '1') !== '0';
118
+
119
+ document.querySelectorAll('[data-cfm-question-number]').forEach(function (el) {
120
+ var hasNumber = Boolean((el.textContent || '').trim());
121
+ el.style.display = showNumbers && hasNumber ? '' : 'none';
122
+ });
123
+ document.querySelectorAll('[data-cfm-required]').forEach(function (el) {
124
+ el.style.display = showRequired ? '' : 'none';
125
+ });
126
+ }
127
+
84
128
  function applyContent(patch) {
85
129
  if (!patch) return;
86
130
  if (patch.logoUrl !== undefined) {
@@ -103,19 +147,11 @@
103
147
  el.classList.remove('hidden');
104
148
  }
105
149
  });
106
- document.querySelectorAll('[data-cfm-company]').forEach(function (el) {
107
- if (patch.logoUrl && !el.textContent) {
108
- el.style.display = 'none';
109
- }
110
- });
111
150
  }
112
151
  if (patch.companyName !== undefined) {
113
152
  var name = patch.companyName || '';
114
153
  document.querySelectorAll('[data-cfm-company]').forEach(function (el) {
115
154
  el.textContent = name;
116
- if (!name) {
117
- el.style.display = 'none';
118
- }
119
155
  });
120
156
  document.querySelectorAll('[data-cfm-logo-text]').forEach(function (el) {
121
157
  var logoImg = document.querySelector('[data-cfm-logo]');
@@ -176,7 +212,7 @@
176
212
  if (patch.layoutFlags) {
177
213
  var flags = patch.layoutFlags;
178
214
  if (flags.showCompanyName !== undefined) {
179
- setDisplay('[data-cfm-company]', flags.showCompanyName);
215
+ syncCompanyNameVisibility(flags.showCompanyName);
180
216
  }
181
217
  if (flags.showHeader !== undefined) {
182
218
  setDisplay('.cfm-header, header.cfm-header', flags.showHeader);
@@ -197,10 +233,10 @@
197
233
  setDisplay('[data-cfm-btn-back]', flags.showBackButton);
198
234
  }
199
235
  if (flags.showQuestionNumbers !== undefined) {
200
- setDisplay('[data-cfm-question-number]', flags.showQuestionNumbers);
236
+ syncQuestionChromeVisibility(flags.showQuestionNumbers, undefined);
201
237
  }
202
238
  if (flags.showRequiredAsterisk !== undefined) {
203
- setDisplay('[data-cfm-required]', flags.showRequiredAsterisk);
239
+ syncQuestionChromeVisibility(undefined, flags.showRequiredAsterisk);
204
240
  }
205
241
  if (flags.showFooterLogo !== undefined) {
206
242
  setDisplay('[data-cfm-footer-logo]', flags.showFooterLogo);
@@ -234,6 +270,8 @@
234
270
  applyMultiStatement(!!patch.previewState.multiStatement);
235
271
  }
236
272
  }
273
+ syncCompanyNameVisibility();
274
+ syncQuestionChromeVisibility();
237
275
  applyThemeSideEffects();
238
276
  }
239
277
 
@@ -97,8 +97,8 @@ const PREVIEW_PAGE_DEFS = [
97
97
  { key: 'TEXTFIELD_short', title: 'Text Short', body: `<p style="font-weight:600;margin-bottom:8px">Your name</p><input class="cfm-input" data-cfm-textfield placeholder="Type here..." />` },
98
98
  { key: 'TEXTFIELD_long', title: 'Text Long', body: `<p style="font-weight:600;margin-bottom:8px">Comments</p><textarea class="cfm-input" data-cfm-textfield style="height:120px;padding:12px" placeholder="Type your response..."></textarea>` },
99
99
  { key: 'NPS_SCALE', title: 'NPS', body: npsPreview() },
100
- { key: 'CFM_likert', title: 'CFM Likert', body: `${matrixPreview('Statement item 1', 'radio')}<div class="cfm-matrix-row" data-cfm-matrix-row-extra style="display:none"><div class="cfm-matrix-label">Statement item 2</div><div class="cfm-matrix-cells">${cells(5)}</div></div>` },
101
- { key: 'CFM_bipolar', title: 'CFM Bipolar', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width,25%)">Left statement</div><div class="cfm-matrix-cells">${cells(5)}</div><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width,25%);text-align:right">Right statement</div></div>` },
100
+ { key: 'CFM_likert', title: 'CFM Likert', body: `${matrixPreview('Statement item 1', 'radio')}<div class="cfm-matrix-row" data-cfm-matrix-row-extra style="display:none"><div class="cfm-matrix-label">Statement item 2</div><div class="cfm-matrix-cells">${cells(5)}</div></div><div class="cfm-matrix-row" data-cfm-matrix-row-extra style="display:none"><div class="cfm-matrix-label">Statement item 3</div><div class="cfm-matrix-cells">${cells(5)}</div></div>` },
101
+ { key: 'CFM_bipolar', title: 'CFM Bipolar', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width,25%)">Statement 1</div><div class="cfm-matrix-cells">${cells(5)}</div><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width,25%);text-align:right">Opposite 1</div></div><div class="cfm-matrix-row" data-cfm-matrix-row-extra style="display:none"><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width,25%)">Statement 2</div><div class="cfm-matrix-cells">${cells(5)}</div><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width,25%);text-align:right">Opposite 2</div></div><div class="cfm-matrix-row" data-cfm-matrix-row-extra style="display:none"><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width,25%)">Statement 3</div><div class="cfm-matrix-cells">${cells(5)}</div><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width,25%);text-align:right">Opposite 3</div></div>` },
102
102
  { key: 'CFM_transpose', title: 'CFM Transpose', body: matrixPreview('Scale item (transposed row)', 'radio') },
103
103
  { key: 'CFM_carousel', title: 'CFM Carousel', body: `${matrixPreview('Row 1 of 3', 'radio')}<div style="text-align:center;margin-top:12px"><span style="color:var(--cfm-matrix-selected)">●</span> ○ ○</div>` },
104
104
  { key: 'CFM_dropdown', title: 'CFM Dropdown', body: `<div class="cfm-matrix-label" style="width:100%;margin-bottom:8px">Statement 1</div><select class="cfm-input"><option>Select...</option><option>Col 1</option><option>Col 2</option></select>` },
@@ -141,7 +141,7 @@ export const PREVIEW_PAGE_DEFS: PreviewPageDef[] = [
141
141
  {
142
142
  key: 'CFM_bipolar',
143
143
  title: 'CFM Bipolar',
144
- body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width)">Left statement</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 statement</div></div>`,
144
+ body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width)">Statement 1</div><div class="cfm-matrix-cells">${cells(5)}</div><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width);text-align:right">Opposite 1</div></div><div class="cfm-matrix-row" data-cfm-matrix-row-extra style="display:none"><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width)">Statement 2</div><div class="cfm-matrix-cells">${cells(5)}</div><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width);text-align:right">Opposite 2</div></div><div class="cfm-matrix-row" data-cfm-matrix-row-extra style="display:none"><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width)">Statement 3</div><div class="cfm-matrix-cells">${cells(5)}</div><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width);text-align:right">Opposite 3</div></div>`,
145
145
  },
146
146
  {
147
147
  key: 'CFM_transpose',
@@ -70,14 +70,19 @@ export function PreviewConfigProvider({ children }: { children: ReactNode }) {
70
70
  const data = event.data;
71
71
  if (!data?.type) return;
72
72
  if (data.type !== PREVIEW_BRIDGE_SNAPSHOT && data.type !== PREVIEW_BRIDGE_DELTA) return;
73
+
74
+ let shouldBumpRevision = false;
75
+
73
76
  if (data.contentPatch?.previewState) {
74
77
  setPreviewState((prev) => ({ ...prev, ...data.contentPatch.previewState }));
78
+ shouldBumpRevision = true;
75
79
  }
76
80
  if (data.contentPatch?.domAttributes) {
77
81
  const root = document.documentElement;
78
82
  for (const [key, value] of Object.entries(data.contentPatch.domAttributes)) {
79
83
  root.setAttribute(key, value as string);
80
84
  }
85
+ shouldBumpRevision = true;
81
86
  }
82
87
  if (data.cssVars) {
83
88
  const root = document.documentElement;
@@ -85,7 +90,9 @@ export function PreviewConfigProvider({ children }: { children: ReactNode }) {
85
90
  root.style.setProperty(key, value as string);
86
91
  }
87
92
  }
88
- setConfigRevision((r) => r + 1);
93
+ if (shouldBumpRevision) {
94
+ setConfigRevision((r) => r + 1);
95
+ }
89
96
  applyThemeSideEffects();
90
97
  };
91
98