@explorer02/cfm-survey-sdk 0.4.0 → 0.4.2
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 +69 -65
- package/dist/cli/index.mjs +69 -65
- package/package.json +1 -1
- package/templates/docs/templates/CsatMatrixScale.tsx +102 -29
- package/templates/docs/templates/Footer.tsx +3 -2
- package/templates/docs/templates/RatingScale.tsx +4 -20
- package/templates/docs/templates/labelStyles.ts +44 -0
- package/templates/docs/templates/surveyUiIcons.tsx +6 -1
- package/templates/docs/templates/uiConfig.ts +23 -8
- package/templates/preview-harness/preview-bridge.inline.js +12 -0
- package/templates/preview-harness/preview-google-fonts.html +6 -0
- package/templates/preview-harness/previewPages.js +3 -2
- package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +82 -18
- package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +1 -1
- package/templates/preview-harness/vite-app/src/SurveyPagePreview.tsx +3 -3
- package/templates/preview-harness/vite-app/src/fixtures/questions.ts +6 -9
- package/templates/preview-harness/vite-app/src/globals.css +4 -3
- package/templates/preview-harness/vite-app/src/mergePreviewQuestion.ts +67 -7
- package/templates/preview-harness/vite-app/src/preview-live-overrides.css +9 -0
- package/templates/previewBridge.ts +13 -0
- package/templates/survey-theme.css +13 -0
- package/templates/wizard-dist/assets/{PreviewMock-BlOAvUCN.js → PreviewMock-B2SDZ408.js} +1 -1
- package/templates/wizard-dist/assets/TypePanel-COi_YFKb.js +1 -0
- package/templates/wizard-dist/assets/index-BbqXvrx7.js +34 -0
- package/templates/wizard-dist/index.html +1 -1
- package/templates/wizard-dist/assets/TypePanel-DcbMaw1b.js +0 -1
- package/templates/wizard-dist/assets/index-2Bi_7Fdh.js +0 -34
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import type { CsatQuestion, RatingMatrixQuestion, MatrixRowAnswers, ScaleColumn } from '@explorer02/cfm-survey-sdk';
|
|
3
3
|
import { getEmojiForIndex, CsatStarIcons } from './surveyUiIcons';
|
|
4
|
-
import { columnSubmitValue, nonNaScaleColumns } from './surveyUiScaleUtils';
|
|
4
|
+
import { columnSubmitValue, nonNaScaleColumns, resolveNumberLabelColor } from './surveyUiScaleUtils';
|
|
5
5
|
import { CustomSliderTrack } from './CustomSliderTrack';
|
|
6
6
|
import MatrixDropdown from './MatrixDropdown';
|
|
7
7
|
import {
|
|
@@ -16,14 +16,78 @@ 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, csatAnchorLabelPillStyle } 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 =
|
|
38
|
+
isGraphics || question.type === 'RATING_MATRIX' || question.type === 'CSAT_MATRIX';
|
|
39
|
+
const startPercent = useFullSpan ? 0 : 0.5 / columnCount;
|
|
40
|
+
const rangePercent = useFullSpan ? 1 : (columnCount - 1) / columnCount;
|
|
41
|
+
return (startPercent + (index / (labelCount - 1)) * rangePercent) * 100;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function anchorLabelRowMinHeight(question: CsatMatrixQuestion, hasLabels: boolean): string {
|
|
45
|
+
if (
|
|
46
|
+
hasLabels &&
|
|
47
|
+
(question.type === 'RATING_MATRIX' || question.type === 'CSAT_MATRIX')
|
|
48
|
+
) {
|
|
49
|
+
return '44px';
|
|
50
|
+
}
|
|
51
|
+
return '24px';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderAnchorLabelContent(label: string, question: CsatMatrixQuestion): React.ReactNode {
|
|
55
|
+
if (question.type !== 'RATING_MATRIX' && question.type !== 'CSAT_MATRIX') return label;
|
|
56
|
+
const lines = stackAnchorLabelLines(label);
|
|
57
|
+
if (lines.length === 1) return label;
|
|
58
|
+
return lines.map((line, idx) => (
|
|
59
|
+
<span key={idx} style={{ display: 'block' }}>
|
|
60
|
+
{line}
|
|
61
|
+
</span>
|
|
62
|
+
));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function anchorLabelBoxStyle(question: CsatMatrixQuestion, isGraphics: boolean): React.CSSProperties {
|
|
66
|
+
if (question.type === 'RATING_MATRIX') {
|
|
67
|
+
return {
|
|
68
|
+
textAlign: 'center',
|
|
69
|
+
fontSize: '12px',
|
|
70
|
+
fontWeight: 600,
|
|
71
|
+
...ratingAnchorLabelPillStyle(),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (question.type === 'CSAT_MATRIX') {
|
|
75
|
+
return {
|
|
76
|
+
textAlign: 'center',
|
|
77
|
+
fontSize: '12px',
|
|
78
|
+
fontWeight: 600,
|
|
79
|
+
...csatAnchorLabelPillStyle(),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
textAlign: 'center',
|
|
84
|
+
fontSize: isGraphics ? '12px' : '13px',
|
|
85
|
+
fontWeight: 600,
|
|
86
|
+
lineHeight: 1.3,
|
|
87
|
+
whiteSpace: isGraphics ? 'normal' : 'nowrap',
|
|
88
|
+
maxWidth: isGraphics ? '72px' : undefined,
|
|
89
|
+
...anchorLabelStyle(question),
|
|
90
|
+
};
|
|
27
91
|
}
|
|
28
92
|
|
|
29
93
|
function columnHeaderLabelStyle(question: CsatMatrixQuestion) {
|
|
@@ -227,24 +291,27 @@ function CsatMatrixGrid({
|
|
|
227
291
|
<div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: '12px', minWidth: 0 }}>
|
|
228
292
|
<div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0, minWidth: 0 }} />
|
|
229
293
|
<div style={{ flex: 1, display: 'flex', minWidth: 0 }}>
|
|
230
|
-
<div style={{
|
|
294
|
+
<div style={{
|
|
295
|
+
flex: 1,
|
|
296
|
+
position: 'relative',
|
|
297
|
+
minHeight: anchorLabelRowMinHeight(question, Boolean(showLabels)),
|
|
298
|
+
minWidth: 0,
|
|
299
|
+
...(isGraphics || question.type === 'RATING_MATRIX' || question.type === 'CSAT_MATRIX'
|
|
300
|
+
? { margin: '0 8px' }
|
|
301
|
+
: {}),
|
|
302
|
+
}}>
|
|
231
303
|
{labels!.map((label, i) => {
|
|
232
|
-
const
|
|
233
|
-
const rangePercent = isGraphics ? 1 : (m - 1) / m;
|
|
234
|
-
const percent = n === 1 ? 50 : (startPercent + (i / (n - 1)) * rangePercent) * 100;
|
|
304
|
+
const percent = anchorLabelPositionPercent(i, n, m, isGraphics, question);
|
|
235
305
|
|
|
236
306
|
return (
|
|
237
307
|
<div key={i} style={{
|
|
238
308
|
position: 'absolute',
|
|
239
309
|
left: `${percent}%`,
|
|
310
|
+
bottom: 0,
|
|
240
311
|
transform: 'translateX(-50%)',
|
|
241
|
-
|
|
242
|
-
lineHeight: 1.3,
|
|
243
|
-
whiteSpace: isGraphics ? 'normal' : 'nowrap',
|
|
244
|
-
maxWidth: isGraphics ? '72px' : undefined,
|
|
245
|
-
...anchorLabelStyle(question),
|
|
312
|
+
...anchorLabelBoxStyle(question, isGraphics),
|
|
246
313
|
}}>
|
|
247
|
-
{label}
|
|
314
|
+
{renderAnchorLabelContent(label, question)}
|
|
248
315
|
</div>
|
|
249
316
|
);
|
|
250
317
|
})}
|
|
@@ -367,7 +434,6 @@ function CsatMatrixGrid({
|
|
|
367
434
|
} : isStar ? {
|
|
368
435
|
padding: '4px',
|
|
369
436
|
transform: isHovered ? 'scale(1.1)' : 'scale(1)',
|
|
370
|
-
color: isSelected ? cellSelectedVar : focusRingVar,
|
|
371
437
|
...(isSelected ? {} : unselectedOpacityStyle()),
|
|
372
438
|
} : isNumbered ? {
|
|
373
439
|
width: '40px', height: '40px', borderRadius: '8px',
|
|
@@ -376,8 +442,8 @@ function CsatMatrixGrid({
|
|
|
376
442
|
? scaleCellSelectedStyle(true)
|
|
377
443
|
: {
|
|
378
444
|
border: '1px solid #d1d5db',
|
|
379
|
-
backgroundColor: '#fff',
|
|
380
|
-
color: '#
|
|
445
|
+
backgroundColor: resolveNumberLabelColor(displayIdx, '#fff'),
|
|
446
|
+
color: '#fff',
|
|
381
447
|
...unselectedOpacityStyle(),
|
|
382
448
|
}),
|
|
383
449
|
transition: 'all 0.15s',
|
|
@@ -465,24 +531,31 @@ function CsatMatrixCarousel({
|
|
|
465
531
|
<h3 style={{ fontSize: '18px', fontWeight: 500, color: '#111827', marginBottom: '32px', textAlign: 'center' }} dangerouslySetInnerHTML={{ __html: row.statementText }} />
|
|
466
532
|
|
|
467
533
|
{showLabels && scaleColumns.length > 0 && (
|
|
468
|
-
<div style={{
|
|
534
|
+
<div style={{
|
|
535
|
+
width: '100%',
|
|
536
|
+
maxWidth: '100%',
|
|
537
|
+
minWidth: 0,
|
|
538
|
+
position: 'relative',
|
|
539
|
+
minHeight: anchorLabelRowMinHeight(question, Boolean(showLabels)),
|
|
540
|
+
marginBottom: '16px',
|
|
541
|
+
...(isGraphics || question.type === 'RATING_MATRIX' || question.type === 'CSAT_MATRIX'
|
|
542
|
+
? { padding: '0 8px' }
|
|
543
|
+
: {}),
|
|
544
|
+
}}>
|
|
469
545
|
{labels!.map((label, i) => {
|
|
470
546
|
const m = scaleColumns.length;
|
|
471
547
|
const n = labels!.length;
|
|
472
|
-
const
|
|
473
|
-
const rangePercent = isGraphics ? 1 : (m - 1) / m;
|
|
474
|
-
const percent = n === 1 ? 50 : (startPercent + (i / (n - 1)) * rangePercent) * 100;
|
|
548
|
+
const percent = anchorLabelPositionPercent(i, n, m, isGraphics, question);
|
|
475
549
|
|
|
476
550
|
return (
|
|
477
551
|
<div key={i} style={{
|
|
478
|
-
position: 'absolute',
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
...anchorLabelStyle(question),
|
|
552
|
+
position: 'absolute',
|
|
553
|
+
left: `${percent}%`,
|
|
554
|
+
bottom: 0,
|
|
555
|
+
transform: 'translateX(-50%)',
|
|
556
|
+
...anchorLabelBoxStyle(question, isGraphics),
|
|
484
557
|
}}>
|
|
485
|
-
{label}
|
|
558
|
+
{renderAnchorLabelContent(label, question)}
|
|
486
559
|
</div>
|
|
487
560
|
);
|
|
488
561
|
})}
|
|
@@ -551,8 +624,8 @@ function CsatMatrixCarousel({
|
|
|
551
624
|
? scaleCellSelectedStyle(true)
|
|
552
625
|
: {
|
|
553
626
|
border: '1px solid #d1d5db',
|
|
554
|
-
backgroundColor: '#fff',
|
|
555
|
-
color: '#
|
|
627
|
+
backgroundColor: resolveNumberLabelColor(colIdx, '#fff'),
|
|
628
|
+
color: '#fff',
|
|
556
629
|
...unselectedOpacityStyle(),
|
|
557
630
|
}),
|
|
558
631
|
} : { padding: '8px', ...(!isSelected ? unselectedOpacityStyle() : {}) }),
|
|
@@ -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>
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getAccentColor,
|
|
11
11
|
isScaleAnswerSelected,
|
|
12
12
|
npsValueFromOption,
|
|
13
|
+
resolveNumberLabelColor,
|
|
13
14
|
starValueFromOption,
|
|
14
15
|
} from './surveyUiScaleUtils';
|
|
15
16
|
import { getEmojiForIndex } from './surveyUiIcons';
|
|
@@ -23,25 +24,6 @@ type RatingScaleProps = {
|
|
|
23
24
|
buttonStyle?: 'standard' | 'numbered' | 'emoji' | 'pill';
|
|
24
25
|
};
|
|
25
26
|
|
|
26
|
-
function resolveBadgeColor(index: number, value: number): string {
|
|
27
|
-
if (typeof document === 'undefined') return getAccentColor(value);
|
|
28
|
-
|
|
29
|
-
const root = document.documentElement;
|
|
30
|
-
const mode = getComputedStyle(root).getPropertyValue('--cfm-number-label-mode').trim();
|
|
31
|
-
if (mode === 'monochrome') {
|
|
32
|
-
return (
|
|
33
|
-
getComputedStyle(root).getPropertyValue('--cfm-number-mono-color').trim() || getAccentColor(value)
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
if (mode === 'individual') {
|
|
37
|
-
return (
|
|
38
|
-
getComputedStyle(root).getPropertyValue(`--cfm-number-color-${index}`).trim() ||
|
|
39
|
-
getAccentColor(value)
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
return getAccentColor(value);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
27
|
export default function RatingScale({
|
|
46
28
|
questionId,
|
|
47
29
|
options,
|
|
@@ -57,7 +39,9 @@ export default function RatingScale({
|
|
|
57
39
|
: npsValueFromOption(option as NpsScalePoint, index);
|
|
58
40
|
const numericValue = typeof value === 'number' ? value : index;
|
|
59
41
|
const color =
|
|
60
|
-
variant === 'nps'
|
|
42
|
+
variant === 'nps'
|
|
43
|
+
? resolveNumberLabelColor(index, getAccentColor(numericValue))
|
|
44
|
+
: 'var(--cfm-primary, #e20074)';
|
|
61
45
|
return { id: option.id, label: option.optionLabel, value, color };
|
|
62
46
|
});
|
|
63
47
|
|
|
@@ -64,3 +64,47 @@ 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
|
+
/** CSAT matrix anchor pills — stacked text, fixed width, same look on every format. */
|
|
79
|
+
export function csatAnchorLabelPillStyle(): CSSProperties {
|
|
80
|
+
return {
|
|
81
|
+
...columnLabelPillStyle('csat'),
|
|
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
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Rating matrix anchor pills — stacked text, fixed width, same look on every format. */
|
|
96
|
+
export function ratingAnchorLabelPillStyle(): CSSProperties {
|
|
97
|
+
return {
|
|
98
|
+
...columnLabelPillStyle('rating'),
|
|
99
|
+
display: 'inline-flex',
|
|
100
|
+
flexDirection: 'column',
|
|
101
|
+
alignItems: 'center',
|
|
102
|
+
justifyContent: 'center',
|
|
103
|
+
whiteSpace: 'normal',
|
|
104
|
+
maxWidth: '72px',
|
|
105
|
+
minWidth: '52px',
|
|
106
|
+
padding: '3px 6px',
|
|
107
|
+
wordBreak: 'break-word',
|
|
108
|
+
lineHeight: 1.15,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -58,6 +58,11 @@ export const CsatStarIcons: { filled: React.ReactNode; empty: React.ReactNode }
|
|
|
58
58
|
style: { color: 'var(--cfm-csat-accent, var(--cfm-primary, #e20074))', fontSize: 'var(--cfm-csat-emoji-size, 28px)' },
|
|
59
59
|
}),
|
|
60
60
|
empty: React.createElement(FaRegStar as React.ElementType, {
|
|
61
|
-
style: {
|
|
61
|
+
style: {
|
|
62
|
+
color: 'var(--cfm-star-color, var(--cfm-csat-accent, var(--cfm-primary, #e20074)))',
|
|
63
|
+
fontSize: 'var(--cfm-csat-emoji-size, 28px)',
|
|
64
|
+
stroke: 'var(--cfm-star-color, var(--cfm-csat-accent, var(--cfm-primary, #e20074)))',
|
|
65
|
+
strokeWidth: '16px',
|
|
66
|
+
},
|
|
62
67
|
}),
|
|
63
68
|
};
|
|
@@ -136,6 +136,15 @@ export function resolveNpsButtonStyle(questionStyle?: string): NpsButtonStyle {
|
|
|
136
136
|
const live = readLiveNpsButtonStyle();
|
|
137
137
|
if (live) return live;
|
|
138
138
|
|
|
139
|
+
if (
|
|
140
|
+
questionStyle === 'standard' ||
|
|
141
|
+
questionStyle === 'numbered' ||
|
|
142
|
+
questionStyle === 'emoji' ||
|
|
143
|
+
questionStyle === 'pill'
|
|
144
|
+
) {
|
|
145
|
+
return questionStyle;
|
|
146
|
+
}
|
|
147
|
+
|
|
139
148
|
const fromJson = (
|
|
140
149
|
surveyUiConfig.questionTypes as { NPS_SCALE?: { buttonStyle?: string } } | undefined
|
|
141
150
|
)?.NPS_SCALE?.buttonStyle;
|
|
@@ -147,18 +156,24 @@ export function resolveNpsButtonStyle(questionStyle?: string): NpsButtonStyle {
|
|
|
147
156
|
) {
|
|
148
157
|
return fromJson;
|
|
149
158
|
}
|
|
150
|
-
if (
|
|
151
|
-
questionStyle === 'standard' ||
|
|
152
|
-
questionStyle === 'numbered' ||
|
|
153
|
-
questionStyle === 'emoji' ||
|
|
154
|
-
questionStyle === 'pill'
|
|
155
|
-
) {
|
|
156
|
-
return questionStyle;
|
|
157
|
-
}
|
|
158
159
|
return 'numbered';
|
|
159
160
|
}
|
|
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
|
+
|
|
161
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
|
+
|
|
162
177
|
const nps = (
|
|
163
178
|
surveyUiConfig.questionTypes as { NPS_SCALE?: { hintMinText?: string; hintMaxText?: string } } | undefined
|
|
164
179
|
)?.NPS_SCALE;
|
|
@@ -2,10 +2,21 @@
|
|
|
2
2
|
var SNAPSHOT = 'CFM_UI_CONFIG_SNAPSHOT';
|
|
3
3
|
var DELTA = 'CFM_UI_CONFIG_DELTA';
|
|
4
4
|
var LEGACY = 'CFM_UI_CONFIG';
|
|
5
|
+
var PREVIEW_FONTS_HREF =
|
|
6
|
+
'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Roboto:wght@400;500;600;700&family=Open+Sans:wght@400;500;600;700&family=Lato:wght@400;500;600;700&family=Poppins:wght@400;500;600;700&family=Montserrat:wght@400;500;600;700&family=Nunito:wght@400;500;600;700&family=Raleway:wght@400;500;600;700&family=Source+Sans+3:wght@400;500;600;700&family=Merriweather:wght@400;500;600;700&family=Playfair+Display:wght@400;500;600;700&display=swap';
|
|
5
7
|
var rafId = null;
|
|
6
8
|
var pending = {};
|
|
7
9
|
var liveEdit = false;
|
|
8
10
|
|
|
11
|
+
function ensurePreviewFonts() {
|
|
12
|
+
if (document.getElementById('cfm-preview-fonts')) return;
|
|
13
|
+
var link = document.createElement('link');
|
|
14
|
+
link.id = 'cfm-preview-fonts';
|
|
15
|
+
link.rel = 'stylesheet';
|
|
16
|
+
link.href = PREVIEW_FONTS_HREF;
|
|
17
|
+
document.head.appendChild(link);
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
function flush() {
|
|
10
21
|
rafId = null;
|
|
11
22
|
var root = document.documentElement;
|
|
@@ -282,6 +293,7 @@ function setDisplay(selector, visible) {
|
|
|
282
293
|
|
|
283
294
|
if (!liveEdit) {
|
|
284
295
|
liveEdit = true;
|
|
296
|
+
ensurePreviewFonts();
|
|
285
297
|
document.documentElement.classList.add('cfm-live-edit');
|
|
286
298
|
}
|
|
287
299
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<link id="cfm-preview-fonts" rel="preconnect" href="https://fonts.googleapis.com" />
|
|
2
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
3
|
+
<link
|
|
4
|
+
rel="stylesheet"
|
|
5
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Roboto:wght@400;500;600;700&family=Open+Sans:wght@400;500;600;700&family=Lato:wght@400;500;600;700&family=Poppins:wght@400;500;600;700&family=Montserrat:wght@400;500;600;700&family=Nunito:wght@400;500;600;700&family=Raleway:wght@400;500;600;700&family=Source+Sans+3:wght@400;500;600;700&family=Merriweather:wght@400;500;600;700&family=Playfair+Display:wght@400;500;600;700&display=swap"
|
|
6
|
+
/>
|
|
@@ -122,14 +122,15 @@ const PREVIEW_PAGE_DEFS = [
|
|
|
122
122
|
{ key: 'RANK_dropdown', title: 'Rank Dropdown', body: `<div class="cfm-rank-item"><span>Item A</span><select class="cfm-input cfm-rank-select" style="width:80px;margin-left:auto"><option>1</option><option>2</option></select></div>` },
|
|
123
123
|
];
|
|
124
124
|
|
|
125
|
-
function buildPreviewHtml(def, inlineCss, inlineJs) {
|
|
125
|
+
function buildPreviewHtml(def, inlineCss, inlineJs, fontsSnippet) {
|
|
126
|
+
const fonts = fontsSnippet ? `${fontsSnippet}\n` : '';
|
|
126
127
|
return `<!DOCTYPE html>
|
|
127
128
|
<html lang="en">
|
|
128
129
|
<head>
|
|
129
130
|
<meta charset="UTF-8" />
|
|
130
131
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
131
132
|
<title>${def.title} — CFM Preview</title>
|
|
132
|
-
<style>${inlineCss}\n${sharedStyles}</style>
|
|
133
|
+
${fonts}<style>${inlineCss}\n${sharedStyles}</style>
|
|
133
134
|
</head>
|
|
134
135
|
<body>
|
|
135
136
|
<div class="cfm-preview-root">${def.body}</div>
|
|
@@ -15,27 +15,38 @@ export type PreviewStatePatch = {
|
|
|
15
15
|
|
|
16
16
|
export const PREVIEW_BRIDGE_SNAPSHOT = 'CFM_UI_CONFIG_SNAPSHOT';
|
|
17
17
|
export const PREVIEW_BRIDGE_DELTA = 'CFM_UI_CONFIG_DELTA';
|
|
18
|
+
export const PREVIEW_BRIDGE_READY = 'CFM_PREVIEW_READY';
|
|
18
19
|
|
|
19
|
-
const
|
|
20
|
-
'#
|
|
21
|
-
'#
|
|
22
|
-
];
|
|
20
|
+
const SCALE_COLORS = [
|
|
21
|
+
'#e2001a', '#e4251b', '#ec610a', '#f18b00', '#f7b200', '#fcd900',
|
|
22
|
+
'#d2dc00', '#9fca00', '#6bb300', '#339a00', '#008000',
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
function trafficColor(index: number): string {
|
|
26
|
+
return SCALE_COLORS[index] ?? '#9ca3af';
|
|
27
|
+
}
|
|
23
28
|
|
|
24
29
|
function readVar(name: string, fallback: string): string {
|
|
25
30
|
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
function
|
|
33
|
+
function resolveLiveNumberLabelColor(index: number): string {
|
|
29
34
|
const mode = readVar('--cfm-number-label-mode', 'traffic');
|
|
30
|
-
|
|
35
|
+
if (mode === 'monochrome') {
|
|
36
|
+
return readVar('--cfm-number-mono-color', '#9ca3af');
|
|
37
|
+
}
|
|
38
|
+
if (mode === 'individual') {
|
|
39
|
+
return readVar(`--cfm-number-color-${index}`, '#9ca3af');
|
|
40
|
+
}
|
|
41
|
+
return trafficColor(index);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function applyNumberBadgeColors(): void {
|
|
31
45
|
document.querySelectorAll('[data-cfm-number-badge]').forEach((el) => {
|
|
32
46
|
if (el.classList.contains('selected')) return;
|
|
33
47
|
const i = parseInt(el.getAttribute('data-cfm-number-badge') ?? '', 10);
|
|
34
48
|
if (Number.isNaN(i)) return;
|
|
35
|
-
|
|
36
|
-
if (mode === 'monochrome') bg = mono;
|
|
37
|
-
if (mode === 'individual') bg = readVar(`--cfm-number-color-${i}`, '#9ca3af');
|
|
38
|
-
(el as HTMLElement).style.backgroundColor = bg;
|
|
49
|
+
(el as HTMLElement).style.backgroundColor = resolveLiveNumberLabelColor(i);
|
|
39
50
|
});
|
|
40
51
|
}
|
|
41
52
|
|
|
@@ -51,6 +62,26 @@ function applyThemeSideEffects(): void {
|
|
|
51
62
|
applyDropzoneStyle();
|
|
52
63
|
}
|
|
53
64
|
|
|
65
|
+
function previewStateFromCssVars(
|
|
66
|
+
cssVars: Record<string, string>,
|
|
67
|
+
prev: PreviewStatePatch,
|
|
68
|
+
): PreviewStatePatch {
|
|
69
|
+
const next: PreviewStatePatch = { ...prev };
|
|
70
|
+
if (cssVars['--cfm-matrix-multi-statement'] !== undefined) {
|
|
71
|
+
next.multiStatement = cssVars['--cfm-matrix-multi-statement'] === '1';
|
|
72
|
+
}
|
|
73
|
+
if (cssVars['--cfm-nps-button-style']) {
|
|
74
|
+
next.npsButtonStyle = cssVars['--cfm-nps-button-style'];
|
|
75
|
+
}
|
|
76
|
+
if (cssVars['--cfm-hint-min-text']) {
|
|
77
|
+
next.hintMinText = cssVars['--cfm-hint-min-text'];
|
|
78
|
+
}
|
|
79
|
+
if (cssVars['--cfm-hint-max-text']) {
|
|
80
|
+
next.hintMaxText = cssVars['--cfm-hint-max-text'];
|
|
81
|
+
}
|
|
82
|
+
return next;
|
|
83
|
+
}
|
|
84
|
+
|
|
54
85
|
type PreviewConfigContextValue = {
|
|
55
86
|
previewState: PreviewStatePatch;
|
|
56
87
|
configRevision: number;
|
|
@@ -61,22 +92,60 @@ const PreviewConfigContext = createContext<PreviewConfigContextValue>({
|
|
|
61
92
|
configRevision: 0,
|
|
62
93
|
});
|
|
63
94
|
|
|
95
|
+
function enableLiveEditMode(): void {
|
|
96
|
+
document.documentElement.classList.add('cfm-live-edit');
|
|
97
|
+
}
|
|
98
|
+
|
|
64
99
|
export function PreviewConfigProvider({ children }: { children: ReactNode }) {
|
|
65
100
|
const [previewState, setPreviewState] = useState<PreviewStatePatch>({});
|
|
66
101
|
const [configRevision, setConfigRevision] = useState(0);
|
|
67
102
|
|
|
68
103
|
useEffect(() => {
|
|
104
|
+
enableLiveEditMode();
|
|
105
|
+
|
|
106
|
+
const requestSnapshot = () => {
|
|
107
|
+
try {
|
|
108
|
+
window.parent.postMessage({ type: PREVIEW_BRIDGE_READY }, '*');
|
|
109
|
+
} catch {
|
|
110
|
+
/* ignore */
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
requestSnapshot();
|
|
115
|
+
|
|
69
116
|
const handler = (event: MessageEvent) => {
|
|
70
117
|
const data = event.data;
|
|
71
118
|
if (!data?.type) return;
|
|
72
119
|
if (data.type !== PREVIEW_BRIDGE_SNAPSHOT && data.type !== PREVIEW_BRIDGE_DELTA) return;
|
|
73
120
|
|
|
74
|
-
|
|
121
|
+
enableLiveEditMode();
|
|
122
|
+
|
|
123
|
+
const isSnapshot = data.type === PREVIEW_BRIDGE_SNAPSHOT;
|
|
124
|
+
let shouldBumpRevision = isSnapshot;
|
|
125
|
+
|
|
126
|
+
if (data.cssVars) {
|
|
127
|
+
const root = document.documentElement;
|
|
128
|
+
for (const [key, value] of Object.entries(data.cssVars)) {
|
|
129
|
+
root.style.setProperty(key, value as string);
|
|
130
|
+
}
|
|
131
|
+
shouldBumpRevision = true;
|
|
132
|
+
}
|
|
75
133
|
|
|
76
|
-
if (
|
|
134
|
+
if (isSnapshot) {
|
|
135
|
+
const patchState = (data.contentPatch?.previewState ?? {}) as PreviewStatePatch;
|
|
136
|
+
const nextState = data.cssVars
|
|
137
|
+
? previewStateFromCssVars(data.cssVars, patchState)
|
|
138
|
+
: patchState;
|
|
139
|
+
setPreviewState(nextState);
|
|
140
|
+
shouldBumpRevision = true;
|
|
141
|
+
} else if (data.contentPatch?.previewState) {
|
|
77
142
|
setPreviewState((prev) => ({ ...prev, ...data.contentPatch.previewState }));
|
|
78
143
|
shouldBumpRevision = true;
|
|
144
|
+
} else if (data.cssVars) {
|
|
145
|
+
setPreviewState((prev) => previewStateFromCssVars(data.cssVars, prev));
|
|
146
|
+
shouldBumpRevision = true;
|
|
79
147
|
}
|
|
148
|
+
|
|
80
149
|
if (data.contentPatch?.domAttributes) {
|
|
81
150
|
const root = document.documentElement;
|
|
82
151
|
for (const [key, value] of Object.entries(data.contentPatch.domAttributes)) {
|
|
@@ -84,12 +153,7 @@ export function PreviewConfigProvider({ children }: { children: ReactNode }) {
|
|
|
84
153
|
}
|
|
85
154
|
shouldBumpRevision = true;
|
|
86
155
|
}
|
|
87
|
-
|
|
88
|
-
const root = document.documentElement;
|
|
89
|
-
for (const [key, value] of Object.entries(data.cssVars)) {
|
|
90
|
-
root.style.setProperty(key, value as string);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
156
|
+
|
|
93
157
|
if (shouldBumpRevision) {
|
|
94
158
|
setConfigRevision((r) => r + 1);
|
|
95
159
|
}
|
|
@@ -34,7 +34,7 @@ function QuestionPreviewInner({ question, initialValue }: QuestionPreviewProps)
|
|
|
34
34
|
|
|
35
35
|
const mergedQuestion = useMemo(
|
|
36
36
|
() => mergePreviewQuestion(question, previewState),
|
|
37
|
-
[question, previewState],
|
|
37
|
+
[question, previewState, configRevision],
|
|
38
38
|
);
|
|
39
39
|
|
|
40
40
|
const heatmapInitial =
|
|
@@ -22,15 +22,15 @@ function SurveyPagePreviewInner() {
|
|
|
22
22
|
|
|
23
23
|
const mcqQuestion = useMemo(
|
|
24
24
|
() => mergeSurveyPageQuestion(FIXTURE_SURVEY_PAGE_MCQ, previewState),
|
|
25
|
-
[previewState],
|
|
25
|
+
[previewState, configRevision],
|
|
26
26
|
);
|
|
27
27
|
const matrixQuestion = useMemo(
|
|
28
28
|
() => mergeSurveyPageQuestion(FIXTURE_SURVEY_PAGE_CFM_LIKERT, previewState),
|
|
29
|
-
[previewState],
|
|
29
|
+
[previewState, configRevision],
|
|
30
30
|
);
|
|
31
31
|
const npsQuestion = useMemo(
|
|
32
32
|
() => mergeSurveyPageQuestion(FIXTURE_SURVEY_PAGE_NPS, previewState),
|
|
33
|
-
[previewState],
|
|
33
|
+
[previewState, configRevision],
|
|
34
34
|
);
|
|
35
35
|
|
|
36
36
|
const allQuestions = [mcqQuestion, matrixQuestion, npsQuestion];
|