@explorer02/cfm-survey-sdk 0.3.8 → 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 +60 -45
  5. package/templates/docs/templates/CustomSliderTrack.tsx +2 -2
  6. package/templates/docs/templates/Header.tsx +14 -13
  7. package/templates/docs/templates/LikertMatrixScale.tsx +2 -2
  8. package/templates/docs/templates/Question.tsx +31 -13
  9. package/templates/docs/templates/RankOrderScale.tsx +10 -2
  10. package/templates/docs/templates/SliderMatrixScale.tsx +3 -4
  11. package/templates/docs/templates/uiConfig.ts +89 -2
  12. package/templates/preview-harness/preview-bridge.inline.js +55 -15
  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 +72 -47
  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 -14
  23. package/templates/survey-theme.css +4 -1
  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
@@ -26,8 +26,23 @@ function anchorLabelStyle(question: CsatMatrixQuestion) {
26
26
  return question.type === 'RATING_MATRIX' ? ratingColumnLabelStyle() : csatColumnLabelStyle();
27
27
  }
28
28
 
29
- function columnHeaderLabelStyle(_question: CsatMatrixQuestion) {
30
- return { fontSize: '13px', fontWeight: 500, textAlign: 'center' as const, lineHeight: 1.2, color: '#4b5563' };
29
+ function columnHeaderLabelStyle(question: CsatMatrixQuestion) {
30
+ const pillStyle = question.type === 'RATING_MATRIX' ? ratingColumnLabelStyle() : csatColumnLabelStyle();
31
+ return { fontSize: '13px', fontWeight: 500, textAlign: 'center' as const, lineHeight: 1.2, ...pillStyle };
32
+ }
33
+
34
+ function graphicsColumnLabelStyle(question: CsatMatrixQuestion) {
35
+ const pillStyle = question.type === 'RATING_MATRIX' ? ratingColumnLabelStyle() : csatColumnLabelStyle();
36
+ return { fontSize: '12px', fontWeight: 500, lineHeight: 1.2, wordBreak: 'break-word' as const, ...pillStyle };
37
+ }
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);
31
46
  }
32
47
 
33
48
  function GraphicsColumnLabelsRow({
@@ -38,17 +53,11 @@ function GraphicsColumnLabelsRow({
38
53
  question: CsatMatrixQuestion;
39
54
  }) {
40
55
  if (scaleColumns.length === 0) return null;
41
- const labelStyle = {
42
- fontSize: '12px',
43
- fontWeight: 500,
44
- lineHeight: 1.2,
45
- wordBreak: 'break-word' as const,
46
- color: '#4b5563',
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} />
@@ -256,8 +267,8 @@ function CsatMatrixGrid({
256
267
  </div>
257
268
  )}
258
269
 
259
- {/* Column Headers for non-graphics star format always shows labels above stars */}
260
- {!isGraphics && (!showLabels || isStar) && m > 0 && (
270
+ {/* Column Headers for non-graphics when no anchor labels */}
271
+ {!isGraphics && !showLabels && m > 0 && (
261
272
  <div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: '16px', minHeight: '24px' }}>
262
273
  <div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0 }} />
263
274
  <div style={{ flex: 1, display: 'flex' }}>
@@ -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}
@@ -364,16 +375,13 @@ function CsatMatrixGrid({
364
375
  ...(isSelected
365
376
  ? scaleCellSelectedStyle(true)
366
377
  : {
367
- border: '1px solid #d1d5db',
368
- backgroundColor: '#fff',
369
- color: '#111827',
370
- ...unselectedOpacityStyle(),
371
- }),
378
+ border: '1px solid #d1d5db',
379
+ backgroundColor: '#fff',
380
+ color: '#111827',
381
+ ...unselectedOpacityStyle(),
382
+ }),
372
383
  transition: 'all 0.15s',
373
- } : {
374
- padding: '8px',
375
- ...(isSelected ? {} : unselectedOpacityStyle()),
376
- }),
384
+ } : { padding: '8px', ...(!isSelected ? unselectedOpacityStyle() : {}) }),
377
385
  }}
378
386
  >
379
387
  {(EmojiNode ?? StarNode ?? (isNumbered ? (displayIdx + 1) : RadioNode)) as React.ReactNode}
