@explorer02/cfm-survey-sdk 0.3.0 → 0.3.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 +94 -83
- package/dist/cli/index.mjs +94 -83
- package/package.json +1 -1
- package/templates/docs/00-integration/client-lib-folder.md +1 -0
- package/templates/docs/00-integration/wizard-chrome-contract.md +2 -1
- package/templates/docs/00-integration/wizard-preview-build-guide.md +17 -0
- package/templates/docs/01-components/03-rating-scale.md +8 -3
- package/templates/docs/02-reference/value-derivation.md +2 -0
- package/templates/docs/03-ui-specs/01-rating.md +15 -1
- package/templates/docs/templates/CsatMatrixScale.tsx +18 -24
- package/templates/docs/templates/CustomSliderTrack.tsx +5 -3
- package/templates/docs/templates/FileUploadScale.tsx +143 -18
- package/templates/docs/templates/Header.tsx +1 -0
- package/templates/docs/templates/HeatmapScale.tsx +11 -2
- package/templates/docs/templates/LanguageSelector.tsx +42 -19
- package/templates/docs/templates/LikertMatrixScale.tsx +43 -33
- package/templates/docs/templates/MatrixDropdown.tsx +12 -11
- package/templates/docs/templates/Question.tsx +109 -44
- package/templates/docs/templates/RankOrderScale.tsx +22 -5
- package/templates/docs/templates/RatingScale.tsx +77 -39
- package/templates/docs/templates/SliderMatrixScale.tsx +44 -16
- package/templates/docs/templates/selectionStyles.ts +90 -2
- package/templates/docs/templates/surveyUiIcons.tsx +2 -2
- package/templates/docs/templates/surveyUiScaleUtils.ts +52 -5
- package/templates/docs/templates/uiConfig.ts +56 -0
- package/templates/preview-harness/preview-bridge.inline.js +44 -0
- package/templates/preview-harness/previewPages.js +76 -51
- package/templates/preview-harness/previewPages.ts +7 -34
- package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +7 -0
- package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +59 -30
- package/templates/preview-harness/vite-app/src/SurveyPagePreview.tsx +36 -8
- package/templates/preview-harness/vite-app/src/fixtures/questions.ts +182 -82
- package/templates/previewBridge.ts +33 -0
- package/templates/survey-theme.css +13 -4
- package/templates/wizard-dist/assets/{PreviewMock-Bax7oRAL.js → PreviewMock-DbbLpHdF.js} +1 -1
- package/templates/wizard-dist/assets/TypePanel-DQbV2iCf.js +1 -0
- package/templates/wizard-dist/assets/index-CY7WMJ93.js +34 -0
- package/templates/wizard-dist/index.html +1 -1
- package/templates/wizard-dist/assets/TypePanel-D2t4FPSd.js +0 -1
- package/templates/wizard-dist/assets/index-c5lka74l.js +0 -34
|
@@ -6,7 +6,12 @@ import {
|
|
|
6
6
|
scaleRadioDotStyle,
|
|
7
7
|
scaleCellSelectedStyle,
|
|
8
8
|
} from '@/lib/surveyUi/selectionStyles';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
getAccentColor,
|
|
11
|
+
isScaleAnswerSelected,
|
|
12
|
+
npsValueFromOption,
|
|
13
|
+
starValueFromOption,
|
|
14
|
+
} from './surveyUiScaleUtils';
|
|
10
15
|
import { getEmojiForIndex } from './surveyUiIcons';
|
|
11
16
|
|
|
12
17
|
type RatingScaleProps = {
|
|
@@ -15,22 +20,22 @@ type RatingScaleProps = {
|
|
|
15
20
|
selectedValue?: AnswerValue;
|
|
16
21
|
onSelect: (value: AnswerValue) => void;
|
|
17
22
|
variant?: 'nps' | 'star';
|
|
18
|
-
buttonStyle?: 'standard' | 'numbered' | 'emoji';
|
|
23
|
+
buttonStyle?: 'standard' | 'numbered' | 'emoji' | 'pill';
|
|
19
24
|
};
|
|
20
25
|
|
|
21
26
|
function resolveBadgeColor(index: number, value: number): string {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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();
|
|
25
31
|
if (mode === 'monochrome') {
|
|
26
32
|
return (
|
|
27
|
-
getComputedStyle(
|
|
28
|
-
'var(--cfm-primary)'
|
|
33
|
+
getComputedStyle(root).getPropertyValue('--cfm-number-mono-color').trim() || getAccentColor(value)
|
|
29
34
|
);
|
|
30
35
|
}
|
|
31
36
|
if (mode === 'individual') {
|
|
32
37
|
return (
|
|
33
|
-
getComputedStyle(
|
|
38
|
+
getComputedStyle(root).getPropertyValue(`--cfm-number-color-${index}`).trim() ||
|
|
34
39
|
getAccentColor(value)
|
|
35
40
|
);
|
|
36
41
|
}
|
|
@@ -52,7 +57,7 @@ export default function RatingScale({
|
|
|
52
57
|
: npsValueFromOption(option as NpsScalePoint, index);
|
|
53
58
|
const numericValue = typeof value === 'number' ? value : index;
|
|
54
59
|
const color =
|
|
55
|
-
variant === 'nps' ? resolveBadgeColor(index, numericValue) : 'var(--cfm-primary)';
|
|
60
|
+
variant === 'nps' ? resolveBadgeColor(index, numericValue) : 'var(--cfm-primary, #e20074)';
|
|
56
61
|
return { id: option.id, label: option.optionLabel, value, color };
|
|
57
62
|
});
|
|
58
63
|
|
|
@@ -62,19 +67,34 @@ export default function RatingScale({
|
|
|
62
67
|
gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))`,
|
|
63
68
|
gap: 'var(--cfm-nps-cell-gap, 2px)',
|
|
64
69
|
};
|
|
70
|
+
const showNumberBadges = buttonStyle !== 'standard';
|
|
71
|
+
const usePillCells = buttonStyle === 'pill';
|
|
65
72
|
|
|
66
73
|
if (buttonStyle === 'emoji') {
|
|
67
74
|
return (
|
|
68
75
|
<div role="radiogroup" className="w-full overflow-x-auto" data-cfm-nps-area>
|
|
69
76
|
<div className="min-w-0" style={gridStyle}>
|
|
70
77
|
{resolvedOptions.map((option, index) => {
|
|
71
|
-
const isSelected = selectedValue
|
|
78
|
+
const isSelected = isScaleAnswerSelected(selectedValue, option.value);
|
|
72
79
|
return (
|
|
73
|
-
<label
|
|
74
|
-
|
|
80
|
+
<label
|
|
81
|
+
key={`${questionId}-${option.value}`}
|
|
82
|
+
title={option.label}
|
|
83
|
+
className="flex cursor-pointer justify-center"
|
|
84
|
+
>
|
|
85
|
+
<input
|
|
86
|
+
type="radio"
|
|
87
|
+
name={questionId}
|
|
88
|
+
checked={isSelected}
|
|
89
|
+
onChange={() => onSelect(option.value)}
|
|
90
|
+
className="sr-only"
|
|
91
|
+
/>
|
|
75
92
|
<div
|
|
76
93
|
style={{
|
|
77
|
-
...scaleRadioRingStyle(isSelected, {
|
|
94
|
+
...scaleRadioRingStyle(isSelected, {
|
|
95
|
+
pill: false,
|
|
96
|
+
size: 'var(--cfm-csat-emoji-size, 40px)',
|
|
97
|
+
}),
|
|
78
98
|
fontSize: 'var(--cfm-csat-emoji-size, 24px)',
|
|
79
99
|
}}
|
|
80
100
|
>
|
|
@@ -90,28 +110,30 @@ export default function RatingScale({
|
|
|
90
110
|
|
|
91
111
|
return (
|
|
92
112
|
<div role="radiogroup" className="w-full overflow-x-auto" data-cfm-nps-area>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<div
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
{showNumberBadges && (
|
|
114
|
+
<div className="mb-3 min-w-0" style={gridStyle}>
|
|
115
|
+
{resolvedOptions.map((option) => {
|
|
116
|
+
const isSelected = isScaleAnswerSelected(selectedValue, option.value);
|
|
117
|
+
return (
|
|
118
|
+
<div key={`${questionId}-badge-${option.value}`} className="flex justify-center">
|
|
119
|
+
<div
|
|
120
|
+
className="flex items-center justify-center rounded-[3px] text-[10px] font-bold text-white shadow-sm sm:text-xs"
|
|
121
|
+
style={{
|
|
122
|
+
width: 'var(--cfm-nps-cell-size, 24px)',
|
|
123
|
+
height: 'var(--cfm-nps-cell-size, 24px)',
|
|
124
|
+
...(isSelected
|
|
125
|
+
? scaleCellSelectedStyle(true)
|
|
126
|
+
: { backgroundColor: option.color }),
|
|
127
|
+
}}
|
|
128
|
+
title={option.label}
|
|
129
|
+
>
|
|
130
|
+
{option.label}
|
|
131
|
+
</div>
|
|
110
132
|
</div>
|
|
111
|
-
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
|
|
133
|
+
);
|
|
134
|
+
})}
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
115
137
|
|
|
116
138
|
<div
|
|
117
139
|
className="relative w-full rounded-md px-1 py-2"
|
|
@@ -119,12 +141,28 @@ export default function RatingScale({
|
|
|
119
141
|
>
|
|
120
142
|
<div className="min-w-0" style={gridStyle}>
|
|
121
143
|
{resolvedOptions.map((option) => {
|
|
122
|
-
const isSelected = selectedValue
|
|
144
|
+
const isSelected = isScaleAnswerSelected(selectedValue, option.value);
|
|
123
145
|
return (
|
|
124
|
-
<label
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
146
|
+
<label
|
|
147
|
+
key={`${questionId}-${option.value}`}
|
|
148
|
+
title={option.label}
|
|
149
|
+
className="flex cursor-pointer justify-center"
|
|
150
|
+
>
|
|
151
|
+
<input
|
|
152
|
+
type="radio"
|
|
153
|
+
name={questionId}
|
|
154
|
+
checked={isSelected}
|
|
155
|
+
onChange={() => onSelect(option.value)}
|
|
156
|
+
className="sr-only"
|
|
157
|
+
/>
|
|
158
|
+
<div
|
|
159
|
+
className="flex items-center justify-center rounded-full border-2 transition-all"
|
|
160
|
+
style={scaleRadioRingStyle(isSelected, {
|
|
161
|
+
size: 'var(--cfm-nps-cell-size, 40px)',
|
|
162
|
+
pill: usePillCells,
|
|
163
|
+
})}
|
|
164
|
+
>
|
|
165
|
+
<div style={scaleRadioDotStyle(isSelected, { pill: usePillCells })} />
|
|
128
166
|
</div>
|
|
129
167
|
</label>
|
|
130
168
|
);
|
|
@@ -9,6 +9,10 @@ import React from 'react';
|
|
|
9
9
|
import type { SliderMatrixQuestion, MatrixRowAnswers } from '@explorer02/cfm-survey-sdk';
|
|
10
10
|
import { tickColorFromIndex } from './surveyUiScaleUtils';
|
|
11
11
|
import { CustomSliderTrack } from './CustomSliderTrack';
|
|
12
|
+
import { hintLabelStyle, sliderTickLabelStyle } from '@/lib/surveyUi/labelStyles';
|
|
13
|
+
|
|
14
|
+
const MATRIX_ROW_LABEL_WIDTH = 'var(--cfm-matrix-row-width, 180px)';
|
|
15
|
+
const SLIDER_ROW_BAND = 'var(--cfm-slider-row-band, var(--cfm-row-band, #f3f4f6))';
|
|
12
16
|
|
|
13
17
|
type SliderMatrixScaleProps = {
|
|
14
18
|
question: SliderMatrixQuestion;
|
|
@@ -67,14 +71,22 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
|
|
|
67
71
|
display: 'flex', width: '100%', marginBottom: '4px', alignItems: 'flex-end',
|
|
68
72
|
padding: '0 16px', boxSizing: 'border-box',
|
|
69
73
|
}}>
|
|
70
|
-
<div style={{ flex:
|
|
74
|
+
<div style={{ flex: `0 0 ${MATRIX_ROW_LABEL_WIDTH}`, paddingRight: '16px' }} />
|
|
71
75
|
|
|
72
76
|
<div style={{ flex: '1', position: 'relative' }}>
|
|
73
77
|
{labels && labels.length > 0 && (
|
|
74
|
-
<div
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
<div
|
|
79
|
+
style={{
|
|
80
|
+
position: 'relative',
|
|
81
|
+
display: 'flex',
|
|
82
|
+
justifyContent: 'space-between',
|
|
83
|
+
margin: '0 16px',
|
|
84
|
+
marginBottom: '16px',
|
|
85
|
+
fontSize: '14px',
|
|
86
|
+
fontWeight: 500,
|
|
87
|
+
...hintLabelStyle(),
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
78
90
|
{labels.map((lbl, idx) => (
|
|
79
91
|
<div key={idx} style={{ width: 0, display: 'flex', justifyContent: 'center' }}>
|
|
80
92
|
<span style={{ whiteSpace: 'nowrap' }}>{lbl}</span>
|
|
@@ -83,10 +95,17 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
|
|
|
83
95
|
</div>
|
|
84
96
|
)}
|
|
85
97
|
|
|
86
|
-
<div
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
<div
|
|
99
|
+
style={{
|
|
100
|
+
position: 'relative',
|
|
101
|
+
margin: '0 16px',
|
|
102
|
+
marginBottom: '8px',
|
|
103
|
+
fontSize: '14px',
|
|
104
|
+
fontWeight: 400,
|
|
105
|
+
height: '28px',
|
|
106
|
+
...sliderTickLabelStyle(),
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
90
109
|
{tickValues.length > 0 ? (
|
|
91
110
|
tickValues.map((tv, idx) => {
|
|
92
111
|
const percentage = max !== min ? ((tv.value - min) / (max - min)) * 100 : 0;
|
|
@@ -101,11 +120,20 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
|
|
|
101
120
|
{tv.label}
|
|
102
121
|
</div>
|
|
103
122
|
) : (
|
|
104
|
-
<div
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
123
|
+
<div
|
|
124
|
+
style={{
|
|
125
|
+
backgroundColor: tv.color,
|
|
126
|
+
color: 'white',
|
|
127
|
+
padding: '4px 8px',
|
|
128
|
+
borderRadius: '4px',
|
|
129
|
+
fontWeight: 600,
|
|
130
|
+
wordBreak: 'break-word',
|
|
131
|
+
textAlign: 'center',
|
|
132
|
+
lineHeight: '1.2',
|
|
133
|
+
boxShadow: '0 1px 2px rgba(0,0,0,0.1)',
|
|
134
|
+
minWidth: 'var(--cfm-slider-tick-size, 28px)',
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
109
137
|
{tv.label}
|
|
110
138
|
</div>
|
|
111
139
|
)}
|
|
@@ -151,9 +179,9 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
|
|
|
151
179
|
return (
|
|
152
180
|
<div key={row.id} style={{
|
|
153
181
|
display: 'flex', width: '100%', alignItems: 'center',
|
|
154
|
-
backgroundColor:
|
|
182
|
+
backgroundColor: SLIDER_ROW_BAND, borderRadius: '12px', padding: '16px', boxSizing: 'border-box',
|
|
155
183
|
}}>
|
|
156
|
-
<div style={{ flex:
|
|
184
|
+
<div style={{ flex: `0 0 ${MATRIX_ROW_LABEL_WIDTH}`, paddingRight: '16px', fontSize: '15px', color: '#111827', wordWrap: 'break-word', display: 'flex', alignItems: 'center' }}>
|
|
157
185
|
{row.statementText !== 'Question' && row.statementText !== '' ? (
|
|
158
186
|
<span dangerouslySetInnerHTML={{ __html: row.statementText }} />
|
|
159
187
|
) : null}
|
|
@@ -8,8 +8,96 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { CSSProperties } from 'react';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
export const
|
|
11
|
+
/** Terminal hex fallbacks match apps/client — visible before survey-theme.css loads. */
|
|
12
|
+
export const cellSelectedVar = 'var(--cfm-matrix-selected, var(--cfm-primary, #fbe8f3))';
|
|
13
|
+
export const focusRingVar = 'var(--cfm-input-focus-ring, var(--cfm-primary, #e20074))';
|
|
14
|
+
export const mcqSelectedAccentVar = focusRingVar;
|
|
15
|
+
export const mcqSelectedBgVar = 'var(--cfm-mcq-selected-bg, var(--cfm-secondary, #fdf2f8))';
|
|
16
|
+
export const matrixCarouselDotVar = 'var(--cfm-matrix-carousel-dot, #2563eb)';
|
|
17
|
+
|
|
18
|
+
const unselectedBorder = 'var(--cfm-card-border-color, #e5e5e5)';
|
|
19
|
+
|
|
20
|
+
function readMcqOptionStyle(): string {
|
|
21
|
+
if (typeof document === 'undefined') return 'outlined';
|
|
22
|
+
const root = document.documentElement;
|
|
23
|
+
return (
|
|
24
|
+
root.getAttribute('data-cfm-mcq-style') ||
|
|
25
|
+
getComputedStyle(root).getPropertyValue('--cfm-mcq-option-style').trim() ||
|
|
26
|
+
'outlined'
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** MCQ option card — border, background, padding (wizard: --cfm-mcq-*). */
|
|
31
|
+
export function mcqOptionCardStyle(isSelected: boolean): CSSProperties {
|
|
32
|
+
const optionStyle = readMcqOptionStyle();
|
|
33
|
+
const base: CSSProperties = {
|
|
34
|
+
borderRadius: 'var(--cfm-mcq-border-radius, 8px)',
|
|
35
|
+
border: `2px solid ${isSelected ? focusRingVar : unselectedBorder}`,
|
|
36
|
+
background: isSelected ? mcqSelectedBgVar : 'var(--cfm-card-bg, #fff)',
|
|
37
|
+
padding: 'var(--cfm-mcq-card-padding, 16px 20px)',
|
|
38
|
+
marginBottom: 'var(--cfm-mcq-option-gap, 8px)',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (optionStyle === 'filled') {
|
|
42
|
+
return {
|
|
43
|
+
...base,
|
|
44
|
+
background: isSelected ? mcqSelectedBgVar : 'var(--cfm-secondary, #f9fafb)',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (optionStyle === 'minimal') {
|
|
49
|
+
return {
|
|
50
|
+
...base,
|
|
51
|
+
border: isSelected ? `2px solid ${focusRingVar}` : '2px solid transparent',
|
|
52
|
+
boxShadow: 'none',
|
|
53
|
+
background: isSelected ? mcqSelectedBgVar : 'transparent',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return base;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** MCQ checkbox control indicator. */
|
|
61
|
+
export function mcqCheckboxControlStyle(isSelected: boolean): CSSProperties {
|
|
62
|
+
return {
|
|
63
|
+
display: 'flex',
|
|
64
|
+
height: '20px',
|
|
65
|
+
width: '20px',
|
|
66
|
+
flexShrink: 0,
|
|
67
|
+
alignItems: 'center',
|
|
68
|
+
justifyContent: 'center',
|
|
69
|
+
borderRadius: '4px',
|
|
70
|
+
border: `1px solid ${isSelected ? focusRingVar : '#9ca3af'}`,
|
|
71
|
+
backgroundColor: isSelected ? focusRingVar : '#fff',
|
|
72
|
+
transition: 'all 0.15s',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** MCQ radio outer ring. */
|
|
77
|
+
export function mcqRadioOuterStyle(isSelected: boolean): CSSProperties {
|
|
78
|
+
return {
|
|
79
|
+
display: 'flex',
|
|
80
|
+
height: '20px',
|
|
81
|
+
width: '20px',
|
|
82
|
+
flexShrink: 0,
|
|
83
|
+
alignItems: 'center',
|
|
84
|
+
justifyContent: 'center',
|
|
85
|
+
borderRadius: '50%',
|
|
86
|
+
border: isSelected ? `2px solid ${focusRingVar}` : '1px solid #9ca3af',
|
|
87
|
+
backgroundColor: isSelected ? cellSelectedVar : '#fff',
|
|
88
|
+
transition: 'all 0.15s',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** MCQ radio inner dot — focus ring color. */
|
|
93
|
+
export function mcqRadioInnerStyle(): CSSProperties {
|
|
94
|
+
return {
|
|
95
|
+
height: '10px',
|
|
96
|
+
width: '10px',
|
|
97
|
+
borderRadius: '50%',
|
|
98
|
+
backgroundColor: focusRingVar,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
13
101
|
|
|
14
102
|
const unselectedRing = '#d1d5db';
|
|
15
103
|
const unselectedDot = '#d1d5db';
|
|
@@ -55,9 +55,9 @@ export function getEmojiForIndex(index: number, total: number): React.ReactNode
|
|
|
55
55
|
|
|
56
56
|
export const CsatStarIcons: { filled: React.ReactNode; empty: React.ReactNode } = {
|
|
57
57
|
filled: React.createElement(FaStar as React.ElementType, {
|
|
58
|
-
style: { color: '#e20074', fontSize: '28px' },
|
|
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: { color: '#d1d5db', fontSize: '28px', stroke: '#d1d5db', strokeWidth: '16px' },
|
|
61
|
+
style: { color: '#d1d5db', fontSize: 'var(--cfm-csat-emoji-size, 28px)', stroke: '#d1d5db', strokeWidth: '16px' },
|
|
62
62
|
}),
|
|
63
63
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Portable scale utilities — copy to src/lib/surveyUiScaleUtils.ts or co-locate with components.
|
|
3
3
|
*/
|
|
4
|
-
import type { NpsScalePoint, ScaleColumn } from '@explorer02/cfm-survey-sdk';
|
|
4
|
+
import type { AnswerValue, NpsScalePoint, ScaleColumn } from '@explorer02/cfm-survey-sdk';
|
|
5
5
|
|
|
6
6
|
export const SCALE_COLORS: readonly string[] = [
|
|
7
7
|
'#e2001a', '#e4251b', '#ec610a', '#f18b00', '#f7b200', '#fcd900',
|
|
@@ -13,23 +13,70 @@ export function getAccentColor(value: number): string {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function tickColorFromIndex(index: number, total: number): string {
|
|
16
|
+
if (typeof document !== 'undefined') {
|
|
17
|
+
const resolved = resolveNumberLabelColor(index);
|
|
18
|
+
if (resolved) return resolved;
|
|
19
|
+
}
|
|
16
20
|
if (total <= 1) return '#e20074';
|
|
17
21
|
const hue = Math.round((index / (total - 1)) * 120);
|
|
18
22
|
return `hsl(${hue}, 90%, 40%)`;
|
|
19
23
|
}
|
|
20
24
|
|
|
25
|
+
/** Reads wizard number-label CSS vars for traffic / monochrome / individual modes. */
|
|
26
|
+
export function resolveNumberLabelColor(index: number, fallback = ''): string {
|
|
27
|
+
if (typeof document === 'undefined') return fallback || getAccentColor(index);
|
|
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() ||
|
|
34
|
+
fallback ||
|
|
35
|
+
'#9CA3AF'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (mode === 'individual') {
|
|
39
|
+
return (
|
|
40
|
+
getComputedStyle(root).getPropertyValue(`--cfm-number-color-${index}`).trim() ||
|
|
41
|
+
fallback ||
|
|
42
|
+
'#9CA3AF'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return fallback || getAccentColor(index);
|
|
46
|
+
}
|
|
47
|
+
|
|
21
48
|
export function nonNaScaleColumns(columns: ScaleColumn[]): ScaleColumn[] {
|
|
22
49
|
return columns.filter(col => col.id !== 'na');
|
|
23
50
|
}
|
|
24
51
|
|
|
52
|
+
function stripHtml(label: string): string {
|
|
53
|
+
return label.replace(/<[^>]*>/g, '').trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
25
56
|
export function starValueFromOption(option: NpsScalePoint, index: number): number {
|
|
26
|
-
const
|
|
27
|
-
|
|
57
|
+
const label = stripHtml(option.optionLabel);
|
|
58
|
+
const numeric = Number(label);
|
|
59
|
+
return Number.isFinite(numeric) && label !== '' ? numeric : index;
|
|
28
60
|
}
|
|
29
61
|
|
|
30
62
|
export function npsValueFromOption(option: NpsScalePoint, index: number): number {
|
|
31
|
-
const
|
|
32
|
-
|
|
63
|
+
const label = stripHtml(option.optionLabel);
|
|
64
|
+
const numeric = Number(label);
|
|
65
|
+
return Number.isFinite(numeric) && label !== '' ? numeric : index;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Loose match for NPS/star stored answers (number) vs derived option values. */
|
|
69
|
+
export function isScaleAnswerSelected(
|
|
70
|
+
selectedValue: AnswerValue | undefined,
|
|
71
|
+
optionValue: number,
|
|
72
|
+
): boolean {
|
|
73
|
+
if (selectedValue === undefined || selectedValue === null) return false;
|
|
74
|
+
if (typeof selectedValue === 'number') return selectedValue === optionValue;
|
|
75
|
+
if (typeof selectedValue === 'string') {
|
|
76
|
+
const parsed = Number(stripHtml(selectedValue));
|
|
77
|
+
return Number.isFinite(parsed) && parsed === optionValue;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
33
80
|
}
|
|
34
81
|
|
|
35
82
|
/** Matches SDK matrixColumnStoredValue — scale-matrix answers store column ids. */
|
|
@@ -31,3 +31,59 @@ export function getLogoSrc(options?: { staticExport?: boolean }): string | null
|
|
|
31
31
|
export function shouldShowCompanyName(): boolean {
|
|
32
32
|
return surveyUiConfig.global?.header?.showCompanyName !== false;
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
export function getSurveyTitle(): string {
|
|
36
|
+
return surveyUiConfig.global?.surveyTitle?.trim() ?? '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getThankYouMessage(): string {
|
|
40
|
+
return surveyUiConfig.global?.thankYouMessage?.trim() ?? '';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getBrowserTabTitle(): string {
|
|
44
|
+
return surveyUiConfig.global?.browserTabTitle?.trim() ?? getSurveyTitle();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getButtonLabels(): { next: string; back: string; submit: string } {
|
|
48
|
+
const buttons = surveyUiConfig.global?.buttons;
|
|
49
|
+
return {
|
|
50
|
+
next: buttons?.next?.trim() || 'Next',
|
|
51
|
+
back: buttons?.back?.trim() || 'Back',
|
|
52
|
+
submit: buttons?.submit?.trim() || 'Submit',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getFooterCopyright(): string {
|
|
57
|
+
return surveyUiConfig.global?.footer?.copyrightText?.trim() ?? '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getFooterLinks(): { label: string; url: string }[] {
|
|
61
|
+
const links = surveyUiConfig.global?.footer?.links;
|
|
62
|
+
return Array.isArray(links) ? links : [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getLayoutFlags() {
|
|
66
|
+
const layout = surveyUiConfig.global?.layout ?? {};
|
|
67
|
+
const header = surveyUiConfig.global?.header ?? {};
|
|
68
|
+
const footer = surveyUiConfig.global?.footer ?? {};
|
|
69
|
+
return {
|
|
70
|
+
showHeader: layout.showHeader !== false,
|
|
71
|
+
showFooter: layout.showFooter !== false,
|
|
72
|
+
showProgressBar: layout.showProgressBar !== false,
|
|
73
|
+
showLanguageSwitcher: layout.showLanguageSwitcher === true,
|
|
74
|
+
showNextButton: layout.showNextButton !== false,
|
|
75
|
+
showBackButton: layout.showBackButton !== false,
|
|
76
|
+
showQuestionNumbers: layout.showQuestionNumbers !== false,
|
|
77
|
+
showRequiredAsterisk: layout.showRequiredAsterisk !== false,
|
|
78
|
+
showCompanyName: header.showCompanyName !== false,
|
|
79
|
+
showFooterLogo: footer.showLogo !== false,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getPlaceholders(): Record<string, string> {
|
|
84
|
+
return { ...(surveyUiConfig.placeholders ?? {}) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isDebugEnabled(): boolean {
|
|
88
|
+
return surveyUiConfig.debug?.enabled === true;
|
|
89
|
+
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
delete pending[k];
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
applyThemeSideEffects();
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function queue(vars) {
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
root.style.setProperty(k, vars[k]);
|
|
34
35
|
}
|
|
35
36
|
}
|
|
37
|
+
applyThemeSideEffects();
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
function setDisplay(selector, visible) {
|
|
@@ -41,6 +43,44 @@
|
|
|
41
43
|
});
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
var TRAFFIC = ['#ef4444', '#f97316', '#eab308', '#84cc16', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7'];
|
|
47
|
+
|
|
48
|
+
function readVar(name, fallback) {
|
|
49
|
+
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function applyNumberBadgeColors() {
|
|
53
|
+
var mode = readVar('--cfm-number-label-mode', 'traffic');
|
|
54
|
+
var mono = readVar('--cfm-number-mono-color', '#9ca3af');
|
|
55
|
+
document.querySelectorAll('[data-cfm-number-badge]').forEach(function (el) {
|
|
56
|
+
if (el.classList.contains('selected')) return;
|
|
57
|
+
var i = parseInt(el.getAttribute('data-cfm-number-badge'), 10);
|
|
58
|
+
if (isNaN(i)) return;
|
|
59
|
+
var bg = TRAFFIC[i] || '#9ca3af';
|
|
60
|
+
if (mode === 'monochrome') bg = mono;
|
|
61
|
+
if (mode === 'individual') bg = readVar('--cfm-number-color-' + i, '#9ca3af');
|
|
62
|
+
el.style.backgroundColor = bg;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function applyMultiStatement(show) {
|
|
67
|
+
document.querySelectorAll('[data-cfm-matrix-row-extra]').forEach(function (el) {
|
|
68
|
+
el.style.display = show ? '' : 'none';
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function applyDropzoneStyle() {
|
|
73
|
+
var style = readVar('--cfm-upload-border-style', 'dashed');
|
|
74
|
+
document.querySelectorAll('[data-cfm-dropzone], .cfm-dropzone').forEach(function (el) {
|
|
75
|
+
el.style.borderStyle = style;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function applyThemeSideEffects() {
|
|
80
|
+
applyNumberBadgeColors();
|
|
81
|
+
applyDropzoneStyle();
|
|
82
|
+
}
|
|
83
|
+
|
|
44
84
|
function applyContent(patch) {
|
|
45
85
|
if (!patch) return;
|
|
46
86
|
if (patch.logoUrl !== undefined) {
|
|
@@ -188,7 +228,11 @@
|
|
|
188
228
|
el.textContent = patch.previewState.hintMaxText || '';
|
|
189
229
|
});
|
|
190
230
|
}
|
|
231
|
+
if (patch.previewState.multiStatement !== undefined) {
|
|
232
|
+
applyMultiStatement(!!patch.previewState.multiStatement);
|
|
233
|
+
}
|
|
191
234
|
}
|
|
235
|
+
applyThemeSideEffects();
|
|
192
236
|
}
|
|
193
237
|
|
|
194
238
|
function handleMessage(event) {
|