@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
|
@@ -3,8 +3,10 @@ import type { CfmMatrixQuestion, MatrixRowAnswers } from '@explorer02/cfm-survey
|
|
|
3
3
|
import { columnSubmitValue } from './surveyUiScaleUtils';
|
|
4
4
|
import MatrixDropdown from './MatrixDropdown';
|
|
5
5
|
import { matrixColumnLabelStyle } from '@/lib/surveyUi/labelStyles';
|
|
6
|
+
import { focusRingVar, matrixCarouselDotVar, matrixRadioDotStyle, matrixRadioRingStyle } from '@/lib/surveyUi/selectionStyles';
|
|
6
7
|
|
|
7
8
|
const MATRIX_ROW_LABEL_WIDTH = 'var(--cfm-matrix-row-width, 180px)';
|
|
9
|
+
const BIPOLAR_COL_WIDTH = 'var(--cfm-matrix-bipolar-width, 25%)';
|
|
8
10
|
|
|
9
11
|
type LikertMatrixScaleProps = {
|
|
10
12
|
question: CfmMatrixQuestion;
|
|
@@ -114,8 +116,8 @@ function LikertMatrixCarouselLayout({
|
|
|
114
116
|
<div style={{
|
|
115
117
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
116
118
|
width: '18px', height: '18px', borderRadius: '4px',
|
|
117
|
-
border: selected ?
|
|
118
|
-
backgroundColor: selected ?
|
|
119
|
+
border: selected ? `2px solid ${focusRingVar}` : '2px solid #d1d5db',
|
|
120
|
+
backgroundColor: selected ? focusRingVar : '#fff',
|
|
119
121
|
flexShrink: 0, transition: 'all 0.15s'
|
|
120
122
|
}}>
|
|
121
123
|
{selected && (
|
|
@@ -125,13 +127,8 @@ function LikertMatrixCarouselLayout({
|
|
|
125
127
|
)}
|
|
126
128
|
</div>
|
|
127
129
|
) : (
|
|
128
|
-
<div style={
|
|
129
|
-
|
|
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' }} />}
|
|
130
|
+
<div style={matrixRadioRingStyle(selected, '18px')}>
|
|
131
|
+
<div style={matrixRadioDotStyle(selected, '8px')} />
|
|
135
132
|
</div>
|
|
136
133
|
)}
|
|
137
134
|
<span style={{ fontSize: '15px', fontWeight: 400, color: '#374151', userSelect: 'none' }} dangerouslySetInnerHTML={{ __html: col.label }} />
|
|
@@ -154,7 +151,7 @@ function LikertMatrixCarouselLayout({
|
|
|
154
151
|
{statementRows.map((_, i) => (
|
|
155
152
|
<div key={i} style={{
|
|
156
153
|
width: '10px', height: '10px', borderRadius: '50%',
|
|
157
|
-
backgroundColor: i === activeCarouselIndex ?
|
|
154
|
+
backgroundColor: i === activeCarouselIndex ? matrixCarouselDotVar : '#e5e7eb',
|
|
158
155
|
cursor: 'pointer', transition: 'background-color 0.2s'
|
|
159
156
|
}} onClick={() => setActiveCarouselIndex(i)} />
|
|
160
157
|
))}
|
|
@@ -209,7 +206,7 @@ function LikertMatrixGridLayout({
|
|
|
209
206
|
<div style={{ display: 'flex', flexDirection: 'column', marginBottom: '16px' }}>
|
|
210
207
|
{hasLabels && (
|
|
211
208
|
<div style={{ display: 'flex', alignItems: 'flex-end', minHeight: '24px', marginBottom: '8px' }}>
|
|
212
|
-
<div style={{ flex:
|
|
209
|
+
<div style={{ flex: `0 0 ${BIPOLAR_COL_WIDTH}`, paddingRight: '16px', boxSizing: 'border-box' }} />
|
|
213
210
|
<div style={{ flex: 1, position: 'relative', height: '20px' }}>
|
|
214
211
|
{labels.map((lbl, idx) => {
|
|
215
212
|
let leftPos = '50%';
|
|
@@ -227,12 +224,12 @@ function LikertMatrixGridLayout({
|
|
|
227
224
|
);
|
|
228
225
|
})}
|
|
229
226
|
</div>
|
|
230
|
-
<div style={{ flex:
|
|
227
|
+
<div style={{ flex: `0 0 ${BIPOLAR_COL_WIDTH}`, paddingLeft: '16px', boxSizing: 'border-box' }} />
|
|
231
228
|
</div>
|
|
232
229
|
)}
|
|
233
230
|
{showColumnHeaders && (
|
|
234
231
|
<div style={{ display: 'flex', alignItems: 'flex-end', minHeight: '24px' }}>
|
|
235
|
-
<div style={{ flex: isBipolar ?
|
|
232
|
+
<div style={{ flex: isBipolar ? `0 0 ${BIPOLAR_COL_WIDTH}` : `0 0 ${MATRIX_ROW_LABEL_WIDTH}`, paddingRight: '16px', boxSizing: 'border-box' }} />
|
|
236
233
|
<div style={{ flex: 1, display: 'flex' }}>
|
|
237
234
|
<div style={{ flex: 1, display: 'grid', gridTemplateColumns: `repeat(${gridCols.length}, 1fr)` }}>
|
|
238
235
|
{gridCols.map((col) => (
|
|
@@ -243,7 +240,7 @@ function LikertMatrixGridLayout({
|
|
|
243
240
|
))}
|
|
244
241
|
</div>
|
|
245
242
|
</div>
|
|
246
|
-
{isBipolar && <div style={{ flex:
|
|
243
|
+
{isBipolar && <div style={{ flex: `0 0 ${BIPOLAR_COL_WIDTH}`, paddingLeft: '16px', boxSizing: 'border-box' }} />}
|
|
247
244
|
</div>
|
|
248
245
|
)}
|
|
249
246
|
</div>
|
|
@@ -261,10 +258,10 @@ function LikertMatrixGridLayout({
|
|
|
261
258
|
{repeatColumnHeaders && renderHeader()}
|
|
262
259
|
<div style={{
|
|
263
260
|
display: 'flex', alignItems: 'center', borderRadius: '8px',
|
|
264
|
-
padding: '12px 0', backgroundColor: !repeatColumnHeaders && rowIdx % 2 === 0 ? 'rgba(249, 250, 251, 0.8)' : '#fff',
|
|
261
|
+
padding: 'var(--cfm-matrix-cell-padding, 12px) 0', backgroundColor: !repeatColumnHeaders && rowIdx % 2 === 0 ? 'var(--cfm-zebra-row, rgba(249, 250, 251, 0.8))' : '#fff',
|
|
265
262
|
boxSizing: 'border-box'
|
|
266
263
|
}}>
|
|
267
|
-
<div style={{ flex: isBipolar ?
|
|
264
|
+
<div style={{ flex: isBipolar ? `0 0 ${BIPOLAR_COL_WIDTH}` : `0 0 ${MATRIX_ROW_LABEL_WIDTH}`, padding: '0 16px', boxSizing: 'border-box' }}>
|
|
268
265
|
<span style={{ fontSize: '14px', fontWeight: 500, color: '#111827', lineHeight: 1.4, wordWrap: 'break-word', display: 'block' }}
|
|
269
266
|
dangerouslySetInnerHTML={{ __html: transposeTable ? ('label' in rowItem ? rowItem.label : '') : ('statementText' in rowItem ? rowItem.statementText : '') }} />
|
|
270
267
|
</div>
|
|
@@ -282,31 +279,44 @@ function LikertMatrixGridLayout({
|
|
|
282
279
|
|
|
283
280
|
return (
|
|
284
281
|
<div key={colItem.id} style={{ display: 'flex', justifyContent: 'center' }}>
|
|
285
|
-
<label style={{
|
|
286
|
-
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
|
282
|
+
<label style={{
|
|
283
|
+
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
|
287
284
|
opacity: isDisabled ? 0.4 : 1,
|
|
288
|
-
padding: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center'
|
|
285
|
+
padding: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
289
286
|
}}>
|
|
290
|
-
<input
|
|
291
|
-
type=
|
|
287
|
+
<input
|
|
288
|
+
type="checkbox"
|
|
289
|
+
className="sr-only"
|
|
292
290
|
checked={selected}
|
|
293
291
|
disabled={isDisabled}
|
|
294
|
-
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
onChange={(e) => {
|
|
300
|
-
if (isMultiple || !selected) {
|
|
292
|
+
onChange={() => {
|
|
293
|
+
if (isDisabled) return;
|
|
294
|
+
if (isMultiple) {
|
|
295
|
+
onCellSelect(actualRowId, selected ? undefined : actualColValue);
|
|
296
|
+
} else if (!selected) {
|
|
301
297
|
onCellSelect(actualRowId, actualColValue);
|
|
298
|
+
} else {
|
|
299
|
+
onCellSelect(actualRowId, undefined);
|
|
302
300
|
}
|
|
303
301
|
}}
|
|
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
302
|
/>
|
|
303
|
+
{isMultiple ? (
|
|
304
|
+
<div style={{
|
|
305
|
+
display: 'flex', width: '18px', height: '18px', alignItems: 'center', justifyContent: 'center',
|
|
306
|
+
borderRadius: '4px', border: selected ? `2px solid ${focusRingVar}` : '2px solid #d1d5db',
|
|
307
|
+
backgroundColor: selected ? focusRingVar : '#fff',
|
|
308
|
+
}}>
|
|
309
|
+
{selected && (
|
|
310
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="3">
|
|
311
|
+
<polyline points="20 6 9 17 4 12" />
|
|
312
|
+
</svg>
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
) : (
|
|
316
|
+
<div style={matrixRadioRingStyle(selected, '18px')}>
|
|
317
|
+
<div style={matrixRadioDotStyle(selected, '8px')} />
|
|
318
|
+
</div>
|
|
319
|
+
)}
|
|
310
320
|
</label>
|
|
311
321
|
</div>
|
|
312
322
|
);
|
|
@@ -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
|
>
|