@explorer02/cfm-survey-sdk 0.3.1 → 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/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/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 +5 -3
- package/templates/docs/templates/SliderMatrixScale.tsx +44 -16
- package/templates/docs/templates/selectionStyles.ts +88 -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 +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/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
|
@@ -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;
|
|
@@ -205,14 +212,8 @@ export default function Question({
|
|
|
205
212
|
return (
|
|
206
213
|
<label
|
|
207
214
|
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
|
-
}}
|
|
215
|
+
className="cfm-mcq-option flex cursor-pointer items-center gap-4 transition-all hover:bg-gray-50/50"
|
|
216
|
+
style={mcqOptionCardStyle(isSelected)}
|
|
216
217
|
>
|
|
217
218
|
<input
|
|
218
219
|
type="checkbox"
|
|
@@ -221,6 +222,13 @@ export default function Question({
|
|
|
221
222
|
onChange={() => handleToggle(option.id)}
|
|
222
223
|
className="sr-only"
|
|
223
224
|
/>
|
|
225
|
+
<div style={mcqCheckboxControlStyle(isSelected)}>
|
|
226
|
+
{isSelected && (
|
|
227
|
+
<svg className="h-3 w-3 text-white" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
228
|
+
<path d="M2 6l3 3 5-5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
229
|
+
</svg>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
224
232
|
<span
|
|
225
233
|
className="text-[15px] font-medium text-gray-900 leading-tight"
|
|
226
234
|
dangerouslySetInnerHTML={{ __html: option.optionLabel }}
|
|
@@ -241,14 +249,8 @@ export default function Question({
|
|
|
241
249
|
return (
|
|
242
250
|
<label
|
|
243
251
|
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
|
-
}}
|
|
252
|
+
className="cfm-mcq-option flex cursor-pointer items-center gap-4 transition-all hover:bg-gray-50/50"
|
|
253
|
+
style={mcqOptionCardStyle(isSelected)}
|
|
252
254
|
>
|
|
253
255
|
<input
|
|
254
256
|
type="radio"
|
|
@@ -257,6 +259,9 @@ export default function Question({
|
|
|
257
259
|
onChange={() => onSelect(option.id)}
|
|
258
260
|
className="sr-only"
|
|
259
261
|
/>
|
|
262
|
+
<div style={mcqRadioOuterStyle(isSelected)}>
|
|
263
|
+
{isSelected && <div style={mcqRadioInnerStyle()} />}
|
|
264
|
+
</div>
|
|
260
265
|
<span
|
|
261
266
|
className="text-[15px] font-medium text-gray-900 leading-tight"
|
|
262
267
|
dangerouslySetInnerHTML={{ __html: option.optionLabel }}
|
|
@@ -271,31 +276,48 @@ export default function Question({
|
|
|
271
276
|
{question.type === QUESTION_TYPE.RANK_ORDER && rankOrderQuestion && (
|
|
272
277
|
<RankOrderScale
|
|
273
278
|
question={rankOrderQuestion}
|
|
274
|
-
selectedValue={
|
|
275
|
-
typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue)
|
|
276
|
-
? selectedValue
|
|
277
|
-
: undefined
|
|
278
|
-
}
|
|
279
|
+
selectedValue={normalizeRankOrderAnswers(selectedValue)}
|
|
279
280
|
onSelect={onSelect}
|
|
280
281
|
/>
|
|
281
282
|
)}
|
|
282
283
|
|
|
283
284
|
{question.type === QUESTION_TYPE.TEXTFIELD && (
|
|
284
|
-
<
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
285
|
+
<div className="space-y-1.5">
|
|
286
|
+
<textarea
|
|
287
|
+
data-cfm-textfield
|
|
288
|
+
rows={question.inputVariant === 'long' ? 4 : 1}
|
|
289
|
+
value={(selectedValue as string) ?? ''}
|
|
290
|
+
onChange={e => onSelect(e.target.value)}
|
|
291
|
+
placeholder={question.placeholder || 'Type your response here...'}
|
|
292
|
+
className="w-full text-sm outline-none transition-colors"
|
|
293
|
+
style={{
|
|
294
|
+
height: question.inputVariant === 'long' ? undefined : 'var(--cfm-input-height, 44px)',
|
|
295
|
+
borderRadius: 'var(--cfm-input-radius, 4px)',
|
|
296
|
+
border: '1px solid var(--cfm-input-border, #d1d5db)',
|
|
297
|
+
padding: 'var(--cfm-input-padding, 16px)',
|
|
298
|
+
color: 'var(--cfm-text)',
|
|
299
|
+
}}
|
|
300
|
+
onFocus={e => {
|
|
301
|
+
e.currentTarget.style.borderColor = focusRingVar;
|
|
302
|
+
e.currentTarget.style.boxShadow = `0 0 0 1px ${focusRingVar}`;
|
|
303
|
+
}}
|
|
304
|
+
onBlur={e => {
|
|
305
|
+
e.currentTarget.style.borderColor = 'var(--cfm-input-border, #d1d5db)';
|
|
306
|
+
e.currentTarget.style.boxShadow = 'none';
|
|
307
|
+
}}
|
|
308
|
+
/>
|
|
309
|
+
<div
|
|
310
|
+
className="text-right text-xs pr-1"
|
|
311
|
+
style={{ color: 'var(--cfm-input-placeholder, #6b7280)' }}
|
|
312
|
+
>
|
|
313
|
+
{Math.max(
|
|
314
|
+
0,
|
|
315
|
+
(question.maxCharacterCount ?? 1000) -
|
|
316
|
+
(typeof selectedValue === 'string' ? selectedValue.length : 0)
|
|
317
|
+
)}{' '}
|
|
318
|
+
characters remaining
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
299
321
|
)}
|
|
300
322
|
|
|
301
323
|
{question.type === QUESTION_TYPE.CFM_MATRIX && (
|
|
@@ -336,21 +358,64 @@ export default function Question({
|
|
|
336
358
|
)}
|
|
337
359
|
|
|
338
360
|
{question.type === QUESTION_TYPE.TEXT_AND_MEDIA && question.media?.url && (
|
|
339
|
-
<div
|
|
340
|
-
|
|
361
|
+
<div
|
|
362
|
+
className={`mt-4 flex w-full ${
|
|
363
|
+
question.media.alignment === 'TOP_CENTER' || question.media.alignment === 'BOTTOM_CENTER'
|
|
364
|
+
? 'justify-center'
|
|
365
|
+
: question.media.alignment === 'TOP_RIGHT' || question.media.alignment === 'BOTTOM_RIGHT'
|
|
366
|
+
? 'justify-end'
|
|
367
|
+
: 'justify-start'
|
|
368
|
+
}`}
|
|
369
|
+
>
|
|
370
|
+
<div
|
|
371
|
+
style={{
|
|
372
|
+
width: question.media.size
|
|
373
|
+
? `${question.media.size}%`
|
|
374
|
+
: 'var(--cfm-media-max-width, 100%)',
|
|
375
|
+
maxWidth: 'var(--cfm-media-max-width, 100%)',
|
|
376
|
+
}}
|
|
377
|
+
className="flex flex-col gap-2"
|
|
378
|
+
>
|
|
341
379
|
{question.media.mimeType?.startsWith('video/') || question.media.mimeType === 'VIDEO' ? (
|
|
342
|
-
<video
|
|
380
|
+
<video
|
|
381
|
+
controls
|
|
382
|
+
src={question.media.url}
|
|
383
|
+
className="max-w-full shadow-sm"
|
|
384
|
+
style={{ borderRadius: 'var(--cfm-media-radius, 8px)' }}
|
|
385
|
+
/>
|
|
343
386
|
) : (
|
|
344
|
-
<img
|
|
387
|
+
<img
|
|
388
|
+
src={question.media.url}
|
|
389
|
+
alt={question.media.title || ''}
|
|
390
|
+
className="max-w-full shadow-sm"
|
|
391
|
+
style={{ borderRadius: 'var(--cfm-media-radius, 8px)' }}
|
|
392
|
+
/>
|
|
393
|
+
)}
|
|
394
|
+
{question.media.title && (
|
|
395
|
+
<p
|
|
396
|
+
className="text-center"
|
|
397
|
+
style={{
|
|
398
|
+
color: 'var(--cfm-media-caption-color, #6b7280)',
|
|
399
|
+
fontSize: 'var(--cfm-media-caption-size, 14px)',
|
|
400
|
+
}}
|
|
401
|
+
>
|
|
402
|
+
{question.media.title}
|
|
403
|
+
</p>
|
|
345
404
|
)}
|
|
346
|
-
{question.media.title && <p className="text-sm text-gray-500 text-center mt-2">{question.media.title}</p>}
|
|
347
405
|
</div>
|
|
348
406
|
</div>
|
|
349
407
|
)}
|
|
350
408
|
|
|
351
409
|
{validationError && (
|
|
352
|
-
<div
|
|
353
|
-
|
|
410
|
+
<div
|
|
411
|
+
className="mt-4 rounded py-3.5 px-4 text-left"
|
|
412
|
+
style={{
|
|
413
|
+
border: '1px solid var(--cfm-validation-border, #333333)',
|
|
414
|
+
backgroundColor: 'var(--cfm-validation-bg, #d9d9d9)',
|
|
415
|
+
borderRadius: 'var(--cfm-input-radius, 4px)',
|
|
416
|
+
}}
|
|
417
|
+
>
|
|
418
|
+
<p className="text-[13px] sm:text-sm font-bold text-gray-900 leading-tight">{validationError}</p>
|
|
354
419
|
</div>
|
|
355
420
|
)}
|
|
356
421
|
</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,22 @@ 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 hover:bg-gray-50/50"
|
|
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
|
+
}}
|
|
125
130
|
>
|
|
126
131
|
{rankSelect}
|
|
127
132
|
{dragHandle}
|
|
128
133
|
{rankBadge !== undefined && (
|
|
129
134
|
<span
|
|
130
|
-
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-sm font-semibold
|
|
131
|
-
style={{
|
|
135
|
+
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-sm font-semibold"
|
|
136
|
+
style={{
|
|
137
|
+
backgroundColor: 'var(--cfm-rank-badge-bg, var(--cfm-mcq-selected-bg, #fdf2f8))',
|
|
138
|
+
color: 'var(--cfm-rank-badge-text, var(--cfm-mcq-selected-accent, var(--cfm-primary, #e20074)))',
|
|
139
|
+
}}
|
|
132
140
|
>
|
|
133
141
|
{rankBadge}
|
|
134
142
|
</span>
|
|
@@ -169,6 +177,14 @@ function DropdownRankLayout({
|
|
|
169
177
|
onChange={event => handleRankChange(option.id, event.target.value)}
|
|
170
178
|
className="h-10 w-16 shrink-0 rounded border bg-white px-2 text-sm outline-none"
|
|
171
179
|
style={{ borderColor: 'var(--cfm-rank-select-border, #d1d5db)' }}
|
|
180
|
+
onFocus={e => {
|
|
181
|
+
e.currentTarget.style.borderColor = focusRingVar;
|
|
182
|
+
e.currentTarget.style.boxShadow = `0 0 0 1px ${focusRingVar}`;
|
|
183
|
+
}}
|
|
184
|
+
onBlur={e => {
|
|
185
|
+
e.currentTarget.style.borderColor = 'var(--cfm-rank-select-border, #d1d5db)';
|
|
186
|
+
e.currentTarget.style.boxShadow = 'none';
|
|
187
|
+
}}
|
|
172
188
|
>
|
|
173
189
|
<option value="">-</option>
|
|
174
190
|
{rankChoices.map(rank => (
|
|
@@ -225,7 +241,8 @@ function SortableRankItem({
|
|
|
225
241
|
<button
|
|
226
242
|
type="button"
|
|
227
243
|
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
|
|
244
|
+
className="flex h-10 w-8 shrink-0 cursor-grab items-center justify-center rounded active:cursor-grabbing"
|
|
245
|
+
style={{ color: 'var(--cfm-rank-handle, #9ca3af)' }}
|
|
229
246
|
{...attributes}
|
|
230
247
|
{...listeners}
|
|
231
248
|
>
|
|
@@ -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
|
}}
|
|
@@ -158,9 +159,10 @@ export default function RatingScale({
|
|
|
158
159
|
className="flex items-center justify-center rounded-full border-2 transition-all"
|
|
159
160
|
style={scaleRadioRingStyle(isSelected, {
|
|
160
161
|
size: 'var(--cfm-nps-cell-size, 40px)',
|
|
162
|
+
pill: usePillCells,
|
|
161
163
|
})}
|
|
162
164
|
>
|
|
163
|
-
<div style={scaleRadioDotStyle(isSelected)} />
|
|
165
|
+
<div style={scaleRadioDotStyle(isSelected, { pill: usePillCells })} />
|
|
164
166
|
</div>
|
|
165
167
|
</label>
|
|
166
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,9 +8,96 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { CSSProperties } from 'react';
|
|
10
10
|
|
|
11
|
-
/** Terminal hex fallbacks match apps/client
|
|
11
|
+
/** Terminal hex fallbacks match apps/client — visible before survey-theme.css loads. */
|
|
12
12
|
export const cellSelectedVar = 'var(--cfm-matrix-selected, var(--cfm-primary, #fbe8f3))';
|
|
13
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
|
+
}
|
|
14
101
|
|
|
15
102
|
const unselectedRing = '#d1d5db';
|
|
16
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
|
};
|