@@ -453,11 +461,11 @@ function CsatMatrixCarousel({
453
461
  const showLabels = labels && labels.length > 0;
454
462
 
455
463
  return (
456
- <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' }}>
457
465
  <h3 style={{ fontSize: '18px', fontWeight: 500, color: '#111827', marginBottom: '32px', textAlign: 'center' }} dangerouslySetInnerHTML={{ __html: row.statementText }} />
458
466
 
459
467
  {showLabels && scaleColumns.length > 0 && (
460
- <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' }}>
461
469
  {labels!.map((label, i) => {
462
470
  const m = scaleColumns.length;
463
471
  const n = labels!.length;
@@ -468,8 +476,10 @@ function CsatMatrixCarousel({
468
476
  return (
469
477
  <div key={i} style={{
470
478
  position: 'absolute', left: `${percent}%`, transform: 'translateX(-50%)',
471
- textAlign: 'center', fontSize: '13px', fontWeight: 600,
472
- 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,
473
483
  ...anchorLabelStyle(question),
474
484
  }}>
475
485
  {label}
@@ -479,14 +489,14 @@ function CsatMatrixCarousel({
479
489
  </div>
480
490
  )}
481
491
 
482
- {isGraphics && scaleColumns.length > 0 && (
483
- <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' }}>
484
494
  <GraphicsColumnLabelsRow scaleColumns={scaleColumns} question={question} />
485
495
  </div>
486
496
  )}
487
497
 
488
498
  {isGraphics ? (
489
- <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' }}>
490
500
  <CustomSliderTrack
491
501
  min={0} max={scaleColumns.length - 1} htmlStep={1}
492
502
  value={
@@ -539,8 +549,13 @@ function CsatMatrixCarousel({
539
549
  width: '40px', height: '40px', borderRadius: '8px', fontSize: '14px', fontWeight: 500,
540
550
  ...(isSelected
541
551
  ? scaleCellSelectedStyle(true)
542
- : { border: '1px solid #d1d5db', backgroundColor: '#fff', color: '#111827' }),
543
- } : { padding: '8px' }),
552
+ : {
553
+ border: '1px solid #d1d5db',
554
+ backgroundColor: '#fff',
555
+ color: '#111827',
556
+ ...unselectedOpacityStyle(),
557
+ }),
558
+ } : { padding: '8px', ...(!isSelected ? unselectedOpacityStyle() : {}) }),
544
559
  }}
545
560
  >
546
561
  {(EmojiNode ?? StarNode ?? (isNumbered ? (displayIdx + 1) : RadioNode)) as React.ReactNode}
@@ -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,19 +76,20 @@ export default function Header({ embedded = false }: HeaderProps) {
76
76
  {seedCompany}
77
77
  </span>
78
78
 
79
- {seedShowCompany && hasLogoFile && seedCompany ? (
80
- <span
81
- data-cfm-company
82
- className="font-semibold"
83
- style={{
84
- color: 'var(--cfm-header-company-color, var(--cfm-text))',
85
- fontSize: 'var(--cfm-header-company-size, 18px)',
86
- fontWeight: 'var(--cfm-header-company-weight, 600)',
87
- }}
88
- >
89
- {seedCompany}
90
- </span>
91
- ) : 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>
92
93
  </div>
93
94
  </div>
94
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,6 +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 { showQuestionNumbersInChrome, showRequiredAsteriskInChrome, resolveNpsButtonStyle, resolveNpsHintMinLabel, resolveNpsHintMaxLabel } from '@/lib/uiConfig';
22
23
  import { hintLabelStyle } from '@/lib/surveyUi/labelStyles';
23
24
  import {
24
25
  mcqCheckboxControlStyle,
@@ -47,6 +48,9 @@ export default function Question({
47
48
  customFieldValues = {},
48
49
  onSelect,
49
50
  }: QuestionProps) {
51
+ const showNumbers = showQuestionNumbersInChrome();
52
+ const showAsterisk = showRequiredAsteriskInChrome();
53
+
50
54
  const questionsById = useMemo(
51
55
  () => new Map(allQuestions.map(q => [q.id, q])),
52
56
  [allQuestions]
@@ -122,23 +126,35 @@ export default function Question({
122
126
  borderRadius: 'var(--cfm-border-radius, 12px)',
123
127
  fontFamily: 'var(--cfm-font-family)',
124
128
  marginBottom: 'var(--cfm-card-gap, 32px)',
129
+ maxWidth: '100%',
130
+ overflowX: 'auto',
131
+ boxSizing: 'border-box',
125
132
  }}
126
133
  >
127
134
  <h2
128
135
  className="text-lg font-medium leading-relaxed"
129
136
  style={{ color: 'var(--cfm-text, #111827)' }}
130
137
  >
131
- {question.questionNumber != null && (
132
- <span data-cfm-question-number className="mr-1 font-semibold">
133
- {question.questionNumber}.
134
- </span>
135
- )}
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>
136
148
  <span dangerouslySetInnerHTML={{ __html: displayQuestionText }} />
137
- {question.required && (
138
- <span data-cfm-required className="ml-1 text-red-500">
139
- *
140
- </span>
141
- )}
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>
142
158
  </h2>
143
159
  {displayQuestionDescription && (
144
160
  <div
@@ -154,7 +170,8 @@ export default function Question({
154
170
  <span
155
171
  className="absolute left-0 top-0 max-w-[30%] text-left leading-tight"
156
172
  style={{ ...hintLabelStyle(), padding: '2px 6px', borderRadius: '4px' }}
157
- dangerouslySetInnerHTML={{ __html: question.minLabel ?? '' }}
173
+ data-cfm-hint-min
174
+ dangerouslySetInnerHTML={{ __html: resolveNpsHintMinLabel(question.minLabel ?? '') }}
158
175
  />
159
176
  {question.midLabel && (
160
177
  <span
@@ -173,7 +190,8 @@ export default function Question({
173
190
  <span
174
191
  className="absolute right-0 top-0 max-w-[30%] text-right leading-tight"
175
192
  style={{ ...hintLabelStyle(), padding: '2px 6px', borderRadius: '4px' }}
176
- dangerouslySetInnerHTML={{ __html: question.maxLabel ?? '' }}
193
+ data-cfm-hint-max
194
+ dangerouslySetInnerHTML={{ __html: resolveNpsHintMaxLabel(question.maxLabel ?? '') }}
177
195
  />
178
196
  </div>
179
197
  )}
@@ -183,7 +201,7 @@ export default function Question({
183
201
  selectedValue={selectedValue}
184
202
  onSelect={onSelect}
185
203
  variant="nps"
186
- buttonStyle={question.buttonStyle}
204
+ buttonStyle={resolveNpsButtonStyle(question.buttonStyle)}
187
205
  />
188
206
  </div>
189
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}`;
@@ -104,7 +104,6 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
104
104
  fontSize: '14px',
105
105
  fontWeight: 400,
106
106
  height: '28px',
107
- ...sliderTickLabelStyle(),
108
107
  }}
109
108
  >
110
109
  {tickValues.length > 0 ? (
@@ -117,7 +116,7 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
117
116
  width: `${sliceWidth}%`, display: 'flex', justifyContent: 'center',
118
117
  }}>
119
118
  {sliderType === 'graphics' ? (
120
- <div style={{ wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2', fontWeight: 500, color: '#4b5563', fontSize: '13px' }}>
119
+ <div style={{ ...sliderTickLabelStyle(), wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2', fontWeight: 500, fontSize: '13px' }}>
121
120
  {tv.label}
122
121
  </div>
123
122
  ) : (
@@ -150,7 +149,7 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
150
149
  position: 'absolute', left: `${percentage}%`, transform: 'translateX(-50%)',
151
150
  width: `${sliceWidth}%`, display: 'flex', justifyContent: 'center',
152
151
  }}>
153
- <span style={{ wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2' }}>{m}</span>
152
+ <span style={{ ...sliderTickLabelStyle(), wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2' }}>{m}</span>
154
153
  </div>
155
154
  );
156
155
  })
@@ -169,7 +168,7 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
169
168
  };
170
169
 
171
170
  return (
172
- <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' }}>
173
172
  {renderHeaderRow()}
174
173
 
175
174
  {statementRows.map(row => {
@@ -17,8 +17,10 @@ export function getCompanyName(): string {
17
17
  }
18
18
 
19
19
  export function getLogoFileName(): string | null {
20
- const fileName = surveyUiConfig.global?.logo?.fileName;
21
- return fileName?.trim() ? fileName : null;
20
+ const fileName = surveyUiConfig.global?.logo?.fileName as string | null | undefined;
21
+ if (typeof fileName !== 'string') return null;
22
+ const trimmed = fileName.trim();
23
+ return trimmed ? trimmed : null;
22
24
  }
23
25
 
24
26
  /** Logo src for Header/Footer img — relative for AWS static export. */
@@ -80,6 +82,29 @@ export function getLayoutFlags() {
80
82
  };
81
83
  }
82
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
+
83
108
  export function getPlaceholders(): Record<string, string> {
84
109
  return { ...(surveyUiConfig.placeholders ?? {}) };
85
110
  }
@@ -87,3 +112,65 @@ export function getPlaceholders(): Record<string, string> {
87
112
  export function isDebugEnabled(): boolean {
88
113
  return surveyUiConfig.debug?.enabled === true;
89
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
+ }