@explorer02/cfm-survey-sdk 0.3.1 → 0.3.3
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/templates/CsatMatrixScale.tsx +22 -27
- package/templates/docs/templates/CustomSliderTrack.tsx +5 -3
- package/templates/docs/templates/FileUploadScale.tsx +143 -18
- package/templates/docs/templates/HeatmapScale.tsx +11 -2
- package/templates/docs/templates/LanguageSelector.tsx +42 -19
- package/templates/docs/templates/LikertMatrixScale.tsx +45 -34
- package/templates/docs/templates/MatrixDropdown.tsx +12 -11
- package/templates/docs/templates/Question.tsx +116 -45
- package/templates/docs/templates/RankOrderScale.tsx +29 -5
- package/templates/docs/templates/RatingScale.tsx +7 -4
- package/templates/docs/templates/SliderMatrixScale.tsx +49 -17
- package/templates/docs/templates/selectionStyles.ts +103 -1
- package/templates/docs/templates/surveyUiIcons.tsx +2 -2
- package/templates/docs/templates/surveyUiScaleUtils.ts +38 -5
- package/templates/docs/templates/uiConfig.ts +56 -0
- package/templates/preview-harness/previewPages.js +4 -7
- package/templates/preview-harness/previewPages.ts +7 -34
- package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +48 -1
- package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +61 -31
- package/templates/preview-harness/vite-app/src/SurveyPagePreview.tsx +37 -8
- package/templates/preview-harness/vite-app/src/fixtures/questions.ts +182 -82
- package/templates/survey-theme.css +13 -4
- package/templates/wizard-dist/assets/{PreviewMock-Bax7oRAL.js → PreviewMock-_9SsNLwC.js} +1 -1
- package/templates/wizard-dist/assets/TypePanel-DyEcZlWP.js +1 -0
- package/templates/wizard-dist/assets/index-CVBd54V0.css +1 -0
- package/templates/wizard-dist/assets/index-pKQOivEa.js +34 -0
- package/templates/wizard-dist/index.html +2 -2
- package/templates/wizard-dist/assets/TypePanel-D2t4FPSd.js +0 -1
- package/templates/wizard-dist/assets/index-BhWM50Yu.css +0 -1
- package/templates/wizard-dist/assets/index-c5lka74l.js +0 -34
|
@@ -9,6 +9,7 @@
|
|
|
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 { focusRingVar, mcqSelectedAccentVar, mcqSelectedBgVar } from '@/lib/surveyUi/selectionStyles';
|
|
12
13
|
|
|
13
14
|
type MatrixDropdownProps = {
|
|
14
15
|
options: ScaleColumn[];
|
|
@@ -72,9 +73,9 @@ export default function MatrixDropdown({
|
|
|
72
73
|
style={{
|
|
73
74
|
display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '8px',
|
|
74
75
|
width: '100%', padding: '10px 14px', backgroundColor: '#fff',
|
|
75
|
-
border: `1px solid ${isOpen ?
|
|
76
|
+
border: `1px solid ${isOpen ? focusRingVar : '#d1d5db'}`, borderRadius: '8px',
|
|
76
77
|
cursor: 'pointer', transition: 'all 0.2s', minHeight: '44px',
|
|
77
|
-
boxShadow: isOpen ? '0 0 0 3px rgba(226, 0, 116, 0.1)' : '0 1px 2px rgba(0,0,0,0.05)',
|
|
78
|
+
boxShadow: isOpen ? '0 0 0 3px var(--cfm-input-focus-glow, rgba(226, 0, 116, 0.1))' : '0 1px 2px rgba(0,0,0,0.05)',
|
|
78
79
|
}}
|
|
79
80
|
>
|
|
80
81
|
{multiValues.length === 0 ? (
|
|
@@ -124,9 +125,9 @@ export default function MatrixDropdown({
|
|
|
124
125
|
style={{
|
|
125
126
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
126
127
|
width: '100%', padding: '14px 16px', backgroundColor: '#fff',
|
|
127
|
-
border: `1px solid ${isOpen ?
|
|
128
|
+
border: `1px solid ${isOpen ? focusRingVar : '#d1d5db'}`, borderRadius: '8px',
|
|
128
129
|
cursor: 'pointer', transition: 'all 0.2s',
|
|
129
|
-
boxShadow: isOpen ? '0 0 0 3px rgba(226, 0, 116, 0.1)' : '0 1px 2px rgba(0,0,0,0.05)',
|
|
130
|
+
boxShadow: isOpen ? '0 0 0 3px var(--cfm-input-focus-glow, rgba(226, 0, 116, 0.1))' : '0 1px 2px rgba(0,0,0,0.05)',
|
|
130
131
|
}}
|
|
131
132
|
>
|
|
132
133
|
<span
|
|
@@ -165,12 +166,12 @@ export default function MatrixDropdown({
|
|
|
165
166
|
key={col.id}
|
|
166
167
|
style={{
|
|
167
168
|
padding: '12px 16px', cursor: isDisabled ? 'not-allowed' : 'pointer', transition: 'background-color 0.15s',
|
|
168
|
-
backgroundColor: isSelected ?
|
|
169
|
+
backgroundColor: isSelected ? mcqSelectedBgVar : '#fff',
|
|
169
170
|
opacity: isDisabled ? 0.5 : 1,
|
|
170
171
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px',
|
|
171
172
|
}}
|
|
172
|
-
onMouseEnter={e => { if (!isDisabled) e.currentTarget.style.backgroundColor =
|
|
173
|
-
onMouseLeave={e => { if (!isDisabled) e.currentTarget.style.backgroundColor = isSelected ?
|
|
173
|
+
onMouseEnter={e => { if (!isDisabled) e.currentTarget.style.backgroundColor = mcqSelectedBgVar; }}
|
|
174
|
+
onMouseLeave={e => { if (!isDisabled) e.currentTarget.style.backgroundColor = isSelected ? mcqSelectedBgVar : '#fff'; }}
|
|
174
175
|
onClick={e => {
|
|
175
176
|
e.preventDefault();
|
|
176
177
|
if (isDisabled) return;
|
|
@@ -187,8 +188,8 @@ export default function MatrixDropdown({
|
|
|
187
188
|
<div style={{
|
|
188
189
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
189
190
|
width: '18px', height: '18px', borderRadius: '4px',
|
|
190
|
-
border: isSelected ?
|
|
191
|
-
backgroundColor: isSelected ?
|
|
191
|
+
border: isSelected ? `2px solid ${mcqSelectedAccentVar}` : '2px solid #d1d5db',
|
|
192
|
+
backgroundColor: isSelected ? mcqSelectedAccentVar : '#fff',
|
|
192
193
|
flexShrink: 0, transition: 'all 0.15s',
|
|
193
194
|
}}>
|
|
194
195
|
{isSelected && (
|
|
@@ -198,11 +199,11 @@ export default function MatrixDropdown({
|
|
|
198
199
|
)}
|
|
199
200
|
</div>
|
|
200
201
|
)}
|
|
201
|
-
<span style={{ fontSize: '15px', fontWeight: isSelected ? 600 : 400, color: isSelected ?
|
|
202
|
+
<span style={{ fontSize: '15px', fontWeight: isSelected ? 600 : 400, color: isSelected ? mcqSelectedAccentVar : '#374151', userSelect: 'none' }}
|
|
202
203
|
dangerouslySetInnerHTML={{ __html: col.label }} />
|
|
203
204
|
</div>
|
|
204
205
|
{!multiple && isSelected && (
|
|
205
|
-
<svg style={{ width: '18px', height: '18px', color:
|
|
206
|
+
<svg style={{ width: '18px', height: '18px', color: mcqSelectedAccentVar, flexShrink: 0 }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
206
207
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
|
207
208
|
</svg>
|
|
208
209
|
)}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import React, { useMemo } from 'react';
|
|
13
13
|
import type { SurveyQuestion, AnswerValue, SurveyAnswers, CustomFieldValueMap, HeatmapAnswer, HeatmapQuestion } from '@explorer02/cfm-survey-sdk';
|
|
14
|
-
import { QUESTION_TYPE, resolveSurveyQuestionPlaceholders, getVisibleMatrixItemsForAnswers, getVisibleMcqOptionsForAnswers, getVisibleRankOrderOptionsForAnswers } from '@explorer02/cfm-survey-sdk';
|
|
14
|
+
import { QUESTION_TYPE, resolveSurveyQuestionPlaceholders, getVisibleMatrixItemsForAnswers, getVisibleMcqOptionsForAnswers, getVisibleRankOrderOptionsForAnswers, normalizeRankOrderAnswers } from '@explorer02/cfm-survey-sdk';
|
|
15
15
|
import { CsatMatrixScale } from './CsatMatrixScale';
|
|
16
16
|
import { SliderMatrixScale } from './SliderMatrixScale';
|
|
17
17
|
import { LikertMatrixScale } from './LikertMatrixScale';
|
|
@@ -20,6 +20,13 @@ import { HeatmapScale } from './HeatmapScale';
|
|
|
20
20
|
import { RankOrderScale } from './RankOrderScale';
|
|
21
21
|
import RatingScale from './RatingScale';
|
|
22
22
|
import { hintLabelStyle } from '@/lib/surveyUi/labelStyles';
|
|
23
|
+
import {
|
|
24
|
+
mcqCheckboxControlStyle,
|
|
25
|
+
mcqOptionCardStyle,
|
|
26
|
+
mcqRadioInnerStyle,
|
|
27
|
+
mcqRadioOuterStyle,
|
|
28
|
+
focusRingVar,
|
|
29
|
+
} from '@/lib/surveyUi/selectionStyles';
|
|
23
30
|
|
|
24
31
|
type QuestionProps = {
|
|
25
32
|
question: SurveyQuestion;
|
|
@@ -114,6 +121,7 @@ export default function Question({
|
|
|
114
121
|
boxShadow: 'var(--cfm-card-shadow, 0 1px 3px rgba(0,0,0,0.1))',
|
|
115
122
|
borderRadius: 'var(--cfm-border-radius, 12px)',
|
|
116
123
|
fontFamily: 'var(--cfm-font-family)',
|
|
124
|
+
marginBottom: 'var(--cfm-card-gap, 32px)',
|
|
117
125
|
}}
|
|
118
126
|
>
|
|
119
127
|
<h2
|
|
@@ -142,15 +150,19 @@ export default function Question({
|
|
|
142
150
|
{question.type === QUESTION_TYPE.NPS_SCALE && (
|
|
143
151
|
<div className="space-y-3">
|
|
144
152
|
{(question.minLabel || question.midLabel || question.maxLabel) && (
|
|
145
|
-
<div className="relative w-full h-
|
|
153
|
+
<div className="relative w-full min-h-[24px] text-xs font-semibold">
|
|
146
154
|
<span
|
|
147
155
|
className="absolute left-0 top-0 max-w-[30%] text-left leading-tight"
|
|
156
|
+
style={{ ...hintLabelStyle(), padding: '2px 6px', borderRadius: '4px' }}
|
|
148
157
|
dangerouslySetInnerHTML={{ __html: question.minLabel ?? '' }}
|
|
149
158
|
/>
|
|
150
159
|
{question.midLabel && (
|
|
151
160
|
<span
|
|
152
161
|
className="absolute top-0 -translate-x-1/2 text-center max-w-[40%] leading-tight"
|
|
153
162
|
style={{
|
|
163
|
+
...hintLabelStyle(),
|
|
164
|
+
padding: '2px 6px',
|
|
165
|
+
borderRadius: '4px',
|
|
154
166
|
left: `${question.midLabelIndex !== undefined && question.options.length > 1
|
|
155
167
|
? (question.midLabelIndex / (question.options.length - 1)) * 100
|
|
156
168
|
: 50}%`,
|
|
@@ -160,6 +172,7 @@ export default function Question({
|
|
|
160
172
|
)}
|
|
161
173
|
<span
|
|
162
174
|
className="absolute right-0 top-0 max-w-[30%] text-right leading-tight"
|
|
175
|
+
style={{ ...hintLabelStyle(), padding: '2px 6px', borderRadius: '4px' }}
|
|
163
176
|
dangerouslySetInnerHTML={{ __html: question.maxLabel ?? '' }}
|
|
164
177
|
/>
|
|
165
178
|
</div>
|
|
@@ -205,14 +218,8 @@ export default function Question({
|
|
|
205
218
|
return (
|
|
206
219
|
<label
|
|
207
220
|
key={option.id}
|
|
208
|
-
className="flex cursor-pointer items-center gap-4 transition-all"
|
|
209
|
-
style={
|
|
210
|
-
borderRadius: 'var(--cfm-mcq-border-radius, 8px)',
|
|
211
|
-
border: `2px solid ${isSelected ? 'var(--cfm-mcq-selected-accent, var(--cfm-primary))' : 'var(--cfm-card-border-color, #e5e7eb)'}`,
|
|
212
|
-
background: isSelected ? 'var(--cfm-mcq-selected-bg, var(--cfm-secondary))' : 'var(--cfm-card-bg, #fff)',
|
|
213
|
-
padding: 'var(--cfm-mcq-card-padding, 12px)',
|
|
214
|
-
marginBottom: 'var(--cfm-mcq-option-gap, 8px)',
|
|
215
|
-
}}
|
|
221
|
+
className="cfm-mcq-option flex cursor-pointer items-center gap-4 transition-all hover:bg-gray-50/50"
|
|
222
|
+
style={mcqOptionCardStyle(isSelected)}
|
|
216
223
|
>
|
|
217
224
|
<input
|
|
218
225
|
type="checkbox"
|
|
@@ -221,6 +228,13 @@ export default function Question({
|
|
|
221
228
|
onChange={() => handleToggle(option.id)}
|
|
222
229
|
className="sr-only"
|
|
223
230
|
/>
|
|
231
|
+
<div style={mcqCheckboxControlStyle(isSelected)}>
|
|
232
|
+
{isSelected && (
|
|
233
|
+
<svg className="h-3 w-3 text-white" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
234
|
+
<path d="M2 6l3 3 5-5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
235
|
+
</svg>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
224
238
|
<span
|
|
225
239
|
className="text-[15px] font-medium text-gray-900 leading-tight"
|
|
226
240
|
dangerouslySetInnerHTML={{ __html: option.optionLabel }}
|
|
@@ -241,14 +255,8 @@ export default function Question({
|
|
|
241
255
|
return (
|
|
242
256
|
<label
|
|
243
257
|
key={option.id}
|
|
244
|
-
className="flex cursor-pointer items-center gap-4 transition-all"
|
|
245
|
-
style={
|
|
246
|
-
borderRadius: 'var(--cfm-mcq-border-radius, 8px)',
|
|
247
|
-
border: `2px solid ${isSelected ? 'var(--cfm-mcq-selected-accent, var(--cfm-primary))' : 'var(--cfm-card-border-color, #e5e7eb)'}`,
|
|
248
|
-
background: isSelected ? 'var(--cfm-mcq-selected-bg, var(--cfm-secondary))' : 'var(--cfm-card-bg, #fff)',
|
|
249
|
-
padding: 'var(--cfm-mcq-card-padding, 12px)',
|
|
250
|
-
marginBottom: 'var(--cfm-mcq-option-gap, 8px)',
|
|
251
|
-
}}
|
|
258
|
+
className="cfm-mcq-option flex cursor-pointer items-center gap-4 transition-all hover:bg-gray-50/50"
|
|
259
|
+
style={mcqOptionCardStyle(isSelected)}
|
|
252
260
|
>
|
|
253
261
|
<input
|
|
254
262
|
type="radio"
|
|
@@ -257,6 +265,9 @@ export default function Question({
|
|
|
257
265
|
onChange={() => onSelect(option.id)}
|
|
258
266
|
className="sr-only"
|
|
259
267
|
/>
|
|
268
|
+
<div style={mcqRadioOuterStyle(isSelected)}>
|
|
269
|
+
{isSelected && <div style={mcqRadioInnerStyle()} />}
|
|
270
|
+
</div>
|
|
260
271
|
<span
|
|
261
272
|
className="text-[15px] font-medium text-gray-900 leading-tight"
|
|
262
273
|
dangerouslySetInnerHTML={{ __html: option.optionLabel }}
|
|
@@ -271,31 +282,48 @@ export default function Question({
|
|
|
271
282
|
{question.type === QUESTION_TYPE.RANK_ORDER && rankOrderQuestion && (
|
|
272
283
|
<RankOrderScale
|
|
273
284
|
question={rankOrderQuestion}
|
|
274
|
-
selectedValue={
|
|
275
|
-
typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue)
|
|
276
|
-
? selectedValue
|
|
277
|
-
: undefined
|
|
278
|
-
}
|
|
285
|
+
selectedValue={normalizeRankOrderAnswers(selectedValue)}
|
|
279
286
|
onSelect={onSelect}
|
|
280
287
|
/>
|
|
281
288
|
)}
|
|
282
289
|
|
|
283
290
|
{question.type === QUESTION_TYPE.TEXTFIELD && (
|
|
284
|
-
<
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
291
|
+
<div className="space-y-1.5">
|
|
292
|
+
<textarea
|
|
293
|
+
data-cfm-textfield
|
|
294
|
+
rows={question.inputVariant === 'long' ? 4 : 1}
|
|
295
|
+
value={(selectedValue as string) ?? ''}
|
|
296
|
+
onChange={e => onSelect(e.target.value)}
|
|
297
|
+
placeholder={question.placeholder || 'Type your response here...'}
|
|
298
|
+
className="w-full text-sm outline-none transition-colors"
|
|
299
|
+
style={{
|
|
300
|
+
height: question.inputVariant === 'long' ? undefined : 'var(--cfm-input-height, 44px)',
|
|
301
|
+
borderRadius: 'var(--cfm-input-radius, 4px)',
|
|
302
|
+
border: '1px solid var(--cfm-input-border, #d1d5db)',
|
|
303
|
+
padding: 'var(--cfm-input-padding, 16px)',
|
|
304
|
+
color: 'var(--cfm-text)',
|
|
305
|
+
}}
|
|
306
|
+
onFocus={e => {
|
|
307
|
+
e.currentTarget.style.borderColor = focusRingVar;
|
|
308
|
+
e.currentTarget.style.boxShadow = `0 0 0 1px ${focusRingVar}`;
|
|
309
|
+
}}
|
|
310
|
+
onBlur={e => {
|
|
311
|
+
e.currentTarget.style.borderColor = 'var(--cfm-input-border, #d1d5db)';
|
|
312
|
+
e.currentTarget.style.boxShadow = 'none';
|
|
313
|
+
}}
|
|
314
|
+
/>
|
|
315
|
+
<div
|
|
316
|
+
className="text-right text-xs pr-1"
|
|
317
|
+
style={{ color: 'var(--cfm-input-placeholder, #6b7280)' }}
|
|
318
|
+
>
|
|
319
|
+
{Math.max(
|
|
320
|
+
0,
|
|
321
|
+
(question.maxCharacterCount ?? 1000) -
|
|
322
|
+
(typeof selectedValue === 'string' ? selectedValue.length : 0)
|
|
323
|
+
)}{' '}
|
|
324
|
+
characters remaining
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
299
327
|
)}
|
|
300
328
|
|
|
301
329
|
{question.type === QUESTION_TYPE.CFM_MATRIX && (
|
|
@@ -336,21 +364,64 @@ export default function Question({
|
|
|
336
364
|
)}
|
|
337
365
|
|
|
338
366
|
{question.type === QUESTION_TYPE.TEXT_AND_MEDIA && question.media?.url && (
|
|
339
|
-
<div
|
|
340
|
-
|
|
367
|
+
<div
|
|
368
|
+
className={`mt-4 flex w-full ${
|
|
369
|
+
question.media.alignment === 'TOP_CENTER' || question.media.alignment === 'BOTTOM_CENTER'
|
|
370
|
+
? 'justify-center'
|
|
371
|
+
: question.media.alignment === 'TOP_RIGHT' || question.media.alignment === 'BOTTOM_RIGHT'
|
|
372
|
+
? 'justify-end'
|
|
373
|
+
: 'justify-start'
|
|
374
|
+
}`}
|
|
375
|
+
>
|
|
376
|
+
<div
|
|
377
|
+
style={{
|
|
378
|
+
width: question.media.size
|
|
379
|
+
? `${question.media.size}%`
|
|
380
|
+
: 'var(--cfm-media-max-width, 100%)',
|
|
381
|
+
maxWidth: 'var(--cfm-media-max-width, 100%)',
|
|
382
|
+
}}
|
|
383
|
+
className="flex flex-col gap-2"
|
|
384
|
+
>
|
|
341
385
|
{question.media.mimeType?.startsWith('video/') || question.media.mimeType === 'VIDEO' ? (
|
|
342
|
-
<video
|
|
386
|
+
<video
|
|
387
|
+
controls
|
|
388
|
+
src={question.media.url}
|
|
389
|
+
className="max-w-full shadow-sm"
|
|
390
|
+
style={{ borderRadius: 'var(--cfm-media-radius, 8px)' }}
|
|
391
|
+
/>
|
|
343
392
|
) : (
|
|
344
|
-
<img
|
|
393
|
+
<img
|
|
394
|
+
src={question.media.url}
|
|
395
|
+
alt={question.media.title || ''}
|
|
396
|
+
className="max-w-full shadow-sm"
|
|
397
|
+
style={{ borderRadius: 'var(--cfm-media-radius, 8px)' }}
|
|
398
|
+
/>
|
|
399
|
+
)}
|
|
400
|
+
{question.media.title && (
|
|
401
|
+
<p
|
|
402
|
+
className="text-center"
|
|
403
|
+
style={{
|
|
404
|
+
color: 'var(--cfm-media-caption-color, #6b7280)',
|
|
405
|
+
fontSize: 'var(--cfm-media-caption-size, 14px)',
|
|
406
|
+
}}
|
|
407
|
+
>
|
|
408
|
+
{question.media.title}
|
|
409
|
+
</p>
|
|
345
410
|
)}
|
|
346
|
-
{question.media.title && <p className="text-sm text-gray-500 text-center mt-2">{question.media.title}</p>}
|
|
347
411
|
</div>
|
|
348
412
|
</div>
|
|
349
413
|
)}
|
|
350
414
|
|
|
351
415
|
{validationError && (
|
|
352
|
-
<div
|
|
353
|
-
|
|
416
|
+
<div
|
|
417
|
+
className="mt-4 rounded py-3.5 px-4 text-left"
|
|
418
|
+
style={{
|
|
419
|
+
border: '1px solid var(--cfm-validation-border, #333333)',
|
|
420
|
+
backgroundColor: 'var(--cfm-validation-bg, #d9d9d9)',
|
|
421
|
+
borderRadius: 'var(--cfm-input-radius, 4px)',
|
|
422
|
+
}}
|
|
423
|
+
>
|
|
424
|
+
<p className="text-[13px] sm:text-sm font-bold text-gray-900 leading-tight">{validationError}</p>
|
|
354
425
|
</div>
|
|
355
426
|
)}
|
|
356
427
|
</section>
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
getOrderedOptionIdsFromRanks,
|
|
38
38
|
normalizeRankOrderAnswers,
|
|
39
39
|
} from '@explorer02/cfm-survey-sdk';
|
|
40
|
+
import { focusRingVar } from '@/lib/surveyUi/selectionStyles';
|
|
40
41
|
|
|
41
42
|
type RankOrderScaleProps = {
|
|
42
43
|
question: RankOrderQuestion;
|
|
@@ -120,15 +121,29 @@ function RankOrderOptionRow({
|
|
|
120
121
|
}: RankOrderOptionRowProps) {
|
|
121
122
|
return (
|
|
122
123
|
<div
|
|
123
|
-
className="flex items-center gap-3 rounded-lg border border-[#e5e5e5]
|
|
124
|
-
style={{
|
|
124
|
+
className="flex items-center gap-3 rounded-lg border border-[#e5e5e5] transition-colors"
|
|
125
|
+
style={{
|
|
126
|
+
backgroundColor: 'var(--cfm-rank-item-bg, #fff)',
|
|
127
|
+
padding: 'var(--cfm-rank-item-padding, 12px 16px)',
|
|
128
|
+
marginBottom: 'var(--cfm-rank-item-gap, 12px)',
|
|
129
|
+
}}
|
|
130
|
+
onMouseEnter={(e) => {
|
|
131
|
+
e.currentTarget.style.backgroundColor =
|
|
132
|
+
'var(--cfm-rank-dropdown-hover-bg, var(--cfm-rank-item-bg, #f9fafb))';
|
|
133
|
+
}}
|
|
134
|
+
onMouseLeave={(e) => {
|
|
135
|
+
e.currentTarget.style.backgroundColor = 'var(--cfm-rank-item-bg, #fff)';
|
|
136
|
+
}}
|
|
125
137
|
>
|
|
126
138
|
{rankSelect}
|
|
127
139
|
{dragHandle}
|
|
128
140
|
{rankBadge !== undefined && (
|
|
129
141
|
<span
|
|
130
|
-
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-sm font-semibold
|
|
131
|
-
style={{
|
|
142
|
+
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-sm font-semibold"
|
|
143
|
+
style={{
|
|
144
|
+
backgroundColor: 'var(--cfm-rank-badge-bg, var(--cfm-mcq-selected-bg, #fdf2f8))',
|
|
145
|
+
color: 'var(--cfm-rank-badge-text, var(--cfm-mcq-selected-accent, var(--cfm-primary, #e20074)))',
|
|
146
|
+
}}
|
|
132
147
|
>
|
|
133
148
|
{rankBadge}
|
|
134
149
|
</span>
|
|
@@ -169,6 +184,14 @@ function DropdownRankLayout({
|
|
|
169
184
|
onChange={event => handleRankChange(option.id, event.target.value)}
|
|
170
185
|
className="h-10 w-16 shrink-0 rounded border bg-white px-2 text-sm outline-none"
|
|
171
186
|
style={{ borderColor: 'var(--cfm-rank-select-border, #d1d5db)' }}
|
|
187
|
+
onFocus={e => {
|
|
188
|
+
e.currentTarget.style.borderColor = focusRingVar;
|
|
189
|
+
e.currentTarget.style.boxShadow = `0 0 0 1px ${focusRingVar}`;
|
|
190
|
+
}}
|
|
191
|
+
onBlur={e => {
|
|
192
|
+
e.currentTarget.style.borderColor = 'var(--cfm-rank-select-border, #d1d5db)';
|
|
193
|
+
e.currentTarget.style.boxShadow = 'none';
|
|
194
|
+
}}
|
|
172
195
|
>
|
|
173
196
|
<option value="">-</option>
|
|
174
197
|
{rankChoices.map(rank => (
|
|
@@ -225,7 +248,8 @@ function SortableRankItem({
|
|
|
225
248
|
<button
|
|
226
249
|
type="button"
|
|
227
250
|
aria-label={`Drag to reorder ${option.optionLabel || option.id}`}
|
|
228
|
-
className="flex h-10 w-8 shrink-0 cursor-grab items-center justify-center rounded
|
|
251
|
+
className="flex h-10 w-8 shrink-0 cursor-grab items-center justify-center rounded active:cursor-grabbing"
|
|
252
|
+
style={{ color: 'var(--cfm-rank-handle, #9ca3af)' }}
|
|
229
253
|
{...attributes}
|
|
230
254
|
{...listeners}
|
|
231
255
|
>
|
|
@@ -20,7 +20,7 @@ type RatingScaleProps = {
|
|
|
20
20
|
selectedValue?: AnswerValue;
|
|
21
21
|
onSelect: (value: AnswerValue) => void;
|
|
22
22
|
variant?: 'nps' | 'star';
|
|
23
|
-
buttonStyle?: 'standard' | 'numbered' | 'emoji';
|
|
23
|
+
buttonStyle?: 'standard' | 'numbered' | 'emoji' | 'pill';
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
function resolveBadgeColor(index: number, value: number): string {
|
|
@@ -68,6 +68,7 @@ export default function RatingScale({
|
|
|
68
68
|
gap: 'var(--cfm-nps-cell-gap, 2px)',
|
|
69
69
|
};
|
|
70
70
|
const showNumberBadges = buttonStyle !== 'standard';
|
|
71
|
+
const usePillCells = buttonStyle === 'pill';
|
|
71
72
|
|
|
72
73
|
if (buttonStyle === 'emoji') {
|
|
73
74
|
return (
|
|
@@ -92,7 +93,7 @@ export default function RatingScale({
|
|
|
92
93
|
style={{
|
|
93
94
|
...scaleRadioRingStyle(isSelected, {
|
|
94
95
|
pill: false,
|
|
95
|
-
size: 'var(--cfm-csat-emoji-size,
|
|
96
|
+
size: 'var(--cfm-csat-emoji-size, 40px)',
|
|
96
97
|
}),
|
|
97
98
|
fontSize: 'var(--cfm-csat-emoji-size, 24px)',
|
|
98
99
|
}}
|
|
@@ -111,7 +112,7 @@ export default function RatingScale({
|
|
|
111
112
|
<div role="radiogroup" className="w-full overflow-x-auto" data-cfm-nps-area>
|
|
112
113
|
{showNumberBadges && (
|
|
113
114
|
<div className="mb-3 min-w-0" style={gridStyle}>
|
|
114
|
-
{resolvedOptions.map((option) => {
|
|
115
|
+
{resolvedOptions.map((option, index) => {
|
|
115
116
|
const isSelected = isScaleAnswerSelected(selectedValue, option.value);
|
|
116
117
|
return (
|
|
117
118
|
<div key={`${questionId}-badge-${option.value}`} className="flex justify-center">
|
|
@@ -124,6 +125,7 @@ export default function RatingScale({
|
|
|
124
125
|
? scaleCellSelectedStyle(true)
|
|
125
126
|
: { backgroundColor: option.color }),
|
|
126
127
|
}}
|
|
128
|
+
data-cfm-number-badge={index}
|
|
127
129
|
title={option.label}
|
|
128
130
|
>
|
|
129
131
|
{option.label}
|
|
@@ -158,9 +160,10 @@ export default function RatingScale({
|
|
|
158
160
|
className="flex items-center justify-center rounded-full border-2 transition-all"
|
|
159
161
|
style={scaleRadioRingStyle(isSelected, {
|
|
160
162
|
size: 'var(--cfm-nps-cell-size, 40px)',
|
|
163
|
+
pill: usePillCells,
|
|
161
164
|
})}
|
|
162
165
|
>
|
|
163
|
-
<div style={scaleRadioDotStyle(isSelected)} />
|
|
166
|
+
<div style={scaleRadioDotStyle(isSelected, { pill: usePillCells })} />
|
|
164
167
|
</div>
|
|
165
168
|
</label>
|
|
166
169
|
);
|
|
@@ -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,26 +71,44 @@ 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
|
+
}}
|
|
88
|
+
>
|
|
78
89
|
{labels.map((lbl, idx) => (
|
|
79
90
|
<div key={idx} style={{ width: 0, display: 'flex', justifyContent: 'center' }}>
|
|
80
|
-
<span
|
|
91
|
+
<span
|
|
92
|
+
style={{ ...hintLabelStyle(), padding: '2px 8px', borderRadius: '4px', whiteSpace: 'nowrap' }}
|
|
93
|
+
>
|
|
94
|
+
{lbl}
|
|
95
|
+
</span>
|
|
81
96
|
</div>
|
|
82
97
|
))}
|
|
83
98
|
</div>
|
|
84
99
|
)}
|
|
85
100
|
|
|
86
|
-
<div
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
101
|
+
<div
|
|
102
|
+
style={{
|
|
103
|
+
position: 'relative',
|
|
104
|
+
margin: '0 16px',
|
|
105
|
+
marginBottom: '8px',
|
|
106
|
+
fontSize: '14px',
|
|
107
|
+
fontWeight: 400,
|
|
108
|
+
height: '28px',
|
|
109
|
+
...sliderTickLabelStyle(),
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
90
112
|
{tickValues.length > 0 ? (
|
|
91
113
|
tickValues.map((tv, idx) => {
|
|
92
114
|
const percentage = max !== min ? ((tv.value - min) / (max - min)) * 100 : 0;
|
|
@@ -101,11 +123,20 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
|
|
|
101
123
|
{tv.label}
|
|
102
124
|
</div>
|
|
103
125
|
) : (
|
|
104
|
-
<div
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
126
|
+
<div
|
|
127
|
+
style={{
|
|
128
|
+
backgroundColor: tv.color,
|
|
129
|
+
color: 'white',
|
|
130
|
+
padding: '4px 8px',
|
|
131
|
+
borderRadius: '4px',
|
|
132
|
+
fontWeight: 600,
|
|
133
|
+
wordBreak: 'break-word',
|
|
134
|
+
textAlign: 'center',
|
|
135
|
+
lineHeight: '1.2',
|
|
136
|
+
boxShadow: '0 1px 2px rgba(0,0,0,0.1)',
|
|
137
|
+
minWidth: 'var(--cfm-slider-tick-size, 28px)',
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
109
140
|
{tv.label}
|
|
110
141
|
</div>
|
|
111
142
|
)}
|
|
@@ -151,9 +182,10 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
|
|
|
151
182
|
return (
|
|
152
183
|
<div key={row.id} style={{
|
|
153
184
|
display: 'flex', width: '100%', alignItems: 'center',
|
|
154
|
-
backgroundColor:
|
|
185
|
+
backgroundColor: SLIDER_ROW_BAND, borderRadius: '12px', padding: '16px', boxSizing: 'border-box',
|
|
186
|
+
borderBottom: '1px solid var(--cfm-card-border-color, #e5e7eb)',
|
|
155
187
|
}}>
|
|
156
|
-
<div style={{ flex:
|
|
188
|
+
<div style={{ flex: `0 0 ${MATRIX_ROW_LABEL_WIDTH}`, paddingRight: '16px', fontSize: '15px', color: '#111827', wordWrap: 'break-word', display: 'flex', alignItems: 'center' }}>
|
|
157
189
|
{row.statementText !== 'Question' && row.statementText !== '' ? (
|
|
158
190
|
<span dangerouslySetInnerHTML={{ __html: row.statementText }} />
|
|
159
191
|
) : null}
|