@explorer02/cfm-survey-sdk 0.2.9 → 0.3.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 +41 -38
- package/dist/cli/index.mjs +39 -36
- package/package.json +1 -1
- package/templates/docs/00-integration/agent-execution-flow.md +6 -6
- package/templates/docs/00-integration/agent-operating-contract.md +1 -2
- package/templates/docs/00-integration/client-lib-folder.md +1 -0
- package/templates/docs/00-integration/component-checklist.md +0 -1
- package/templates/docs/00-integration/ui-customization-wizard.md +38 -29
- package/templates/docs/00-integration/wizard-chrome-contract.md +3 -12
- package/templates/docs/00-integration/wizard-config-handoff.md +19 -11
- package/templates/docs/00-integration/wizard-post-customize-build.md +0 -6
- package/templates/docs/00-integration/wizard-preview-build-guide.md +17 -0
- package/templates/docs/00-integration/wizard-question-type-styling.md +3 -3
- package/templates/docs/00-integration/wizard-troubleshooting.md +4 -20
- 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/index.md +1 -1
- package/templates/docs/templates/CsatMatrixScale.tsx +59 -53
- package/templates/docs/templates/CustomSliderTrack.tsx +8 -17
- package/templates/docs/templates/FileUploadScale.tsx +4 -12
- package/templates/docs/templates/Header.tsx +49 -32
- package/templates/docs/templates/HeatmapScale.tsx +2 -5
- package/templates/docs/templates/LikertMatrixScale.tsx +45 -57
- package/templates/docs/templates/MatrixDropdown.tsx +11 -12
- package/templates/docs/templates/Question.tsx +24 -4
- package/templates/docs/templates/RankOrderScale.tsx +9 -42
- package/templates/docs/templates/RatingScale.tsx +73 -75
- package/templates/docs/templates/SliderMatrixScale.tsx +3 -4
- package/templates/docs/templates/selectionStyles.ts +3 -2
- package/templates/docs/templates/surveyUiScaleUtils.ts +15 -1
- package/templates/docs/templates/verify-agent-build.sh +0 -17
- package/templates/preview-harness/preview-bridge.inline.js +61 -14
- package/templates/preview-harness/previewPages.js +84 -48
- package/templates/preview-harness/previewPages.ts +7 -5
- package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +22 -14
- package/templates/preview-harness/vite-app/src/fixtures/questions.ts +13 -2
- package/templates/preview-harness/vite-app/src/preview-live-overrides.css +6 -11
- package/templates/previewBridge.ts +38 -5
- package/templates/survey-theme.css +17 -17
- package/templates/wizard-dist/assets/{PreviewMock-CysRws9G.js → PreviewMock-Bax7oRAL.js} +1 -1
- package/templates/wizard-dist/assets/{TypePanel-DWX1LL3q.js → TypePanel-D2t4FPSd.js} +1 -1
- package/templates/wizard-dist/assets/index-BhWM50Yu.css +1 -0
- package/templates/wizard-dist/assets/index-c5lka74l.js +34 -0
- package/templates/wizard-dist/index.html +2 -2
- package/templates/wizard-dist/assets/index-DIuc7AU2.js +0 -34
- package/templates/wizard-dist/assets/index-pCM8n0FT.css +0 -1
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import type { CfmMatrixQuestion, MatrixRowAnswers } from '@explorer02/cfm-survey-sdk';
|
|
3
|
-
import { matrixColumnLabelStyle } from '@/lib/surveyUi/labelStyles';
|
|
4
|
-
import {
|
|
5
|
-
cellSelectedVar,
|
|
6
|
-
focusRingVar,
|
|
7
|
-
matrixRadioDotStyle,
|
|
8
|
-
matrixRadioRingStyle,
|
|
9
|
-
} from '@/lib/surveyUi/selectionStyles';
|
|
10
3
|
import { columnSubmitValue } from './surveyUiScaleUtils';
|
|
11
4
|
import MatrixDropdown from './MatrixDropdown';
|
|
5
|
+
import { matrixColumnLabelStyle } from '@/lib/surveyUi/labelStyles';
|
|
6
|
+
|
|
7
|
+
const MATRIX_ROW_LABEL_WIDTH = 'var(--cfm-matrix-row-width, 180px)';
|
|
12
8
|
|
|
13
9
|
type LikertMatrixScaleProps = {
|
|
14
10
|
question: CfmMatrixQuestion;
|
|
@@ -118,8 +114,8 @@ function LikertMatrixCarouselLayout({
|
|
|
118
114
|
<div style={{
|
|
119
115
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
120
116
|
width: '18px', height: '18px', borderRadius: '4px',
|
|
121
|
-
border: selected ?
|
|
122
|
-
backgroundColor: selected ?
|
|
117
|
+
border: selected ? '2px solid #e20074' : '2px solid #d1d5db',
|
|
118
|
+
backgroundColor: selected ? '#e20074' : '#fff',
|
|
123
119
|
flexShrink: 0, transition: 'all 0.15s'
|
|
124
120
|
}}>
|
|
125
121
|
{selected && (
|
|
@@ -129,8 +125,13 @@ function LikertMatrixCarouselLayout({
|
|
|
129
125
|
)}
|
|
130
126
|
</div>
|
|
131
127
|
) : (
|
|
132
|
-
<div style={
|
|
133
|
-
|
|
128
|
+
<div style={{
|
|
129
|
+
display: 'flex', width: '18px', height: '18px', flexShrink: 0,
|
|
130
|
+
alignItems: 'center', justifyContent: 'center', borderRadius: '50%',
|
|
131
|
+
border: selected ? '2px solid #e20074' : '1.5px solid #9ca3af',
|
|
132
|
+
backgroundColor: '#fff',
|
|
133
|
+
}}>
|
|
134
|
+
{selected && <div style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: '#e20074' }} />}
|
|
134
135
|
</div>
|
|
135
136
|
)}
|
|
136
137
|
<span style={{ fontSize: '15px', fontWeight: 400, color: '#374151', userSelect: 'none' }} dangerouslySetInnerHTML={{ __html: col.label }} />
|
|
@@ -208,7 +209,7 @@ function LikertMatrixGridLayout({
|
|
|
208
209
|
<div style={{ display: 'flex', flexDirection: 'column', marginBottom: '16px' }}>
|
|
209
210
|
{hasLabels && (
|
|
210
211
|
<div style={{ display: 'flex', alignItems: 'flex-end', minHeight: '24px', marginBottom: '8px' }}>
|
|
211
|
-
<div style={{ flex:
|
|
212
|
+
<div style={{ flex: '0 0 25%', paddingRight: '16px', boxSizing: 'border-box' }} />
|
|
212
213
|
<div style={{ flex: 1, position: 'relative', height: '20px' }}>
|
|
213
214
|
{labels.map((lbl, idx) => {
|
|
214
215
|
let leftPos = '50%';
|
|
@@ -226,12 +227,12 @@ function LikertMatrixGridLayout({
|
|
|
226
227
|
);
|
|
227
228
|
})}
|
|
228
229
|
</div>
|
|
229
|
-
<div style={{ flex:
|
|
230
|
+
<div style={{ flex: '0 0 25%', paddingLeft: '16px', boxSizing: 'border-box' }} />
|
|
230
231
|
</div>
|
|
231
232
|
)}
|
|
232
233
|
{showColumnHeaders && (
|
|
233
234
|
<div style={{ display: 'flex', alignItems: 'flex-end', minHeight: '24px' }}>
|
|
234
|
-
<div style={{ flex: isBipolar ? '0 0
|
|
235
|
+
<div style={{ flex: isBipolar ? '0 0 25%' : `0 0 ${MATRIX_ROW_LABEL_WIDTH}`, paddingRight: '16px', boxSizing: 'border-box' }} />
|
|
235
236
|
<div style={{ flex: 1, display: 'flex' }}>
|
|
236
237
|
<div style={{ flex: 1, display: 'grid', gridTemplateColumns: `repeat(${gridCols.length}, 1fr)` }}>
|
|
237
238
|
{gridCols.map((col) => (
|
|
@@ -242,7 +243,7 @@ function LikertMatrixGridLayout({
|
|
|
242
243
|
))}
|
|
243
244
|
</div>
|
|
244
245
|
</div>
|
|
245
|
-
{isBipolar && <div style={{ flex:
|
|
246
|
+
{isBipolar && <div style={{ flex: '0 0 25%', paddingLeft: '16px', boxSizing: 'border-box' }} />}
|
|
246
247
|
</div>
|
|
247
248
|
)}
|
|
248
249
|
</div>
|
|
@@ -260,10 +261,10 @@ function LikertMatrixGridLayout({
|
|
|
260
261
|
{repeatColumnHeaders && renderHeader()}
|
|
261
262
|
<div style={{
|
|
262
263
|
display: 'flex', alignItems: 'center', borderRadius: '8px',
|
|
263
|
-
padding: '12px 0', backgroundColor: !repeatColumnHeaders && rowIdx % 2 === 0 ? '
|
|
264
|
+
padding: '12px 0', backgroundColor: !repeatColumnHeaders && rowIdx % 2 === 0 ? 'rgba(249, 250, 251, 0.8)' : '#fff',
|
|
264
265
|
boxSizing: 'border-box'
|
|
265
266
|
}}>
|
|
266
|
-
<div style={{ flex: isBipolar ? '0 0
|
|
267
|
+
<div style={{ flex: isBipolar ? '0 0 25%' : `0 0 ${MATRIX_ROW_LABEL_WIDTH}`, padding: '0 16px', boxSizing: 'border-box' }}>
|
|
267
268
|
<span style={{ fontSize: '14px', fontWeight: 500, color: '#111827', lineHeight: 1.4, wordWrap: 'break-word', display: 'block' }}
|
|
268
269
|
dangerouslySetInnerHTML={{ __html: transposeTable ? ('label' in rowItem ? rowItem.label : '') : ('statementText' in rowItem ? rowItem.statementText : '') }} />
|
|
269
270
|
</div>
|
|
@@ -281,45 +282,32 @@ function LikertMatrixGridLayout({
|
|
|
281
282
|
|
|
282
283
|
return (
|
|
283
284
|
<div key={colItem.id} style={{ display: 'flex', justifyContent: 'center' }}>
|
|
284
|
-
<
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}}>
|
|
311
|
-
{selected && (
|
|
312
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
|
313
|
-
<polyline points="20 6 9 17 4 12" />
|
|
314
|
-
</svg>
|
|
315
|
-
)}
|
|
316
|
-
</div>
|
|
317
|
-
) : (
|
|
318
|
-
<div style={matrixRadioRingStyle(selected)}>
|
|
319
|
-
{selected && <div style={matrixRadioDotStyle(selected)} />}
|
|
320
|
-
</div>
|
|
321
|
-
)}
|
|
322
|
-
</button>
|
|
285
|
+
<label style={{
|
|
286
|
+
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
|
287
|
+
opacity: isDisabled ? 0.4 : 1,
|
|
288
|
+
padding: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center'
|
|
289
|
+
}}>
|
|
290
|
+
<input
|
|
291
|
+
type={isMultiple ? "checkbox" : "radio"}
|
|
292
|
+
checked={selected}
|
|
293
|
+
disabled={isDisabled}
|
|
294
|
+
onClick={(e) => {
|
|
295
|
+
if (!isMultiple && selected) {
|
|
296
|
+
onCellSelect(actualRowId, undefined);
|
|
297
|
+
}
|
|
298
|
+
}}
|
|
299
|
+
onChange={(e) => {
|
|
300
|
+
if (isMultiple || !selected) {
|
|
301
|
+
onCellSelect(actualRowId, actualColValue);
|
|
302
|
+
}
|
|
303
|
+
}}
|
|
304
|
+
style={{
|
|
305
|
+
width: '18px', height: '18px', accentColor: 'var(--cfm-input-focus-ring)',
|
|
306
|
+
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
|
307
|
+
...(isMultiple ? { borderRadius: '4px' } : {})
|
|
308
|
+
}}
|
|
309
|
+
/>
|
|
310
|
+
</label>
|
|
323
311
|
</div>
|
|
324
312
|
);
|
|
325
313
|
})}
|
|
@@ -327,7 +315,7 @@ function LikertMatrixGridLayout({
|
|
|
327
315
|
</div>
|
|
328
316
|
|
|
329
317
|
{isBipolar && (
|
|
330
|
-
<div style={{ flex: '0 0
|
|
318
|
+
<div style={{ flex: '0 0 25%', padding: '0 16px', textAlign: 'right', boxSizing: 'border-box' }}>
|
|
331
319
|
<span style={{ fontSize: '14px', fontWeight: 500, color: '#111827', lineHeight: 1.4, wordWrap: 'break-word', display: 'block' }}
|
|
332
320
|
dangerouslySetInnerHTML={{ __html: transposeTable ? '' : ('oppositeStatementText' in rowItem ? rowItem.oppositeStatementText ?? '' : '') }} />
|
|
333
321
|
</div>
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
import React, { useState, useRef, useEffect } from 'react';
|
|
10
10
|
import type { ScaleColumn } from '@explorer02/cfm-survey-sdk';
|
|
11
11
|
import { matrixColumnStoredValue } from '@explorer02/cfm-survey-sdk';
|
|
12
|
-
import { cellSelectedVar, focusRingVar } from '@/lib/surveyUi/selectionStyles';
|
|
13
12
|
|
|
14
13
|
type MatrixDropdownProps = {
|
|
15
14
|
options: ScaleColumn[];
|
|
@@ -73,9 +72,9 @@ export default function MatrixDropdown({
|
|
|
73
72
|
style={{
|
|
74
73
|
display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '8px',
|
|
75
74
|
width: '100%', padding: '10px 14px', backgroundColor: '#fff',
|
|
76
|
-
border: `1px solid ${isOpen ?
|
|
75
|
+
border: `1px solid ${isOpen ? '#e20074' : '#d1d5db'}`, borderRadius: '8px',
|
|
77
76
|
cursor: 'pointer', transition: 'all 0.2s', minHeight: '44px',
|
|
78
|
-
boxShadow: isOpen ? '0 0 0 3px
|
|
77
|
+
boxShadow: isOpen ? '0 0 0 3px rgba(226, 0, 116, 0.1)' : '0 1px 2px rgba(0,0,0,0.05)',
|
|
79
78
|
}}
|
|
80
79
|
>
|
|
81
80
|
{multiValues.length === 0 ? (
|
|
@@ -125,9 +124,9 @@ export default function MatrixDropdown({
|
|
|
125
124
|
style={{
|
|
126
125
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
127
126
|
width: '100%', padding: '14px 16px', backgroundColor: '#fff',
|
|
128
|
-
border: `1px solid ${isOpen ?
|
|
127
|
+
border: `1px solid ${isOpen ? '#e20074' : '#d1d5db'}`, borderRadius: '8px',
|
|
129
128
|
cursor: 'pointer', transition: 'all 0.2s',
|
|
130
|
-
boxShadow: isOpen ? '0 0 0 3px
|
|
129
|
+
boxShadow: isOpen ? '0 0 0 3px rgba(226, 0, 116, 0.1)' : '0 1px 2px rgba(0,0,0,0.05)',
|
|
131
130
|
}}
|
|
132
131
|
>
|
|
133
132
|
<span
|
|
@@ -166,12 +165,12 @@ export default function MatrixDropdown({
|
|
|
166
165
|
key={col.id}
|
|
167
166
|
style={{
|
|
168
167
|
padding: '12px 16px', cursor: isDisabled ? 'not-allowed' : 'pointer', transition: 'background-color 0.15s',
|
|
169
|
-
backgroundColor: isSelected ? '
|
|
168
|
+
backgroundColor: isSelected ? '#fdf2f8' : '#fff',
|
|
170
169
|
opacity: isDisabled ? 0.5 : 1,
|
|
171
170
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px',
|
|
172
171
|
}}
|
|
173
|
-
onMouseEnter={e => { if (!isDisabled) e.currentTarget.style.backgroundColor = '
|
|
174
|
-
onMouseLeave={e => { if (!isDisabled) e.currentTarget.style.backgroundColor = isSelected ? '
|
|
172
|
+
onMouseEnter={e => { if (!isDisabled) e.currentTarget.style.backgroundColor = '#fdf2f8'; }}
|
|
173
|
+
onMouseLeave={e => { if (!isDisabled) e.currentTarget.style.backgroundColor = isSelected ? '#fdf2f8' : '#fff'; }}
|
|
175
174
|
onClick={e => {
|
|
176
175
|
e.preventDefault();
|
|
177
176
|
if (isDisabled) return;
|
|
@@ -188,8 +187,8 @@ export default function MatrixDropdown({
|
|
|
188
187
|
<div style={{
|
|
189
188
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
190
189
|
width: '18px', height: '18px', borderRadius: '4px',
|
|
191
|
-
border: isSelected ?
|
|
192
|
-
backgroundColor: isSelected ?
|
|
190
|
+
border: isSelected ? '2px solid #e20074' : '2px solid #d1d5db',
|
|
191
|
+
backgroundColor: isSelected ? '#e20074' : '#fff',
|
|
193
192
|
flexShrink: 0, transition: 'all 0.15s',
|
|
194
193
|
}}>
|
|
195
194
|
{isSelected && (
|
|
@@ -199,11 +198,11 @@ export default function MatrixDropdown({
|
|
|
199
198
|
)}
|
|
200
199
|
</div>
|
|
201
200
|
)}
|
|
202
|
-
<span style={{ fontSize: '15px', fontWeight: isSelected ? 600 : 400, color: isSelected ?
|
|
201
|
+
<span style={{ fontSize: '15px', fontWeight: isSelected ? 600 : 400, color: isSelected ? '#e20074' : '#374151', userSelect: 'none' }}
|
|
203
202
|
dangerouslySetInnerHTML={{ __html: col.label }} />
|
|
204
203
|
</div>
|
|
205
204
|
{!multiple && isSelected && (
|
|
206
|
-
<svg style={{ width: '18px', height: '18px', color:
|
|
205
|
+
<svg style={{ width: '18px', height: '18px', color: '#e20074', flexShrink: 0 }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
207
206
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
|
208
207
|
</svg>
|
|
209
208
|
)}
|
|
@@ -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 { hintLabelStyle } from '@/lib/surveyUi/labelStyles';
|
|
22
23
|
|
|
23
24
|
type QuestionProps = {
|
|
24
25
|
question: SurveyQuestion;
|
|
@@ -140,6 +141,29 @@ export default function Question({
|
|
|
140
141
|
|
|
141
142
|
{question.type === QUESTION_TYPE.NPS_SCALE && (
|
|
142
143
|
<div className="space-y-3">
|
|
144
|
+
{(question.minLabel || question.midLabel || question.maxLabel) && (
|
|
145
|
+
<div className="relative w-full h-6 text-xs font-semibold" style={hintLabelStyle()}>
|
|
146
|
+
<span
|
|
147
|
+
className="absolute left-0 top-0 max-w-[30%] text-left leading-tight"
|
|
148
|
+
dangerouslySetInnerHTML={{ __html: question.minLabel ?? '' }}
|
|
149
|
+
/>
|
|
150
|
+
{question.midLabel && (
|
|
151
|
+
<span
|
|
152
|
+
className="absolute top-0 -translate-x-1/2 text-center max-w-[40%] leading-tight"
|
|
153
|
+
style={{
|
|
154
|
+
left: `${question.midLabelIndex !== undefined && question.options.length > 1
|
|
155
|
+
? (question.midLabelIndex / (question.options.length - 1)) * 100
|
|
156
|
+
: 50}%`,
|
|
157
|
+
}}
|
|
158
|
+
dangerouslySetInnerHTML={{ __html: question.midLabel }}
|
|
159
|
+
/>
|
|
160
|
+
)}
|
|
161
|
+
<span
|
|
162
|
+
className="absolute right-0 top-0 max-w-[30%] text-right leading-tight"
|
|
163
|
+
dangerouslySetInnerHTML={{ __html: question.maxLabel ?? '' }}
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
143
167
|
<RatingScale
|
|
144
168
|
questionId={question.id}
|
|
145
169
|
options={question.options}
|
|
@@ -147,10 +171,6 @@ export default function Question({
|
|
|
147
171
|
onSelect={onSelect}
|
|
148
172
|
variant="nps"
|
|
149
173
|
buttonStyle={question.buttonStyle}
|
|
150
|
-
minLabel={question.minLabel}
|
|
151
|
-
midLabel={question.midLabel}
|
|
152
|
-
maxLabel={question.maxLabel}
|
|
153
|
-
midLabelIndex={question.midLabelIndex}
|
|
154
174
|
/>
|
|
155
175
|
</div>
|
|
156
176
|
)}
|
|
@@ -25,7 +25,6 @@ import {
|
|
|
25
25
|
verticalListSortingStrategy,
|
|
26
26
|
} from '@dnd-kit/sortable';
|
|
27
27
|
import { CSS } from '@dnd-kit/utilities';
|
|
28
|
-
import { focusRingVar } from '@/lib/surveyUi/selectionStyles';
|
|
29
28
|
import type {
|
|
30
29
|
RankOrderAnswers,
|
|
31
30
|
RankOrderOption,
|
|
@@ -121,35 +120,15 @@ function RankOrderOptionRow({
|
|
|
121
120
|
}: RankOrderOptionRowProps) {
|
|
122
121
|
return (
|
|
123
122
|
<div
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
alignItems: 'center',
|
|
127
|
-
gap: 'var(--cfm-rank-item-gap, 12px)',
|
|
128
|
-
borderRadius: '8px',
|
|
129
|
-
border: '1px solid #e5e5e5',
|
|
130
|
-
backgroundColor: 'var(--cfm-rank-item-bg, #fff)',
|
|
131
|
-
padding: 'var(--cfm-rank-item-padding, 12px 16px)',
|
|
132
|
-
transition: 'background-color 0.15s',
|
|
133
|
-
}}
|
|
134
|
-
className="hover:bg-gray-50/50"
|
|
123
|
+
className="flex items-center gap-3 rounded-lg border border-[#e5e5e5] px-4 py-3 transition-colors"
|
|
124
|
+
style={{ backgroundColor: 'var(--cfm-rank-item-bg, #fff)', padding: 'var(--cfm-rank-item-padding, 12px 16px)', marginBottom: 'var(--cfm-rank-item-gap, 12px)' }}
|
|
135
125
|
>
|
|
136
126
|
{rankSelect}
|
|
137
127
|
{dragHandle}
|
|
138
128
|
{rankBadge !== undefined && (
|
|
139
129
|
<span
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
height: '28px',
|
|
143
|
-
width: '28px',
|
|
144
|
-
flexShrink: 0,
|
|
145
|
-
alignItems: 'center',
|
|
146
|
-
justifyContent: 'center',
|
|
147
|
-
borderRadius: '9999px',
|
|
148
|
-
backgroundColor: 'var(--cfm-rank-badge)',
|
|
149
|
-
fontSize: '14px',
|
|
150
|
-
fontWeight: 600,
|
|
151
|
-
color: '#fff',
|
|
152
|
-
}}
|
|
130
|
+
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-sm font-semibold text-white"
|
|
131
|
+
style={{ backgroundColor: 'var(--cfm-rank-badge, var(--cfm-primary))' }}
|
|
153
132
|
>
|
|
154
133
|
{rankBadge}
|
|
155
134
|
</span>
|
|
@@ -174,7 +153,7 @@ function DropdownRankLayout({
|
|
|
174
153
|
};
|
|
175
154
|
|
|
176
155
|
return (
|
|
177
|
-
<div className="space-y-3"
|
|
156
|
+
<div className="space-y-3">
|
|
178
157
|
{question.options.map(option => {
|
|
179
158
|
const currentRank = rankAnswers[option.id];
|
|
180
159
|
|
|
@@ -188,19 +167,8 @@ function DropdownRankLayout({
|
|
|
188
167
|
aria-label={`Rank for ${option.optionLabel || option.id}`}
|
|
189
168
|
value={currentRank ?? ''}
|
|
190
169
|
onChange={event => handleRankChange(option.id, event.target.value)}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
width: '64px',
|
|
194
|
-
flexShrink: 0,
|
|
195
|
-
borderRadius: '4px',
|
|
196
|
-
border: '1px solid var(--cfm-rank-select-border, #d1d5db)',
|
|
197
|
-
backgroundColor: '#fff',
|
|
198
|
-
padding: '0 8px',
|
|
199
|
-
fontSize: '14px',
|
|
200
|
-
outline: 'none',
|
|
201
|
-
}}
|
|
202
|
-
onFocus={e => { e.currentTarget.style.borderColor = focusRingVar; }}
|
|
203
|
-
onBlur={e => { e.currentTarget.style.borderColor = 'var(--cfm-rank-select-border, #d1d5db)'; }}
|
|
170
|
+
className="h-10 w-16 shrink-0 rounded border bg-white px-2 text-sm outline-none"
|
|
171
|
+
style={{ borderColor: 'var(--cfm-rank-select-border, #d1d5db)' }}
|
|
204
172
|
>
|
|
205
173
|
<option value="">-</option>
|
|
206
174
|
{rankChoices.map(rank => (
|
|
@@ -257,8 +225,7 @@ function SortableRankItem({
|
|
|
257
225
|
<button
|
|
258
226
|
type="button"
|
|
259
227
|
aria-label={`Drag to reorder ${option.optionLabel || option.id}`}
|
|
260
|
-
className="flex h-10 w-8 shrink-0 cursor-grab items-center justify-center rounded active:cursor-grabbing"
|
|
261
|
-
style={{ color: 'var(--cfm-rank-handle, #6b7280)' }}
|
|
228
|
+
className="flex h-10 w-8 shrink-0 cursor-grab items-center justify-center rounded text-gray-500 hover:bg-gray-100 active:cursor-grabbing"
|
|
262
229
|
{...attributes}
|
|
263
230
|
{...listeners}
|
|
264
231
|
>
|
|
@@ -356,7 +323,7 @@ function DragDropRankLayout({
|
|
|
356
323
|
return (
|
|
357
324
|
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
|
358
325
|
<SortableContext items={orderedOptionIds} strategy={verticalListSortingStrategy}>
|
|
359
|
-
<div className="space-y-3"
|
|
326
|
+
<div className="space-y-3">
|
|
360
327
|
{orderedOptionIds.map((optionId, index) => {
|
|
361
328
|
const option = question.options.find(item => item.id === optionId);
|
|
362
329
|
if (!option) {
|
|
@@ -6,8 +6,12 @@ import {
|
|
|
6
6
|
scaleRadioDotStyle,
|
|
7
7
|
scaleCellSelectedStyle,
|
|
8
8
|
} from '@/lib/surveyUi/selectionStyles';
|
|
9
|
-
import {
|
|
10
|
-
|
|
9
|
+
import {
|
|
10
|
+
getAccentColor,
|
|
11
|
+
isScaleAnswerSelected,
|
|
12
|
+
npsValueFromOption,
|
|
13
|
+
starValueFromOption,
|
|
14
|
+
} from './surveyUiScaleUtils';
|
|
11
15
|
import { getEmojiForIndex } from './surveyUiIcons';
|
|
12
16
|
|
|
13
17
|
type RatingScaleProps = {
|
|
@@ -17,25 +21,21 @@ type RatingScaleProps = {
|
|
|
17
21
|
onSelect: (value: AnswerValue) => void;
|
|
18
22
|
variant?: 'nps' | 'star';
|
|
19
23
|
buttonStyle?: 'standard' | 'numbered' | 'emoji';
|
|
20
|
-
minLabel?: string;
|
|
21
|
-
midLabel?: string;
|
|
22
|
-
maxLabel?: string;
|
|
23
|
-
midLabelIndex?: number;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
function resolveBadgeColor(index: number, value: number): string {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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();
|
|
30
31
|
if (mode === 'monochrome') {
|
|
31
32
|
return (
|
|
32
|
-
getComputedStyle(
|
|
33
|
-
'var(--cfm-primary)'
|
|
33
|
+
getComputedStyle(root).getPropertyValue('--cfm-number-mono-color').trim() || getAccentColor(value)
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
if (mode === 'individual') {
|
|
37
37
|
return (
|
|
38
|
-
getComputedStyle(
|
|
38
|
+
getComputedStyle(root).getPropertyValue(`--cfm-number-color-${index}`).trim() ||
|
|
39
39
|
getAccentColor(value)
|
|
40
40
|
);
|
|
41
41
|
}
|
|
@@ -49,10 +49,6 @@ export default function RatingScale({
|
|
|
49
49
|
onSelect,
|
|
50
50
|
variant = 'nps',
|
|
51
51
|
buttonStyle = 'numbered',
|
|
52
|
-
minLabel,
|
|
53
|
-
midLabel,
|
|
54
|
-
maxLabel,
|
|
55
|
-
midLabelIndex,
|
|
56
52
|
}: RatingScaleProps) {
|
|
57
53
|
const resolvedOptions = options.map((option, index) => {
|
|
58
54
|
const value =
|
|
@@ -61,7 +57,7 @@ export default function RatingScale({
|
|
|
61
57
|
: npsValueFromOption(option as NpsScalePoint, index);
|
|
62
58
|
const numericValue = typeof value === 'number' ? value : index;
|
|
63
59
|
const color =
|
|
64
|
-
variant === 'nps' ? resolveBadgeColor(index, numericValue) : 'var(--cfm-primary)';
|
|
60
|
+
variant === 'nps' ? resolveBadgeColor(index, numericValue) : 'var(--cfm-primary, #e20074)';
|
|
65
61
|
return { id: option.id, label: option.optionLabel, value, color };
|
|
66
62
|
});
|
|
67
63
|
|
|
@@ -71,47 +67,33 @@ export default function RatingScale({
|
|
|
71
67
|
gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))`,
|
|
72
68
|
gap: 'var(--cfm-nps-cell-gap, 2px)',
|
|
73
69
|
};
|
|
74
|
-
|
|
75
|
-
const hintRow = (minLabel || midLabel || maxLabel) ? (
|
|
76
|
-
<div className="relative mb-3 h-6 w-full text-xs font-semibold">
|
|
77
|
-
<span
|
|
78
|
-
className="absolute left-0 top-0 max-w-[30%] text-left leading-tight"
|
|
79
|
-
style={hintLabelStyle()}
|
|
80
|
-
dangerouslySetInnerHTML={{ __html: minLabel ?? '' }}
|
|
81
|
-
/>
|
|
82
|
-
{midLabel && (
|
|
83
|
-
<span
|
|
84
|
-
className="absolute top-0 -translate-x-1/2 max-w-[40%] text-center leading-tight"
|
|
85
|
-
style={{
|
|
86
|
-
...hintLabelStyle(),
|
|
87
|
-
left: `${midLabelIndex !== undefined && columnCount > 1
|
|
88
|
-
? (midLabelIndex / (columnCount - 1)) * 100
|
|
89
|
-
: 50}%`,
|
|
90
|
-
}}
|
|
91
|
-
dangerouslySetInnerHTML={{ __html: midLabel }}
|
|
92
|
-
/>
|
|
93
|
-
)}
|
|
94
|
-
<span
|
|
95
|
-
className="absolute right-0 top-0 max-w-[30%] text-right leading-tight"
|
|
96
|
-
style={hintLabelStyle()}
|
|
97
|
-
dangerouslySetInnerHTML={{ __html: maxLabel ?? '' }}
|
|
98
|
-
/>
|
|
99
|
-
</div>
|
|
100
|
-
) : null;
|
|
70
|
+
const showNumberBadges = buttonStyle !== 'standard';
|
|
101
71
|
|
|
102
72
|
if (buttonStyle === 'emoji') {
|
|
103
73
|
return (
|
|
104
74
|
<div role="radiogroup" className="w-full overflow-x-auto" data-cfm-nps-area>
|
|
105
|
-
{hintRow}
|
|
106
75
|
<div className="min-w-0" style={gridStyle}>
|
|
107
76
|
{resolvedOptions.map((option, index) => {
|
|
108
|
-
const isSelected = selectedValue
|
|
77
|
+
const isSelected = isScaleAnswerSelected(selectedValue, option.value);
|
|
109
78
|
return (
|
|
110
|
-
<label
|
|
111
|
-
|
|
79
|
+
<label
|
|
80
|
+
key={`${questionId}-${option.value}`}
|
|
81
|
+
title={option.label}
|
|
82
|
+
className="flex cursor-pointer justify-center"
|
|
83
|
+
>
|
|
84
|
+
<input
|
|
85
|
+
type="radio"
|
|
86
|
+
name={questionId}
|
|
87
|
+
checked={isSelected}
|
|
88
|
+
onChange={() => onSelect(option.value)}
|
|
89
|
+
className="sr-only"
|
|
90
|
+
/>
|
|
112
91
|
<div
|
|
113
92
|
style={{
|
|
114
|
-
...scaleRadioRingStyle(isSelected, {
|
|
93
|
+
...scaleRadioRingStyle(isSelected, {
|
|
94
|
+
pill: false,
|
|
95
|
+
size: 'var(--cfm-csat-emoji-size, 48px)',
|
|
96
|
+
}),
|
|
115
97
|
fontSize: 'var(--cfm-csat-emoji-size, 24px)',
|
|
116
98
|
}}
|
|
117
99
|
>
|
|
@@ -127,29 +109,30 @@ export default function RatingScale({
|
|
|
127
109
|
|
|
128
110
|
return (
|
|
129
111
|
<div role="radiogroup" className="w-full overflow-x-auto" data-cfm-nps-area>
|
|
130
|
-
{
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
112
|
+
{showNumberBadges && (
|
|
113
|
+
<div className="mb-3 min-w-0" style={gridStyle}>
|
|
114
|
+
{resolvedOptions.map((option) => {
|
|
115
|
+
const isSelected = isScaleAnswerSelected(selectedValue, option.value);
|
|
116
|
+
return (
|
|
117
|
+
<div key={`${questionId}-badge-${option.value}`} className="flex justify-center">
|
|
118
|
+
<div
|
|
119
|
+
className="flex items-center justify-center rounded-[3px] text-[10px] font-bold text-white shadow-sm sm:text-xs"
|
|
120
|
+
style={{
|
|
121
|
+
width: 'var(--cfm-nps-cell-size, 24px)',
|
|
122
|
+
height: 'var(--cfm-nps-cell-size, 24px)',
|
|
123
|
+
...(isSelected
|
|
124
|
+
? scaleCellSelectedStyle(true)
|
|
125
|
+
: { backgroundColor: option.color }),
|
|
126
|
+
}}
|
|
127
|
+
title={option.label}
|
|
128
|
+
>
|
|
129
|
+
{option.label}
|
|
130
|
+
</div>
|
|
148
131
|
</div>
|
|
149
|
-
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
|
|
132
|
+
);
|
|
133
|
+
})}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
153
136
|
|
|
154
137
|
<div
|
|
155
138
|
className="relative w-full rounded-md px-1 py-2"
|
|
@@ -157,11 +140,26 @@ export default function RatingScale({
|
|
|
157
140
|
>
|
|
158
141
|
<div className="min-w-0" style={gridStyle}>
|
|
159
142
|
{resolvedOptions.map((option) => {
|
|
160
|
-
const isSelected = selectedValue
|
|
143
|
+
const isSelected = isScaleAnswerSelected(selectedValue, option.value);
|
|
161
144
|
return (
|
|
162
|
-
<label
|
|
163
|
-
|
|
164
|
-
|
|
145
|
+
<label
|
|
146
|
+
key={`${questionId}-${option.value}`}
|
|
147
|
+
title={option.label}
|
|
148
|
+
className="flex cursor-pointer justify-center"
|
|
149
|
+
>
|
|
150
|
+
<input
|
|
151
|
+
type="radio"
|
|
152
|
+
name={questionId}
|
|
153
|
+
checked={isSelected}
|
|
154
|
+
onChange={() => onSelect(option.value)}
|
|
155
|
+
className="sr-only"
|
|
156
|
+
/>
|
|
157
|
+
<div
|
|
158
|
+
className="flex items-center justify-center rounded-full border-2 transition-all"
|
|
159
|
+
style={scaleRadioRingStyle(isSelected, {
|
|
160
|
+
size: 'var(--cfm-nps-cell-size, 40px)',
|
|
161
|
+
})}
|
|
162
|
+
>
|
|
165
163
|
<div style={scaleRadioDotStyle(isSelected)} />
|
|
166
164
|
</div>
|
|
167
165
|
</label>
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
import React from 'react';
|
|
9
9
|
import type { SliderMatrixQuestion, MatrixRowAnswers } from '@explorer02/cfm-survey-sdk';
|
|
10
|
-
import { sliderTickLabelStyle } from '@/lib/surveyUi/labelStyles';
|
|
11
10
|
import { tickColorFromIndex } from './surveyUiScaleUtils';
|
|
12
11
|
import { CustomSliderTrack } from './CustomSliderTrack';
|
|
13
12
|
|
|
@@ -98,7 +97,7 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
|
|
|
98
97
|
width: `${sliceWidth}%`, display: 'flex', justifyContent: 'center',
|
|
99
98
|
}}>
|
|
100
99
|
{sliderType === 'graphics' ? (
|
|
101
|
-
<div style={{ wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2', fontWeight: 500, fontSize: '13px'
|
|
100
|
+
<div style={{ wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2', fontWeight: 500, color: '#4b5563', fontSize: '13px' }}>
|
|
102
101
|
{tv.label}
|
|
103
102
|
</div>
|
|
104
103
|
) : (
|
|
@@ -122,7 +121,7 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
|
|
|
122
121
|
position: 'absolute', left: `${percentage}%`, transform: 'translateX(-50%)',
|
|
123
122
|
width: `${sliceWidth}%`, display: 'flex', justifyContent: 'center',
|
|
124
123
|
}}>
|
|
125
|
-
<span style={{ wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2'
|
|
124
|
+
<span style={{ wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2' }}>{m}</span>
|
|
126
125
|
</div>
|
|
127
126
|
);
|
|
128
127
|
})
|
|
@@ -152,7 +151,7 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
|
|
|
152
151
|
return (
|
|
153
152
|
<div key={row.id} style={{
|
|
154
153
|
display: 'flex', width: '100%', alignItems: 'center',
|
|
155
|
-
backgroundColor: '
|
|
154
|
+
backgroundColor: '#f3f4f6', borderRadius: '12px', padding: '16px', boxSizing: 'border-box',
|
|
156
155
|
}}>
|
|
157
156
|
<div style={{ flex: '0 0 25%', paddingRight: '16px', fontSize: '15px', color: '#111827', wordWrap: 'break-word', display: 'flex', alignItems: 'center' }}>
|
|
158
157
|
{row.statementText !== 'Question' && row.statementText !== '' ? (
|