@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
package/package.json
CHANGED
|
@@ -7,11 +7,14 @@ import MatrixDropdown from './MatrixDropdown';
|
|
|
7
7
|
import {
|
|
8
8
|
matrixRadioRingStyle,
|
|
9
9
|
matrixRadioDotStyle,
|
|
10
|
+
matrixRowBackgroundStyle,
|
|
10
11
|
scaleCellSelectedStyle,
|
|
11
12
|
unselectedOpacityStyle,
|
|
12
13
|
emojiSizeStyle,
|
|
13
14
|
cellSelectedVar,
|
|
14
15
|
focusRingVar,
|
|
16
|
+
mcqSelectedAccentVar,
|
|
17
|
+
mcqSelectedBgVar,
|
|
15
18
|
} from '@/lib/surveyUi/selectionStyles';
|
|
16
19
|
import { csatColumnLabelStyle } from '@/lib/surveyUi/labelStyles';
|
|
17
20
|
|
|
@@ -106,10 +109,10 @@ function CsatMatrixVerticalList({
|
|
|
106
109
|
onClick={() => onCellSelect(row.id, submitValue)}
|
|
107
110
|
style={{
|
|
108
111
|
width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'flex-start',
|
|
109
|
-
borderRadius: '8px', border: `1px solid ${isSelected ?
|
|
112
|
+
borderRadius: '8px', border: `1px solid ${isSelected ? mcqSelectedAccentVar : '#d1d5db'}`,
|
|
110
113
|
padding: '14px 18px', cursor: 'pointer', transition: 'all 0.15s',
|
|
111
|
-
backgroundColor: isSelected ?
|
|
112
|
-
color: isSelected ?
|
|
114
|
+
backgroundColor: isSelected ? mcqSelectedBgVar : '#fff',
|
|
115
|
+
color: isSelected ? mcqSelectedAccentVar : '#374151',
|
|
113
116
|
fontWeight: isSelected ? 600 : 400, fontSize: '15px', textAlign: 'left',
|
|
114
117
|
}}
|
|
115
118
|
>
|
|
@@ -128,9 +131,9 @@ function CsatMatrixVerticalList({
|
|
|
128
131
|
key={col.id}
|
|
129
132
|
style={{
|
|
130
133
|
display: 'flex', alignItems: 'center', gap: '16px',
|
|
131
|
-
borderRadius: '8px', border: `1px solid ${isSelected ?
|
|
134
|
+
borderRadius: '8px', border: `1px solid ${isSelected ? mcqSelectedAccentVar : '#e5e5e5'}`,
|
|
132
135
|
padding: '16px 20px', cursor: 'pointer', transition: 'all 0.15s',
|
|
133
|
-
backgroundColor: isSelected ?
|
|
136
|
+
backgroundColor: isSelected ? mcqSelectedBgVar : '#fff',
|
|
134
137
|
}}
|
|
135
138
|
>
|
|
136
139
|
<input type="radio" name={`${question.id}-${row.id}`}
|
|
@@ -138,13 +141,8 @@ function CsatMatrixVerticalList({
|
|
|
138
141
|
onChange={() => onCellSelect(row.id, submitValue)}
|
|
139
142
|
style={{ position: 'absolute', width: 0, height: 0, opacity: 0 }}
|
|
140
143
|
/>
|
|
141
|
-
<div style={
|
|
142
|
-
|
|
143
|
-
alignItems: 'center', justifyContent: 'center', borderRadius: '50%',
|
|
144
|
-
border: isSelected ? '2px solid #e20074' : '1.5px solid #9ca3af',
|
|
145
|
-
backgroundColor: '#fff',
|
|
146
|
-
}}>
|
|
147
|
-
{isSelected && <div style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: '#e20074' }} />}
|
|
144
|
+
<div style={matrixRadioRingStyle(isSelected, '20px')}>
|
|
145
|
+
<div style={matrixRadioDotStyle(isSelected, '10px')} />
|
|
148
146
|
</div>
|
|
149
147
|
<span style={{ fontSize: '15px', fontWeight: 500, color: '#111827' }}
|
|
150
148
|
dangerouslySetInnerHTML={{ __html: col.label }} />
|
|
@@ -158,7 +156,7 @@ function CsatMatrixVerticalList({
|
|
|
158
156
|
<label style={{ display: 'flex', alignItems: 'center', gap: '12px', cursor: 'pointer', userSelect: 'none' }}>
|
|
159
157
|
<input type="checkbox" checked={selectedValue[row.id] === null}
|
|
160
158
|
onChange={() => onCellSelect(row.id, selectedValue[row.id] === null ? undefined : null)}
|
|
161
|
-
style={{ width: '16px', height: '16px', accentColor:
|
|
159
|
+
style={{ width: '16px', height: '16px', accentColor: focusRingVar, cursor: 'pointer' }} />
|
|
162
160
|
<span style={{ fontSize: '14px', fontWeight: 500, color: '#4b5563' }}>Not Applicable</span>
|
|
163
161
|
</label>
|
|
164
162
|
)}
|
|
@@ -241,8 +239,8 @@ function CsatMatrixGrid({
|
|
|
241
239
|
</div>
|
|
242
240
|
)}
|
|
243
241
|
|
|
244
|
-
{/* Column Headers for non-graphics */}
|
|
245
|
-
{!isGraphics && !showLabels && m > 0 && (
|
|
242
|
+
{/* Column Headers for non-graphics — star format always shows labels above stars */}
|
|
243
|
+
{!isGraphics && (!showLabels || isStar) && m > 0 && (
|
|
246
244
|
<div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: '16px', minHeight: '24px' }}>
|
|
247
245
|
<div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0 }} />
|
|
248
246
|
<div style={{ flex: 1, display: 'flex' }}>
|
|
@@ -270,7 +268,7 @@ function CsatMatrixGrid({
|
|
|
270
268
|
{statementRows.map((row, rowIdx) => (
|
|
271
269
|
<div key={row.id} style={{
|
|
272
270
|
display: 'flex', alignItems: 'center', borderRadius: '8px',
|
|
273
|
-
padding: '12px 0',
|
|
271
|
+
padding: '12px 0', ...matrixRowBackgroundStyle(rowIdx),
|
|
274
272
|
}}>
|
|
275
273
|
<div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0, padding: '0 16px' }}>
|
|
276
274
|
<span style={{ fontSize: '14px', fontWeight: 500, color: '#111827', lineHeight: 1.4 }}
|
|
@@ -387,7 +385,7 @@ function CsatMatrixGrid({
|
|
|
387
385
|
<label style={{ cursor: 'pointer' }}>
|
|
388
386
|
<input type="checkbox" checked={selectedValue[row.id] === null}
|
|
389
387
|
onChange={() => onCellSelect(row.id, selectedValue[row.id] === null ? undefined : null)}
|
|
390
|
-
style={{ width: '16px', height: '16px', accentColor:
|
|
388
|
+
style={{ width: '16px', height: '16px', accentColor: focusRingVar, cursor: 'pointer' }} />
|
|
391
389
|
</label>
|
|
392
390
|
</div>
|
|
393
391
|
)}
|
|
@@ -490,13 +488,8 @@ function CsatMatrixCarousel({
|
|
|
490
488
|
const EmojiNode = isEmoji ? getEmojiForIndex(displayIdx, scaleColumns.length) : null;
|
|
491
489
|
const StarNode = isStar ? (isSelected ? (CsatStarIcons.filled as any) : (CsatStarIcons.empty as any)) : null;
|
|
492
490
|
const RadioNode = (
|
|
493
|
-
<div style={
|
|
494
|
-
|
|
495
|
-
alignItems: 'center', justifyContent: 'center', borderRadius: '50%',
|
|
496
|
-
margin: '0 auto', border: isSelected ? '2px solid #e20074' : '2px solid #d1d5db',
|
|
497
|
-
backgroundColor: '#fff',
|
|
498
|
-
}}>
|
|
499
|
-
{isSelected && <div style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: '#e20074' }} />}
|
|
491
|
+
<div style={matrixRadioRingStyle(isSelected, '18px')}>
|
|
492
|
+
<div style={matrixRadioDotStyle(isSelected, '8px')} />
|
|
500
493
|
</div>
|
|
501
494
|
);
|
|
502
495
|
|
|
@@ -518,7 +511,9 @@ function CsatMatrixCarousel({
|
|
|
518
511
|
padding: '4px', transform: isHovered ? 'scale(1.1)' : 'scale(1)', opacity: isSelected ? 1 : isHovered ? 1 : 0.5,
|
|
519
512
|
} : isNumbered ? {
|
|
520
513
|
width: '40px', height: '40px', borderRadius: '8px', fontSize: '14px', fontWeight: 500,
|
|
521
|
-
|
|
514
|
+
...(isSelected
|
|
515
|
+
? scaleCellSelectedStyle(true)
|
|
516
|
+
: { border: '1px solid #d1d5db', backgroundColor: '#fff', color: '#111827' }),
|
|
522
517
|
} : { padding: '8px' }),
|
|
523
518
|
}}
|
|
524
519
|
>
|
|
@@ -537,7 +532,7 @@ function CsatMatrixCarousel({
|
|
|
537
532
|
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
|
538
533
|
<input type="checkbox" checked={selectedValue[row.id] === null}
|
|
539
534
|
onChange={() => onCellSelect(row.id, selectedValue[row.id] === null ? undefined : null)}
|
|
540
|
-
style={{ width: '16px', height: '16px', accentColor:
|
|
535
|
+
style={{ width: '16px', height: '16px', accentColor: focusRingVar, cursor: 'pointer' }} />
|
|
541
536
|
<span style={{ fontSize: '14px', fontWeight: 500, color: '#4b5563' }}>N/A</span>
|
|
542
537
|
</label>
|
|
543
538
|
</div>
|
|
@@ -559,7 +554,7 @@ function CsatMatrixCarousel({
|
|
|
559
554
|
<button key={i} type="button" onClick={() => setActiveCarouselIndex(i)}
|
|
560
555
|
style={{
|
|
561
556
|
width: i === activeCarouselIndex ? '20px' : '8px', height: '8px', borderRadius: '4px',
|
|
562
|
-
backgroundColor: i === activeCarouselIndex ?
|
|
557
|
+
backgroundColor: i === activeCarouselIndex ? mcqSelectedAccentVar : '#d1d5db', border: 'none', padding: 0,
|
|
563
558
|
cursor: 'pointer', transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
|
|
564
559
|
}} />
|
|
565
560
|
))}
|
|
@@ -50,7 +50,9 @@ export function CustomSliderTrack({
|
|
|
50
50
|
? tooltipLabel.replace(/<[^>]*>/g, '')
|
|
51
51
|
: String(Math.round(value));
|
|
52
52
|
|
|
53
|
-
const themeColor = disabled
|
|
53
|
+
const themeColor = disabled
|
|
54
|
+
? '#9ca3af'
|
|
55
|
+
: 'var(--cfm-slider-thumb, var(--cfm-input-focus-ring, var(--cfm-primary)))';
|
|
54
56
|
|
|
55
57
|
return (
|
|
56
58
|
<div
|
|
@@ -127,7 +129,7 @@ export function CustomSliderTrack({
|
|
|
127
129
|
{(isHovered || isDragging) && !disabled && (
|
|
128
130
|
<div style={{
|
|
129
131
|
position: 'absolute', bottom: '100%', left: `${percentage}%`, transform: 'translate(-50%, -10px)',
|
|
130
|
-
backgroundColor: '#1f2937', color: '#fff', fontSize: '13px', fontWeight: 600,
|
|
132
|
+
backgroundColor: 'var(--cfm-tooltip-bg, #1f2937)', color: '#fff', fontSize: '13px', fontWeight: 600,
|
|
131
133
|
padding: '4px 10px', borderRadius: '6px', pointerEvents: 'none',
|
|
132
134
|
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)', zIndex: 20,
|
|
133
135
|
}}>
|
|
@@ -135,7 +137,7 @@ export function CustomSliderTrack({
|
|
|
135
137
|
<div style={{
|
|
136
138
|
position: 'absolute', top: '100%', left: '50%', transform: 'translateX(-50%)',
|
|
137
139
|
borderWidth: '5px', borderStyle: 'solid',
|
|
138
|
-
borderColor: '#1f2937 transparent transparent transparent',
|
|
140
|
+
borderColor: 'var(--cfm-tooltip-bg, #1f2937) transparent transparent transparent',
|
|
139
141
|
}} />
|
|
140
142
|
</div>
|
|
141
143
|
)}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import React, { useState, useRef } from 'react';
|
|
10
10
|
import type { FileUploadQuestion } from '@explorer02/cfm-survey-sdk';
|
|
11
|
+
import { focusRingVar, mcqSelectedBgVar } from '@/lib/surveyUi/selectionStyles';
|
|
11
12
|
|
|
12
13
|
type UploadedFileAnswer = {
|
|
13
14
|
id: string;
|
|
@@ -86,8 +87,20 @@ function FileUploadDropzone({
|
|
|
86
87
|
onDrag: (e: React.DragEvent) => void;
|
|
87
88
|
onDrop: (e: React.DragEvent) => void;
|
|
88
89
|
}) {
|
|
90
|
+
const baseBorder = 'var(--cfm-upload-border-color, #d1d5db)';
|
|
91
|
+
const activeBorder = focusRingVar;
|
|
92
|
+
const baseBg = 'var(--cfm-upload-fill, #f9fafb)';
|
|
93
|
+
const activeBg = mcqSelectedBgVar;
|
|
94
|
+
const borderStyle =
|
|
95
|
+
typeof document !== 'undefined'
|
|
96
|
+
? getComputedStyle(document.documentElement).getPropertyValue('--cfm-upload-border-style').trim() ||
|
|
97
|
+
'dashed'
|
|
98
|
+
: 'dashed';
|
|
99
|
+
|
|
89
100
|
return (
|
|
90
101
|
<div
|
|
102
|
+
data-cfm-dropzone
|
|
103
|
+
className="cfm-dropzone"
|
|
91
104
|
onDragEnter={onDrag}
|
|
92
105
|
onDragOver={onDrag}
|
|
93
106
|
onDragLeave={onDrag}
|
|
@@ -98,17 +111,45 @@ function FileUploadDropzone({
|
|
|
98
111
|
flexDirection: 'column',
|
|
99
112
|
alignItems: 'center',
|
|
100
113
|
justifyContent: 'center',
|
|
101
|
-
border: `2px ${isDragActive ?
|
|
114
|
+
border: `2px ${borderStyle} ${isDragActive ? activeBorder : baseBorder}`,
|
|
102
115
|
borderRadius: 'var(--cfm-border-radius, 12px)',
|
|
103
|
-
backgroundColor: isDragActive ?
|
|
116
|
+
backgroundColor: isDragActive ? activeBg : baseBg,
|
|
104
117
|
padding: 'var(--cfm-upload-padding, 40px 24px)',
|
|
105
118
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
106
119
|
opacity: disabled ? 0.6 : 1,
|
|
107
120
|
textAlign: 'center',
|
|
108
121
|
boxSizing: 'border-box',
|
|
122
|
+
transition: 'all 0.2s ease-in-out',
|
|
123
|
+
}}
|
|
124
|
+
onMouseEnter={e => {
|
|
125
|
+
if (disabled) return;
|
|
126
|
+
e.currentTarget.style.borderColor = activeBorder;
|
|
127
|
+
e.currentTarget.style.backgroundColor = activeBg;
|
|
128
|
+
}}
|
|
129
|
+
onMouseLeave={e => {
|
|
130
|
+
if (disabled || isDragActive) return;
|
|
131
|
+
e.currentTarget.style.borderColor = baseBorder;
|
|
132
|
+
e.currentTarget.style.backgroundColor = baseBg;
|
|
109
133
|
}}
|
|
110
134
|
>
|
|
111
|
-
<div style={{
|
|
135
|
+
<div style={{ marginBottom: '16px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
136
|
+
<svg
|
|
137
|
+
width="40"
|
|
138
|
+
height="40"
|
|
139
|
+
viewBox="0 0 24 24"
|
|
140
|
+
fill="none"
|
|
141
|
+
stroke="var(--cfm-upload-icon-color, #9ca3af)"
|
|
142
|
+
strokeWidth="2"
|
|
143
|
+
strokeLinecap="round"
|
|
144
|
+
strokeLinejoin="round"
|
|
145
|
+
>
|
|
146
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
147
|
+
<polyline points="17 8 12 3 7 8" />
|
|
148
|
+
<line x1="12" y1="3" x2="12" y2="15" />
|
|
149
|
+
</svg>
|
|
150
|
+
</div>
|
|
151
|
+
<div
|
|
152
|
+
style={{ fontSize: '16px', fontWeight: 600, color: '#374151', marginBottom: '8px' }}
|
|
112
153
|
dangerouslySetInnerHTML={{ __html: uploadMessage }}
|
|
113
154
|
/>
|
|
114
155
|
<div style={{ fontSize: '13px', color: '#6b7280', lineHeight: 1.5 }}>
|
|
@@ -120,6 +161,104 @@ function FileUploadDropzone({
|
|
|
120
161
|
);
|
|
121
162
|
}
|
|
122
163
|
|
|
164
|
+
function formatFileSize(bytes: number): string {
|
|
165
|
+
if (bytes === 0) return '0 Bytes';
|
|
166
|
+
const k = 1024;
|
|
167
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
168
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
169
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function UploadedFileList({
|
|
173
|
+
files,
|
|
174
|
+
onRemoveFile,
|
|
175
|
+
}: {
|
|
176
|
+
files: UploadedFileAnswer[];
|
|
177
|
+
onRemoveFile: (index: number) => void;
|
|
178
|
+
}) {
|
|
179
|
+
return (
|
|
180
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
181
|
+
{files.map((file, idx) => (
|
|
182
|
+
<div
|
|
183
|
+
key={file.id}
|
|
184
|
+
style={{
|
|
185
|
+
display: 'flex',
|
|
186
|
+
alignItems: 'center',
|
|
187
|
+
justifyContent: 'space-between',
|
|
188
|
+
padding: '12px 16px',
|
|
189
|
+
backgroundColor: 'var(--cfm-card-bg, #fff)',
|
|
190
|
+
border: '1px solid var(--cfm-card-border-color, #e5e7eb)',
|
|
191
|
+
borderRadius: 'var(--cfm-border-radius, 8px)',
|
|
192
|
+
boxSizing: 'border-box',
|
|
193
|
+
}}
|
|
194
|
+
>
|
|
195
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', overflow: 'hidden' }}>
|
|
196
|
+
<svg
|
|
197
|
+
width="20"
|
|
198
|
+
height="20"
|
|
199
|
+
viewBox="0 0 24 24"
|
|
200
|
+
fill="none"
|
|
201
|
+
stroke="#6b7280"
|
|
202
|
+
strokeWidth="2"
|
|
203
|
+
strokeLinecap="round"
|
|
204
|
+
strokeLinejoin="round"
|
|
205
|
+
>
|
|
206
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
207
|
+
<polyline points="14 2 14 8 20 8" />
|
|
208
|
+
</svg>
|
|
209
|
+
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
|
210
|
+
<span
|
|
211
|
+
style={{
|
|
212
|
+
fontSize: '14px',
|
|
213
|
+
fontWeight: 500,
|
|
214
|
+
color: '#374151',
|
|
215
|
+
textOverflow: 'ellipsis',
|
|
216
|
+
overflow: 'hidden',
|
|
217
|
+
whiteSpace: 'nowrap',
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
{file.name ?? file.mediaUrl}
|
|
221
|
+
</span>
|
|
222
|
+
{file.size != null && (
|
|
223
|
+
<span style={{ fontSize: '12px', color: '#9ca3af' }}>{formatFileSize(file.size)}</span>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
<button
|
|
228
|
+
type="button"
|
|
229
|
+
onClick={() => onRemoveFile(idx)}
|
|
230
|
+
style={{
|
|
231
|
+
display: 'flex',
|
|
232
|
+
alignItems: 'center',
|
|
233
|
+
justifyContent: 'center',
|
|
234
|
+
width: '24px',
|
|
235
|
+
height: '24px',
|
|
236
|
+
borderRadius: '50%',
|
|
237
|
+
backgroundColor: '#f3f4f6',
|
|
238
|
+
border: 'none',
|
|
239
|
+
color: '#6b7280',
|
|
240
|
+
cursor: 'pointer',
|
|
241
|
+
fontSize: '14px',
|
|
242
|
+
fontWeight: 'bold',
|
|
243
|
+
transition: 'background-color 0.2s',
|
|
244
|
+
}}
|
|
245
|
+
onMouseEnter={e => {
|
|
246
|
+
e.currentTarget.style.backgroundColor = '#fee2e2';
|
|
247
|
+
e.currentTarget.style.color = '#ef4444';
|
|
248
|
+
}}
|
|
249
|
+
onMouseLeave={e => {
|
|
250
|
+
e.currentTarget.style.backgroundColor = '#f3f4f6';
|
|
251
|
+
e.currentTarget.style.color = '#6b7280';
|
|
252
|
+
}}
|
|
253
|
+
>
|
|
254
|
+
×
|
|
255
|
+
</button>
|
|
256
|
+
</div>
|
|
257
|
+
))}
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
123
262
|
export function FileUploadScale({ question, selectedValue, onSelect }: FileUploadScaleProps) {
|
|
124
263
|
const {
|
|
125
264
|
uploadMessage = 'Drag attachments here or browse',
|
|
@@ -241,21 +380,7 @@ export function FileUploadScale({ question, selectedValue, onSelect }: FileUploa
|
|
|
241
380
|
)}
|
|
242
381
|
|
|
243
382
|
{currentFiles.length > 0 && (
|
|
244
|
-
<
|
|
245
|
-
{currentFiles.map((file, idx) => (
|
|
246
|
-
<div key={file.id} style={{
|
|
247
|
-
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
248
|
-
padding: '12px 16px', backgroundColor: '#fff', border: '1px solid #e5e7eb', borderRadius: '8px',
|
|
249
|
-
}}>
|
|
250
|
-
<span style={{ fontSize: '14px', fontWeight: 500, color: '#374151' }}>{file.name ?? file.mediaUrl}</span>
|
|
251
|
-
<button type="button" onClick={() => removeFile(idx)} style={{
|
|
252
|
-
border: 'none', background: '#f3f4f6', borderRadius: '50%', width: 24, height: 24, cursor: 'pointer',
|
|
253
|
-
}}>
|
|
254
|
-
×
|
|
255
|
-
</button>
|
|
256
|
-
</div>
|
|
257
|
-
))}
|
|
258
|
-
</div>
|
|
383
|
+
<UploadedFileList files={currentFiles} onRemoveFile={removeFile} />
|
|
259
384
|
)}
|
|
260
385
|
</div>
|
|
261
386
|
);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Portable HeatmapScale — copy to src/components/HeatmapScale.tsx
|
|
5
|
-
* Also copy templates/heatmapCoords.ts to src/lib/heatmapCoords.ts
|
|
5
|
+
* Also copy templates/heatmapCoords.ts to src/lib/surveyUi/heatmapCoords.ts
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import React, { useState } from 'react';
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
createNewSpot,
|
|
12
12
|
findSpotNearClick,
|
|
13
13
|
getNormalizedClickFromImage,
|
|
14
|
-
} from '
|
|
14
|
+
} from '@/lib/surveyUi/heatmapCoords';
|
|
15
15
|
|
|
16
16
|
type HeatmapScaleProps = {
|
|
17
17
|
question: HeatmapQuestion;
|
|
@@ -91,6 +91,15 @@ export function HeatmapScale({ question, selectedValue = [], onSelect }: Heatmap
|
|
|
91
91
|
className="block max-w-full h-auto select-none rounded-md"
|
|
92
92
|
style={{ cursor: isImageReady ? 'crosshair' : 'default' }}
|
|
93
93
|
/>
|
|
94
|
+
{isImageReady && (
|
|
95
|
+
<div
|
|
96
|
+
className="pointer-events-none absolute inset-0 rounded-md"
|
|
97
|
+
style={{
|
|
98
|
+
backgroundColor: '#000000',
|
|
99
|
+
opacity: 'var(--cfm-heatmap-overlay-opacity, 0)',
|
|
100
|
+
}}
|
|
101
|
+
/>
|
|
102
|
+
)}
|
|
94
103
|
{isImageReady && (
|
|
95
104
|
<div className="pointer-events-none absolute inset-0">
|
|
96
105
|
{spots.map(spot => (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import type { SurveyLanguage } from '@explorer02/cfm-survey-sdk';
|
|
4
|
+
import { focusRingVar } from '@/lib/surveyUi/selectionStyles';
|
|
4
5
|
|
|
5
6
|
type LanguageSelectorProps = {
|
|
6
7
|
languages: SurveyLanguage[];
|
|
@@ -14,27 +15,49 @@ export default function LanguageSelector({
|
|
|
14
15
|
selectedLanguage,
|
|
15
16
|
onChange,
|
|
16
17
|
}: LanguageSelectorProps) {
|
|
17
|
-
if (languages.length <= 1) return null;
|
|
18
|
+
if (!languages || languages.length <= 1) return null;
|
|
18
19
|
|
|
19
20
|
return (
|
|
20
|
-
<div data-cfm-language className="mb-6 flex justify-end">
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
21
|
+
<div data-cfm-language className="mb-6 mt-4 flex justify-end">
|
|
22
|
+
<div className="relative">
|
|
23
|
+
<select
|
|
24
|
+
value={selectedLanguage}
|
|
25
|
+
onChange={(e) => onChange(e.target.value)}
|
|
26
|
+
className="cursor-pointer appearance-none py-2 pl-4 pr-10 text-sm font-semibold outline-none focus:ring-2"
|
|
27
|
+
style={{
|
|
28
|
+
background: 'var(--cfm-language-bg, #fff)',
|
|
29
|
+
color: 'var(--cfm-language-text, #2563eb)',
|
|
30
|
+
borderColor: 'var(--cfm-language-border, #2563eb)',
|
|
31
|
+
borderWidth: 1,
|
|
32
|
+
borderStyle: 'solid',
|
|
33
|
+
borderRadius: 'var(--cfm-border-radius, 8px)',
|
|
34
|
+
boxShadow: `0 0 0 0px ${focusRingVar}`,
|
|
35
|
+
}}
|
|
36
|
+
onFocus={e => {
|
|
37
|
+
e.currentTarget.style.boxShadow = `0 0 0 2px ${focusRingVar}`;
|
|
38
|
+
}}
|
|
39
|
+
onBlur={e => {
|
|
40
|
+
e.currentTarget.style.boxShadow = 'none';
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
{languages.map((lang) => (
|
|
44
|
+
<option key={lang.code} value={lang.code} className="text-gray-900">
|
|
45
|
+
{lang.name}
|
|
46
|
+
</option>
|
|
47
|
+
))}
|
|
48
|
+
</select>
|
|
49
|
+
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
|
50
|
+
<svg
|
|
51
|
+
className="h-4 w-4"
|
|
52
|
+
style={{ color: 'var(--cfm-language-text, #2563eb)' }}
|
|
53
|
+
fill="none"
|
|
54
|
+
stroke="currentColor"
|
|
55
|
+
viewBox="0 0 24 24"
|
|
56
|
+
>
|
|
57
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
|
|
58
|
+
</svg>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
38
61
|
</div>
|
|
39
62
|
);
|
|
40
63
|
}
|
|
@@ -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, matrixRowBackgroundStyle } 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,11 @@ function LikertMatrixGridLayout({
|
|
|
261
258
|
{repeatColumnHeaders && renderHeader()}
|
|
262
259
|
<div style={{
|
|
263
260
|
display: 'flex', alignItems: 'center', borderRadius: '8px',
|
|
264
|
-
padding: '
|
|
265
|
-
|
|
261
|
+
padding: 'var(--cfm-matrix-cell-padding, 12px) 0',
|
|
262
|
+
...(!repeatColumnHeaders ? matrixRowBackgroundStyle(rowIdx) : { backgroundColor: '#fff' }),
|
|
263
|
+
boxSizing: 'border-box',
|
|
266
264
|
}}>
|
|
267
|
-
<div style={{ flex: isBipolar ?
|
|
265
|
+
<div style={{ flex: isBipolar ? `0 0 ${BIPOLAR_COL_WIDTH}` : `0 0 ${MATRIX_ROW_LABEL_WIDTH}`, padding: '0 16px', boxSizing: 'border-box' }}>
|
|
268
266
|
<span style={{ fontSize: '14px', fontWeight: 500, color: '#111827', lineHeight: 1.4, wordWrap: 'break-word', display: 'block' }}
|
|
269
267
|
dangerouslySetInnerHTML={{ __html: transposeTable ? ('label' in rowItem ? rowItem.label : '') : ('statementText' in rowItem ? rowItem.statementText : '') }} />
|
|
270
268
|
</div>
|
|
@@ -282,31 +280,44 @@ function LikertMatrixGridLayout({
|
|
|
282
280
|
|
|
283
281
|
return (
|
|
284
282
|
<div key={colItem.id} style={{ display: 'flex', justifyContent: 'center' }}>
|
|
285
|
-
<label style={{
|
|
286
|
-
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
|
283
|
+
<label style={{
|
|
284
|
+
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
|
287
285
|
opacity: isDisabled ? 0.4 : 1,
|
|
288
|
-
padding: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center'
|
|
286
|
+
padding: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
289
287
|
}}>
|
|
290
|
-
<input
|
|
291
|
-
type=
|
|
288
|
+
<input
|
|
289
|
+
type="checkbox"
|
|
290
|
+
className="sr-only"
|
|
292
291
|
checked={selected}
|
|
293
292
|
disabled={isDisabled}
|
|
294
|
-
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
onChange={(e) => {
|
|
300
|
-
if (isMultiple || !selected) {
|
|
293
|
+
onChange={() => {
|
|
294
|
+
if (isDisabled) return;
|
|
295
|
+
if (isMultiple) {
|
|
296
|
+
onCellSelect(actualRowId, selected ? undefined : actualColValue);
|
|
297
|
+
} else if (!selected) {
|
|
301
298
|
onCellSelect(actualRowId, actualColValue);
|
|
299
|
+
} else {
|
|
300
|
+
onCellSelect(actualRowId, undefined);
|
|
302
301
|
}
|
|
303
302
|
}}
|
|
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
303
|
/>
|
|
304
|
+
{isMultiple ? (
|
|
305
|
+
<div style={{
|
|
306
|
+
display: 'flex', width: '18px', height: '18px', alignItems: 'center', justifyContent: 'center',
|
|
307
|
+
borderRadius: '4px', border: selected ? `2px solid ${focusRingVar}` : '2px solid #d1d5db',
|
|
308
|
+
backgroundColor: selected ? focusRingVar : '#fff',
|
|
309
|
+
}}>
|
|
310
|
+
{selected && (
|
|
311
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="3">
|
|
312
|
+
<polyline points="20 6 9 17 4 12" />
|
|
313
|
+
</svg>
|
|
314
|
+
)}
|
|
315
|
+
</div>
|
|
316
|
+
) : (
|
|
317
|
+
<div style={matrixRadioRingStyle(selected, '18px')}>
|
|
318
|
+
<div style={matrixRadioDotStyle(selected, '8px')} />
|
|
319
|
+
</div>
|
|
320
|
+
)}
|
|
310
321
|
</label>
|
|
311
322
|
</div>
|
|
312
323
|
);
|