@explorer02/cfm-survey-sdk 0.3.9 → 0.4.1
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 +64 -64
- package/dist/cli/index.mjs +63 -63
- package/package.json +1 -1
- package/templates/docs/templates/CsatMatrixScale.tsx +105 -36
- package/templates/docs/templates/CustomSliderTrack.tsx +2 -2
- package/templates/docs/templates/Footer.tsx +3 -2
- package/templates/docs/templates/Header.tsx +14 -14
- package/templates/docs/templates/LikertMatrixScale.tsx +2 -2
- package/templates/docs/templates/Question.tsx +30 -23
- package/templates/docs/templates/RankOrderScale.tsx +10 -2
- package/templates/docs/templates/SliderMatrixScale.tsx +1 -1
- package/templates/docs/templates/labelStyles.ts +27 -0
- package/templates/docs/templates/uiConfig.ts +100 -0
- package/templates/preview-harness/preview-bridge.inline.js +55 -17
- 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 +60 -7
- 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 +53 -25
- package/templates/preview-harness/vite-app/src/globals.css +6 -0
- package/templates/preview-harness/vite-app/src/mergePreviewQuestion.ts +233 -0
- package/templates/preview-harness/vite-app/src/preview-live-overrides.css +30 -0
- package/templates/previewBridge.ts +61 -18
- package/templates/survey-theme.css +16 -0
- package/templates/wizard-dist/assets/{PreviewMock-CS4WqhDB.js → PreviewMock-MsONyaq9.js} +1 -1
- package/templates/wizard-dist/assets/TypePanel-D24BxbHk.js +1 -0
- package/templates/wizard-dist/assets/index-C7RSJr09.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
package/package.json
CHANGED
|
@@ -16,14 +16,64 @@ import {
|
|
|
16
16
|
mcqSelectedAccentVar,
|
|
17
17
|
mcqSelectedBgVar,
|
|
18
18
|
} from '@/lib/surveyUi/selectionStyles';
|
|
19
|
-
import { csatColumnLabelStyle, ratingColumnLabelStyle } from '@/lib/surveyUi/labelStyles';
|
|
19
|
+
import { csatColumnLabelStyle, ratingColumnLabelStyle, stackAnchorLabelLines, ratingAnchorLabelPillStyle } from '@/lib/surveyUi/labelStyles';
|
|
20
20
|
|
|
21
21
|
const MATRIX_ROW_LABEL_WIDTH = 'var(--cfm-matrix-row-width, 180px)';
|
|
22
22
|
|
|
23
23
|
type CsatMatrixQuestion = CsatQuestion | RatingMatrixQuestion;
|
|
24
24
|
|
|
25
25
|
function anchorLabelStyle(question: CsatMatrixQuestion) {
|
|
26
|
-
return question.type === 'RATING_MATRIX' ?
|
|
26
|
+
return question.type === 'RATING_MATRIX' ? ratingAnchorLabelPillStyle() : csatColumnLabelStyle();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function anchorLabelPositionPercent(
|
|
30
|
+
index: number,
|
|
31
|
+
labelCount: number,
|
|
32
|
+
columnCount: number,
|
|
33
|
+
isGraphics: boolean,
|
|
34
|
+
question: CsatMatrixQuestion,
|
|
35
|
+
): number {
|
|
36
|
+
if (labelCount === 1) return 50;
|
|
37
|
+
const useFullSpan = isGraphics || question.type === 'RATING_MATRIX';
|
|
38
|
+
const startPercent = useFullSpan ? 0 : 0.5 / columnCount;
|
|
39
|
+
const rangePercent = useFullSpan ? 1 : (columnCount - 1) / columnCount;
|
|
40
|
+
return (startPercent + (index / (labelCount - 1)) * rangePercent) * 100;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function anchorLabelRowMinHeight(question: CsatMatrixQuestion, hasLabels: boolean): string {
|
|
44
|
+
if (question.type === 'RATING_MATRIX' && hasLabels) return '44px';
|
|
45
|
+
return '24px';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function renderAnchorLabelContent(label: string, question: CsatMatrixQuestion): React.ReactNode {
|
|
49
|
+
if (question.type !== 'RATING_MATRIX') return label;
|
|
50
|
+
const lines = stackAnchorLabelLines(label);
|
|
51
|
+
if (lines.length === 1) return label;
|
|
52
|
+
return lines.map((line, idx) => (
|
|
53
|
+
<span key={idx} style={{ display: 'block' }}>
|
|
54
|
+
{line}
|
|
55
|
+
</span>
|
|
56
|
+
));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function anchorLabelBoxStyle(question: CsatMatrixQuestion, isGraphics: boolean): React.CSSProperties {
|
|
60
|
+
if (question.type === 'RATING_MATRIX') {
|
|
61
|
+
return {
|
|
62
|
+
textAlign: 'center',
|
|
63
|
+
fontSize: '12px',
|
|
64
|
+
fontWeight: 600,
|
|
65
|
+
...ratingAnchorLabelPillStyle(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
textAlign: 'center',
|
|
70
|
+
fontSize: isGraphics ? '12px' : '13px',
|
|
71
|
+
fontWeight: 600,
|
|
72
|
+
lineHeight: 1.3,
|
|
73
|
+
whiteSpace: isGraphics ? 'normal' : 'nowrap',
|
|
74
|
+
maxWidth: isGraphics ? '72px' : undefined,
|
|
75
|
+
...anchorLabelStyle(question),
|
|
76
|
+
};
|
|
27
77
|
}
|
|
28
78
|
|
|
29
79
|
function columnHeaderLabelStyle(question: CsatMatrixQuestion) {
|
|
@@ -36,6 +86,15 @@ function graphicsColumnLabelStyle(question: CsatMatrixQuestion) {
|
|
|
36
86
|
return { fontSize: '12px', fontWeight: 500, lineHeight: 1.2, wordBreak: 'break-word' as const, ...pillStyle };
|
|
37
87
|
}
|
|
38
88
|
|
|
89
|
+
/** Graphics uses anchor labels (bold top row) — per-column tick badges duplicate them for CSAT + RATING. */
|
|
90
|
+
function hasGraphicsAnchorLabels(labels: string[] | undefined): boolean {
|
|
91
|
+
return Boolean(labels?.some((label) => label.trim().length > 0));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function shouldShowGraphicsColumnLabelRow(isGraphics: boolean, labels: string[] | undefined): boolean {
|
|
95
|
+
return isGraphics && !hasGraphicsAnchorLabels(labels);
|
|
96
|
+
}
|
|
97
|
+
|
|
39
98
|
function GraphicsColumnLabelsRow({
|
|
40
99
|
scaleColumns,
|
|
41
100
|
question,
|
|
@@ -47,8 +106,8 @@ function GraphicsColumnLabelsRow({
|
|
|
47
106
|
const labelStyle = graphicsColumnLabelStyle(question);
|
|
48
107
|
|
|
49
108
|
return (
|
|
50
|
-
<div style={{ flex: 1, padding: '0
|
|
51
|
-
<div style={{ position: 'relative', margin: '0
|
|
109
|
+
<div style={{ flex: 1, minWidth: 0, padding: '0 8px' }}>
|
|
110
|
+
<div style={{ position: 'relative', margin: '0 8px', height: '16px' }}>
|
|
52
111
|
{scaleColumns.map((col, i) => {
|
|
53
112
|
const percent = scaleColumns.length === 1 ? 0 : (i / (scaleColumns.length - 1)) * 100;
|
|
54
113
|
return (
|
|
@@ -60,7 +119,7 @@ function GraphicsColumnLabelsRow({
|
|
|
60
119
|
transform: 'translateX(-50%)',
|
|
61
120
|
bottom: 0,
|
|
62
121
|
textAlign: 'center',
|
|
63
|
-
|
|
122
|
+
maxWidth: '72px',
|
|
64
123
|
}}
|
|
65
124
|
>
|
|
66
125
|
<span style={labelStyle} dangerouslySetInnerHTML={{ __html: col.label }} />
|
|
@@ -212,28 +271,31 @@ function CsatMatrixGrid({
|
|
|
212
271
|
const n = labels?.length || 0;
|
|
213
272
|
|
|
214
273
|
return (
|
|
215
|
-
<div style={{ width: '100%' }}>
|
|
274
|
+
<div style={{ width: '100%', maxWidth: '100%', minWidth: 0, overflowX: 'auto', boxSizing: 'border-box' }}>
|
|
216
275
|
{/* Labels row */}
|
|
217
276
|
{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={{
|
|
277
|
+
<div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: '12px', minWidth: 0 }}>
|
|
278
|
+
<div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0, minWidth: 0 }} />
|
|
279
|
+
<div style={{ flex: 1, display: 'flex', minWidth: 0 }}>
|
|
280
|
+
<div style={{
|
|
281
|
+
flex: 1,
|
|
282
|
+
position: 'relative',
|
|
283
|
+
minHeight: anchorLabelRowMinHeight(question, Boolean(showLabels)),
|
|
284
|
+
minWidth: 0,
|
|
285
|
+
...(isGraphics || question.type === 'RATING_MATRIX' ? { margin: '0 8px' } : {}),
|
|
286
|
+
}}>
|
|
222
287
|
{labels!.map((label, i) => {
|
|
223
|
-
const
|
|
224
|
-
const rangePercent = isGraphics ? 1 : (m - 1) / m;
|
|
225
|
-
const percent = n === 1 ? 50 : (startPercent + (i / (n - 1)) * rangePercent) * 100;
|
|
288
|
+
const percent = anchorLabelPositionPercent(i, n, m, isGraphics, question);
|
|
226
289
|
|
|
227
290
|
return (
|
|
228
291
|
<div key={i} style={{
|
|
229
292
|
position: 'absolute',
|
|
230
293
|
left: `${percent}%`,
|
|
294
|
+
bottom: 0,
|
|
231
295
|
transform: 'translateX(-50%)',
|
|
232
|
-
|
|
233
|
-
lineHeight: 1.3, whiteSpace: 'nowrap',
|
|
234
|
-
...anchorLabelStyle(question),
|
|
296
|
+
...anchorLabelBoxStyle(question, isGraphics),
|
|
235
297
|
}}>
|
|
236
|
-
{label}
|
|
298
|
+
{renderAnchorLabelContent(label, question)}
|
|
237
299
|
</div>
|
|
238
300
|
);
|
|
239
301
|
})}
|
|
@@ -243,9 +305,9 @@ function CsatMatrixGrid({
|
|
|
243
305
|
</div>
|
|
244
306
|
)}
|
|
245
307
|
|
|
246
|
-
{/* Column tick labels for graphics
|
|
247
|
-
{isGraphics && m > 0 && (
|
|
248
|
-
<div style={{ display: 'flex', alignItems: 'flex-end', marginBottom:
|
|
308
|
+
{/* Column tick labels for graphics — only when no anchor label row above */}
|
|
309
|
+
{shouldShowGraphicsColumnLabelRow(isGraphics, labels) && m > 0 && (
|
|
310
|
+
<div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: '16px', minHeight: '24px' }}>
|
|
249
311
|
<div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0 }} />
|
|
250
312
|
<div style={{ flex: 1, display: 'flex' }}>
|
|
251
313
|
<GraphicsColumnLabelsRow scaleColumns={scaleColumns} question={question} />
|
|
@@ -285,15 +347,15 @@ function CsatMatrixGrid({
|
|
|
285
347
|
{statementRows.map((row, rowIdx) => (
|
|
286
348
|
<div key={row.id} style={{
|
|
287
349
|
display: 'flex', alignItems: 'center', borderRadius: '8px',
|
|
288
|
-
padding: '12px 0', ...matrixRowBackgroundStyle(rowIdx),
|
|
350
|
+
padding: '12px 0', minWidth: 0, ...matrixRowBackgroundStyle(rowIdx),
|
|
289
351
|
}}>
|
|
290
|
-
<div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0, padding: '0
|
|
352
|
+
<div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0, minWidth: 0, padding: '0 12px' }}>
|
|
291
353
|
<span style={{ fontSize: '14px', fontWeight: 500, color: '#111827', lineHeight: 1.4 }}
|
|
292
354
|
dangerouslySetInnerHTML={{ __html: row.statementText }} />
|
|
293
355
|
</div>
|
|
294
356
|
|
|
295
357
|
{isGraphics ? (
|
|
296
|
-
<div style={{ flex: 1, padding: '0
|
|
358
|
+
<div style={{ flex: 1, minWidth: 0, padding: '0 8px', position: 'relative', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
|
297
359
|
<CustomSliderTrack
|
|
298
360
|
min={0}
|
|
299
361
|
max={scaleColumns.length - 1}
|
|
@@ -450,40 +512,47 @@ function CsatMatrixCarousel({
|
|
|
450
512
|
const showLabels = labels && labels.length > 0;
|
|
451
513
|
|
|
452
514
|
return (
|
|
453
|
-
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', backgroundColor: '#fff', borderRadius: '12px', border: '1px solid #e5e5e5', padding: '
|
|
515
|
+
<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
516
|
<h3 style={{ fontSize: '18px', fontWeight: 500, color: '#111827', marginBottom: '32px', textAlign: 'center' }} dangerouslySetInnerHTML={{ __html: row.statementText }} />
|
|
455
517
|
|
|
456
518
|
{showLabels && scaleColumns.length > 0 && (
|
|
457
|
-
<div style={{
|
|
519
|
+
<div style={{
|
|
520
|
+
width: '100%',
|
|
521
|
+
maxWidth: '100%',
|
|
522
|
+
minWidth: 0,
|
|
523
|
+
position: 'relative',
|
|
524
|
+
minHeight: anchorLabelRowMinHeight(question, Boolean(showLabels)),
|
|
525
|
+
marginBottom: '16px',
|
|
526
|
+
...(isGraphics || question.type === 'RATING_MATRIX' ? { padding: '0 8px' } : {}),
|
|
527
|
+
}}>
|
|
458
528
|
{labels!.map((label, i) => {
|
|
459
529
|
const m = scaleColumns.length;
|
|
460
530
|
const n = labels!.length;
|
|
461
|
-
const
|
|
462
|
-
const rangePercent = isGraphics ? 1 : (m - 1) / m;
|
|
463
|
-
const percent = n === 1 ? 50 : (startPercent + (i / (n - 1)) * rangePercent) * 100;
|
|
531
|
+
const percent = anchorLabelPositionPercent(i, n, m, isGraphics, question);
|
|
464
532
|
|
|
465
533
|
return (
|
|
466
534
|
<div key={i} style={{
|
|
467
|
-
position: 'absolute',
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
535
|
+
position: 'absolute',
|
|
536
|
+
left: `${percent}%`,
|
|
537
|
+
bottom: 0,
|
|
538
|
+
transform: 'translateX(-50%)',
|
|
539
|
+
...anchorLabelBoxStyle(question, isGraphics),
|
|
471
540
|
}}>
|
|
472
|
-
{label}
|
|
541
|
+
{renderAnchorLabelContent(label, question)}
|
|
473
542
|
</div>
|
|
474
543
|
);
|
|
475
544
|
})}
|
|
476
545
|
</div>
|
|
477
546
|
)}
|
|
478
547
|
|
|
479
|
-
{isGraphics && scaleColumns.length > 0 && (
|
|
480
|
-
<div style={{ width: '100%', maxWidth: '
|
|
548
|
+
{shouldShowGraphicsColumnLabelRow(isGraphics, labels) && scaleColumns.length > 0 && (
|
|
549
|
+
<div style={{ width: '100%', maxWidth: '100%', marginBottom: '16px' }}>
|
|
481
550
|
<GraphicsColumnLabelsRow scaleColumns={scaleColumns} question={question} />
|
|
482
551
|
</div>
|
|
483
552
|
)}
|
|
484
553
|
|
|
485
554
|
{isGraphics ? (
|
|
486
|
-
<div style={{ width: '100%', maxWidth: '
|
|
555
|
+
<div style={{ width: '100%', maxWidth: '100%', minWidth: 0, marginBottom: '40px', padding: '0 8px' }}>
|
|
487
556
|
<CustomSliderTrack
|
|
488
557
|
min={0} max={scaleColumns.length - 1} htmlStep={1}
|
|
489
558
|
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
|
|
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
|
/>
|
|
@@ -39,10 +39,11 @@ export default function Footer() {
|
|
|
39
39
|
data-cfm-logo
|
|
40
40
|
src={seedLogoSrc!}
|
|
41
41
|
alt=""
|
|
42
|
-
className="object-contain"
|
|
42
|
+
className="object-contain object-left"
|
|
43
43
|
style={{
|
|
44
|
-
width: 'var(--cfm-footer-logo-width, 120px)',
|
|
45
44
|
height: 'var(--cfm-footer-logo-height, 32px)',
|
|
45
|
+
maxWidth: 'var(--cfm-footer-logo-width, 120px)',
|
|
46
|
+
width: 'auto',
|
|
46
47
|
}}
|
|
47
48
|
/>
|
|
48
49
|
</div>
|
|
@@ -76,20 +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
|
-
|
|
92
|
-
|
|
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:
|
|
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 {
|
|
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
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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}`;
|
|
@@ -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 => {
|
|
@@ -64,3 +64,30 @@ export function ratingColumnLabelStyle(): CSSProperties {
|
|
|
64
64
|
export function sliderTickLabelStyle(): CSSProperties {
|
|
65
65
|
return columnLabelPillStyle('sliderTick');
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
/** Split multi-word anchor labels into stacked lines for equal-width matrix headers. */
|
|
69
|
+
export function stackAnchorLabelLines(label: string): string[] {
|
|
70
|
+
const trimmed = label.trim();
|
|
71
|
+
if (!trimmed) return [''];
|
|
72
|
+
const words = trimmed.split(/\s+/);
|
|
73
|
+
if (words.length <= 1) return [trimmed];
|
|
74
|
+
const mid = Math.ceil(words.length / 2);
|
|
75
|
+
return [words.slice(0, mid).join(' '), words.slice(mid).join(' ')].filter(Boolean);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Rating matrix anchor pills — stacked text, fixed width, same look on every format. */
|
|
79
|
+
export function ratingAnchorLabelPillStyle(): CSSProperties {
|
|
80
|
+
return {
|
|
81
|
+
...columnLabelPillStyle('rating'),
|
|
82
|
+
display: 'inline-flex',
|
|
83
|
+
flexDirection: 'column',
|
|
84
|
+
alignItems: 'center',
|
|
85
|
+
justifyContent: 'center',
|
|
86
|
+
whiteSpace: 'normal',
|
|
87
|
+
maxWidth: '72px',
|
|
88
|
+
minWidth: '52px',
|
|
89
|
+
padding: '3px 6px',
|
|
90
|
+
wordBreak: 'break-word',
|
|
91
|
+
lineHeight: 1.15,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -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,80 @@ 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
|
+
if (
|
|
140
|
+
questionStyle === 'standard' ||
|
|
141
|
+
questionStyle === 'numbered' ||
|
|
142
|
+
questionStyle === 'emoji' ||
|
|
143
|
+
questionStyle === 'pill'
|
|
144
|
+
) {
|
|
145
|
+
return questionStyle;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const fromJson = (
|
|
149
|
+
surveyUiConfig.questionTypes as { NPS_SCALE?: { buttonStyle?: string } } | undefined
|
|
150
|
+
)?.NPS_SCALE?.buttonStyle;
|
|
151
|
+
if (
|
|
152
|
+
fromJson === 'standard' ||
|
|
153
|
+
fromJson === 'numbered' ||
|
|
154
|
+
fromJson === 'emoji' ||
|
|
155
|
+
fromJson === 'pill'
|
|
156
|
+
) {
|
|
157
|
+
return fromJson;
|
|
158
|
+
}
|
|
159
|
+
return 'numbered';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function readLiveNpsHintText(kind: 'min' | 'max'): string | null {
|
|
163
|
+
if (typeof window === 'undefined') return null;
|
|
164
|
+
const root = document.documentElement;
|
|
165
|
+
const cssVar = kind === 'min' ? '--cfm-hint-min-text' : '--cfm-hint-max-text';
|
|
166
|
+
const fromCss =
|
|
167
|
+
root.style.getPropertyValue(cssVar).trim() ||
|
|
168
|
+
getComputedStyle(root).getPropertyValue(cssVar).trim();
|
|
169
|
+
return fromCss || null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function resolveNpsHintText(kind: 'min' | 'max', questionFallback: string): string {
|
|
173
|
+
const fromCss = readLiveNpsHintText(kind);
|
|
174
|
+
if (fromCss) return fromCss;
|
|
175
|
+
if (questionFallback.trim()) return questionFallback;
|
|
176
|
+
|
|
177
|
+
const nps = (
|
|
178
|
+
surveyUiConfig.questionTypes as { NPS_SCALE?: { hintMinText?: string; hintMaxText?: string } } | undefined
|
|
179
|
+
)?.NPS_SCALE;
|
|
180
|
+
const fromJson = kind === 'min' ? nps?.hintMinText : nps?.hintMaxText;
|
|
181
|
+
const trimmed = fromJson?.trim();
|
|
182
|
+
return trimmed || questionFallback;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function resolveNpsHintMinLabel(questionFallback = ''): string {
|
|
186
|
+
return resolveNpsHintText('min', questionFallback);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function resolveNpsHintMaxLabel(questionFallback = ''): string {
|
|
190
|
+
return resolveNpsHintText('max', questionFallback);
|
|
191
|
+
}
|