@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.
- package/dist/cli/index.js +35 -35
- package/dist/cli/index.mjs +35 -35
- package/package.json +1 -1
- package/templates/docs/templates/CsatMatrixScale.tsx +60 -45
- package/templates/docs/templates/CustomSliderTrack.tsx +2 -2
- package/templates/docs/templates/Header.tsx +14 -13
- package/templates/docs/templates/LikertMatrixScale.tsx +2 -2
- package/templates/docs/templates/Question.tsx +31 -13
- package/templates/docs/templates/RankOrderScale.tsx +10 -2
- package/templates/docs/templates/SliderMatrixScale.tsx +3 -4
- package/templates/docs/templates/uiConfig.ts +89 -2
- package/templates/preview-harness/preview-bridge.inline.js +55 -15
- package/templates/preview-harness/previewPages.js +2 -2
- package/templates/preview-harness/previewPages.ts +1 -1
- package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +8 -1
- package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +17 -96
- package/templates/preview-harness/vite-app/src/SurveyPagePreview.tsx +32 -8
- package/templates/preview-harness/vite-app/src/fixtures/questions.ts +72 -47
- package/templates/preview-harness/vite-app/src/globals.css +6 -0
- package/templates/preview-harness/vite-app/src/mergePreviewQuestion.ts +173 -0
- package/templates/preview-harness/vite-app/src/preview-live-overrides.css +22 -0
- package/templates/previewBridge.ts +61 -14
- package/templates/survey-theme.css +4 -1
- package/templates/wizard-dist/assets/{PreviewMock-CS4WqhDB.js → PreviewMock-BlOAvUCN.js} +1 -1
- package/templates/wizard-dist/assets/TypePanel-DcbMaw1b.js +1 -0
- package/templates/wizard-dist/assets/index-2Bi_7Fdh.js +34 -0
- package/templates/wizard-dist/index.html +1 -1
- package/templates/wizard-dist/assets/TypePanel-BgTW2c74.js +0 -1
- 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(
|
|
30
|
-
|
|
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
|
|
51
|
-
<div style={{ position: 'relative', margin: '0
|
|
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
|
-
|
|
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',
|
|
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,
|
|
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
|
|
247
|
-
{isGraphics && m > 0 && (
|
|
248
|
-
<div style={{ display: 'flex', alignItems: 'flex-end', marginBottom:
|
|
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
|
|
260
|
-
{!isGraphics &&
|
|
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
|
|
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
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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: '
|
|
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: '
|
|
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,
|
|
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: '
|
|
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: '
|
|
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
|
-
: {
|
|
543
|
-
|
|
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
|
|
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: '-
|
|
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
|
-
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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:
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
186
|
-
style={{
|
|
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,
|
|
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
|
-
|
|
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
|
+
}
|