@explorer02/cfm-survey-sdk 0.2.7 → 0.2.9
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 +38 -41
- package/dist/cli/index.mjs +36 -39
- package/package.json +1 -1
- package/templates/docs/00-integration/agent-execution-flow.md +7 -9
- package/templates/docs/00-integration/agent-operating-contract.md +2 -1
- package/templates/docs/00-integration/component-checklist.md +1 -0
- package/templates/docs/00-integration/ui-customization-wizard.md +29 -7
- package/templates/docs/00-integration/wizard-chrome-contract.md +11 -1
- package/templates/docs/00-integration/wizard-config-handoff.md +11 -9
- package/templates/docs/00-integration/wizard-post-customize-build.md +8 -2
- package/templates/docs/00-integration/wizard-preview-build-guide.md +0 -2
- package/templates/docs/00-integration/wizard-question-type-styling.md +7 -6
- package/templates/docs/00-integration/wizard-troubleshooting.md +20 -4
- package/templates/docs/03-ui-specs/README.md +1 -1
- package/templates/docs/MANIFEST.json +0 -3
- package/templates/docs/index.md +4 -6
- package/templates/docs/templates/CsatMatrixScale.tsx +56 -53
- package/templates/docs/templates/CustomSliderTrack.tsx +17 -8
- package/templates/docs/templates/FileUploadScale.tsx +11 -3
- package/templates/docs/templates/Header.tsx +32 -48
- package/templates/docs/templates/HeatmapScale.tsx +7 -4
- package/templates/docs/templates/LikertMatrixScale.tsx +58 -43
- package/templates/docs/templates/MatrixDropdown.tsx +12 -11
- package/templates/docs/templates/Question.tsx +4 -23
- package/templates/docs/templates/RankOrderScale.tsx +46 -6
- package/templates/docs/templates/RatingScale.tsx +38 -0
- package/templates/docs/templates/SliderMatrixScale.tsx +4 -3
- package/templates/docs/templates/verify-agent-build.sh +17 -0
- package/templates/preview-harness/preview-bridge.inline.js +10 -0
- package/templates/preview-harness/previewPages.ts +5 -7
- package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +11 -6
- package/templates/preview-harness/vite-app/src/fixtures/questions.ts +2 -10
- package/templates/preview-harness/vite-app/src/preview-live-overrides.css +11 -6
- package/templates/previewBridge.ts +13 -0
- package/templates/survey-theme.css +17 -17
- package/templates/wizard-dist/assets/{PreviewMock-tTMvwuEy.js → PreviewMock-CysRws9G.js} +1 -1
- package/templates/wizard-dist/assets/TypePanel-DWX1LL3q.js +1 -0
- package/templates/wizard-dist/assets/index-DIuc7AU2.js +34 -0
- package/templates/wizard-dist/assets/index-pCM8n0FT.css +1 -0
- package/templates/wizard-dist/index.html +2 -2
- package/templates/docs/00-integration/wizard-customization-catalog.md +0 -391
- package/templates/wizard-dist/assets/TypePanel-B9Ls7BSq.js +0 -1
- package/templates/wizard-dist/assets/index-CjkIlOUI.css +0 -1
- package/templates/wizard-dist/assets/index-CoCrnnxt.js +0 -34
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import React from 'react';
|
|
10
|
+
import { focusRingVar, cellSelectedVar } from '@/lib/surveyUi/selectionStyles';
|
|
10
11
|
import { getEmojiForIndex } from './surveyUiIcons';
|
|
11
12
|
|
|
12
13
|
type CustomSliderTrackProps = {
|
|
@@ -50,7 +51,8 @@ export function CustomSliderTrack({
|
|
|
50
51
|
? tooltipLabel.replace(/<[^>]*>/g, '')
|
|
51
52
|
: String(Math.round(value));
|
|
52
53
|
|
|
53
|
-
const
|
|
54
|
+
const trackColor = disabled ? 'var(--cfm-slider-tick, #9ca3af)' : 'var(--cfm-slider-track, var(--cfm-primary))';
|
|
55
|
+
const thumbBorderColor = disabled ? 'var(--cfm-slider-tick, #9ca3af)' : focusRingVar;
|
|
54
56
|
|
|
55
57
|
return (
|
|
56
58
|
<div
|
|
@@ -60,12 +62,12 @@ export function CustomSliderTrack({
|
|
|
60
62
|
>
|
|
61
63
|
<div style={{
|
|
62
64
|
position: 'absolute', top: '50%', left: 0, right: 0, transform: 'translateY(-50%)',
|
|
63
|
-
height: '4px', borderRadius: '2px', backgroundColor: '#e5e7eb',
|
|
65
|
+
height: '4px', borderRadius: '2px', backgroundColor: 'var(--cfm-slider-tick, #e5e7eb)',
|
|
64
66
|
}} />
|
|
65
67
|
|
|
66
68
|
<div style={{
|
|
67
69
|
position: 'absolute', top: '50%', left: 0, width: `${percentage}%`, transform: 'translateY(-50%)',
|
|
68
|
-
height: '4px', borderRadius: '2px', backgroundColor:
|
|
70
|
+
height: '4px', borderRadius: '2px', backgroundColor: trackColor,
|
|
69
71
|
transition: isDragging ? 'none' : 'width 0.15s ease-in-out',
|
|
70
72
|
}} />
|
|
71
73
|
|
|
@@ -76,7 +78,7 @@ export function CustomSliderTrack({
|
|
|
76
78
|
key={idx}
|
|
77
79
|
style={{
|
|
78
80
|
position: 'absolute', top: '50%', left: `${tickPct}%`, transform: 'translate(-50%, -50%)',
|
|
79
|
-
width: '1px', height: '8px', backgroundColor: '#d1d5db', pointerEvents: 'none',
|
|
81
|
+
width: '1px', height: '8px', backgroundColor: 'var(--cfm-slider-tick, #d1d5db)', pointerEvents: 'none',
|
|
80
82
|
}}
|
|
81
83
|
/>
|
|
82
84
|
);
|
|
@@ -113,15 +115,22 @@ export function CustomSliderTrack({
|
|
|
113
115
|
) : (
|
|
114
116
|
<div style={{
|
|
115
117
|
position: 'absolute', top: '50%', left: `${percentage}%`, transform: 'translate(-50%, -50%)',
|
|
116
|
-
width: '18px', height: '18px', borderRadius: '50%',
|
|
117
|
-
border: `2.5px solid ${
|
|
118
|
+
width: '18px', height: '18px', borderRadius: '50%',
|
|
119
|
+
border: `2.5px solid ${thumbBorderColor}`,
|
|
120
|
+
backgroundColor: cellSelectedVar,
|
|
121
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
118
122
|
boxShadow: (isHovered || isDragging) && !disabled
|
|
119
|
-
? '0 0 0 6px rgba(226, 0, 116, 0.2), 0 1px 3px rgba(0,0,0,0.2)'
|
|
123
|
+
? '0 0 0 6px var(--cfm-mcq-selected-bg, rgba(226, 0, 116, 0.2)), 0 1px 3px rgba(0,0,0,0.2)'
|
|
120
124
|
: '0 1px 3px rgba(0,0,0,0.2)',
|
|
121
125
|
pointerEvents: 'none',
|
|
122
126
|
transition: isDragging ? 'none' : 'left 0.15s ease-in-out, box-shadow 0.15s, border-color 0.2s',
|
|
123
127
|
boxSizing: 'border-box',
|
|
124
|
-
}}
|
|
128
|
+
}}>
|
|
129
|
+
<div style={{
|
|
130
|
+
width: '33%', height: '33%', borderRadius: '50%',
|
|
131
|
+
backgroundColor: 'var(--cfm-slider-thumb, var(--cfm-primary))',
|
|
132
|
+
}} />
|
|
133
|
+
</div>
|
|
125
134
|
)}
|
|
126
135
|
|
|
127
136
|
{(isHovered || isDragging) && !disabled && (
|
|
@@ -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 } from '@/lib/surveyUi/selectionStyles';
|
|
11
12
|
|
|
12
13
|
type UploadedFileAnswer = {
|
|
13
14
|
id: string;
|
|
@@ -86,8 +87,15 @@ function FileUploadDropzone({
|
|
|
86
87
|
onDrag: (e: React.DragEvent) => void;
|
|
87
88
|
onDrop: (e: React.DragEvent) => void;
|
|
88
89
|
}) {
|
|
90
|
+
const dropzoneStyleAttr =
|
|
91
|
+
typeof document !== 'undefined'
|
|
92
|
+
? document.documentElement.getAttribute('data-cfm-dropzone-style') ?? undefined
|
|
93
|
+
: undefined;
|
|
94
|
+
|
|
89
95
|
return (
|
|
90
96
|
<div
|
|
97
|
+
className="cfm-dropzone"
|
|
98
|
+
{...(dropzoneStyleAttr ? { 'data-cfm-dropzone-style': dropzoneStyleAttr } : {})}
|
|
91
99
|
onDragEnter={onDrag}
|
|
92
100
|
onDragOver={onDrag}
|
|
93
101
|
onDragLeave={onDrag}
|
|
@@ -98,10 +106,10 @@ function FileUploadDropzone({
|
|
|
98
106
|
flexDirection: 'column',
|
|
99
107
|
alignItems: 'center',
|
|
100
108
|
justifyContent: 'center',
|
|
101
|
-
border: `2px dashed ${isDragActive ?
|
|
109
|
+
border: `2px var(--cfm-upload-border-style, dashed) ${isDragActive ? focusRingVar : 'var(--cfm-upload-border-color)'}`,
|
|
102
110
|
borderRadius: '12px',
|
|
103
|
-
backgroundColor: isDragActive ? '
|
|
104
|
-
padding: '
|
|
111
|
+
backgroundColor: isDragActive ? 'var(--cfm-mcq-selected-bg)' : 'var(--cfm-upload-fill)',
|
|
112
|
+
padding: 'var(--cfm-upload-padding)',
|
|
105
113
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
106
114
|
opacity: disabled ? 0.6 : 1,
|
|
107
115
|
textAlign: 'center',
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Wizard-ready Header — copy to src/components/Header.tsx
|
|
3
3
|
*
|
|
4
4
|
* Brand row: [logo image] [company name] — company sits after logo when both exist.
|
|
5
|
-
*
|
|
5
|
+
* Layout locked: logo left, name after (flex row only).
|
|
6
6
|
*
|
|
7
7
|
* After wizard save, logo src MUST come from survey-ui-config.json global.logo.fileName
|
|
8
8
|
* via uiConfig.ts — never hardcode telekom-logo.svg or change fileName manually.
|
|
@@ -33,42 +33,30 @@ export default function Header({ embedded = false }: HeaderProps) {
|
|
|
33
33
|
}}
|
|
34
34
|
>
|
|
35
35
|
<div
|
|
36
|
-
className="cfm-header-inner
|
|
36
|
+
className="cfm-header-inner flex h-full w-full items-center"
|
|
37
37
|
style={{
|
|
38
|
-
gridTemplateColumns: '1fr 1fr 1fr',
|
|
39
38
|
padding: `0 var(--cfm-header-padding-x, 40px)`,
|
|
40
39
|
}}
|
|
41
40
|
>
|
|
42
41
|
<div
|
|
43
|
-
className="cfm-header-brand flex items-center"
|
|
42
|
+
className="cfm-header-brand flex min-w-0 items-center"
|
|
44
43
|
style={{
|
|
45
|
-
gridColumn: 'var(--cfm-header-logo-col, 1)',
|
|
46
|
-
justifySelf: 'var(--cfm-header-logo-justify, start)',
|
|
47
44
|
gap: 'var(--cfm-header-brand-gap, 16px)',
|
|
48
45
|
}}
|
|
49
46
|
>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
) : (
|
|
64
|
-
<img
|
|
65
|
-
data-cfm-logo
|
|
66
|
-
src=""
|
|
67
|
-
alt="Logo"
|
|
68
|
-
className="shrink-0 object-contain"
|
|
69
|
-
style={{ display: 'none' }}
|
|
70
|
-
/>
|
|
71
|
-
)}
|
|
47
|
+
<img
|
|
48
|
+
data-cfm-logo
|
|
49
|
+
src={hasLogoFile ? seedLogoSrc! : ''}
|
|
50
|
+
alt={seedCompany || 'Logo'}
|
|
51
|
+
className="shrink-0 object-contain"
|
|
52
|
+
style={{
|
|
53
|
+
display: hasLogoFile ? '' : 'none',
|
|
54
|
+
width: 'var(--cfm-header-logo-width, 120px)',
|
|
55
|
+
height: 'var(--cfm-header-logo-height, 120px)',
|
|
56
|
+
padding: 'var(--cfm-header-logo-padding, 0px)',
|
|
57
|
+
background: 'var(--cfm-header-logo-bg, transparent)',
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
72
60
|
|
|
73
61
|
<span
|
|
74
62
|
data-cfm-logo-text
|
|
@@ -83,26 +71,22 @@ export default function Header({ embedded = false }: HeaderProps) {
|
|
|
83
71
|
{seedCompany}
|
|
84
72
|
</span>
|
|
85
73
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}}
|
|
103
|
-
>
|
|
104
|
-
{seedCompany}
|
|
105
|
-
</span>
|
|
74
|
+
{seedShowCompany && seedCompany ? (
|
|
75
|
+
<span
|
|
76
|
+
data-cfm-company
|
|
77
|
+
className="truncate font-semibold"
|
|
78
|
+
style={{
|
|
79
|
+
display: hasLogoFile ? '' : 'none',
|
|
80
|
+
color: 'var(--cfm-header-company-color, var(--cfm-text))',
|
|
81
|
+
fontSize: 'var(--cfm-header-company-size, 18px)',
|
|
82
|
+
fontWeight: 'var(--cfm-header-company-weight, 600)',
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
{seedCompany}
|
|
86
|
+
</span>
|
|
87
|
+
) : (
|
|
88
|
+
<span data-cfm-company className="hidden" aria-hidden />
|
|
89
|
+
)}
|
|
106
90
|
</div>
|
|
107
91
|
</div>
|
|
108
92
|
</header>
|
|
@@ -26,11 +26,11 @@ function HeatmapSpot({ spot }: { spot: { id: string; x: number; y: number } }) {
|
|
|
26
26
|
style={{
|
|
27
27
|
left: `${spot.x * 100}%`,
|
|
28
28
|
top: `${spot.y * 100}%`,
|
|
29
|
-
width:
|
|
30
|
-
height:
|
|
29
|
+
width: 'var(--cfm-heatmap-pin-size, 16px)',
|
|
30
|
+
height: 'var(--cfm-heatmap-pin-size, 16px)',
|
|
31
31
|
transform: 'translate(-50%, -50%)',
|
|
32
|
-
backgroundColor: '
|
|
33
|
-
border: '2px solid white',
|
|
32
|
+
backgroundColor: 'var(--cfm-heatmap-pin)',
|
|
33
|
+
border: '2px solid var(--cfm-heatmap-pin-border, white)',
|
|
34
34
|
boxShadow: '0 1px 3px rgba(0,0,0,0.25)',
|
|
35
35
|
}}
|
|
36
36
|
/>
|
|
@@ -63,6 +63,9 @@ export function HeatmapScale({ question, selectedValue = [], onSelect }: Heatmap
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
onSelect([...spots, createNewSpot(x, y)]);
|
|
66
|
+
if (typeof window !== 'undefined' && window.parent !== window) {
|
|
67
|
+
window.parent.postMessage({ type: 'CFM_HEATMAP_PIN', pin: { x, y } }, '*');
|
|
68
|
+
}
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
function handleImageLoad() {
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import type { CfmMatrixQuestion, MatrixRowAnswers } from '@explorer02/cfm-survey-sdk';
|
|
3
|
+
import { matrixColumnLabelStyle } from '@/lib/surveyUi/labelStyles';
|
|
4
|
+
import {
|
|
5
|
+
cellSelectedVar,
|
|
6
|
+
focusRingVar,
|
|
7
|
+
matrixRadioDotStyle,
|
|
8
|
+
matrixRadioRingStyle,
|
|
9
|
+
} from '@/lib/surveyUi/selectionStyles';
|
|
3
10
|
import { columnSubmitValue } from './surveyUiScaleUtils';
|
|
4
11
|
import MatrixDropdown from './MatrixDropdown';
|
|
5
12
|
|
|
@@ -111,8 +118,8 @@ function LikertMatrixCarouselLayout({
|
|
|
111
118
|
<div style={{
|
|
112
119
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
113
120
|
width: '18px', height: '18px', borderRadius: '4px',
|
|
114
|
-
border: selected ?
|
|
115
|
-
backgroundColor: selected ?
|
|
121
|
+
border: selected ? `2px solid ${focusRingVar}` : '2px solid #d1d5db',
|
|
122
|
+
backgroundColor: selected ? cellSelectedVar : '#fff',
|
|
116
123
|
flexShrink: 0, transition: 'all 0.15s'
|
|
117
124
|
}}>
|
|
118
125
|
{selected && (
|
|
@@ -122,13 +129,8 @@ function LikertMatrixCarouselLayout({
|
|
|
122
129
|
)}
|
|
123
130
|
</div>
|
|
124
131
|
) : (
|
|
125
|
-
<div style={
|
|
126
|
-
|
|
127
|
-
alignItems: 'center', justifyContent: 'center', borderRadius: '50%',
|
|
128
|
-
border: selected ? '2px solid #e20074' : '1.5px solid #9ca3af',
|
|
129
|
-
backgroundColor: '#fff',
|
|
130
|
-
}}>
|
|
131
|
-
{selected && <div style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: '#e20074' }} />}
|
|
132
|
+
<div style={matrixRadioRingStyle(selected)}>
|
|
133
|
+
{selected && <div style={matrixRadioDotStyle(selected, '10px')} />}
|
|
132
134
|
</div>
|
|
133
135
|
)}
|
|
134
136
|
<span style={{ fontSize: '15px', fontWeight: 400, color: '#374151', userSelect: 'none' }} dangerouslySetInnerHTML={{ __html: col.label }} />
|
|
@@ -206,7 +208,7 @@ function LikertMatrixGridLayout({
|
|
|
206
208
|
<div style={{ display: 'flex', flexDirection: 'column', marginBottom: '16px' }}>
|
|
207
209
|
{hasLabels && (
|
|
208
210
|
<div style={{ display: 'flex', alignItems: 'flex-end', minHeight: '24px', marginBottom: '8px' }}>
|
|
209
|
-
<div style={{ flex:
|
|
211
|
+
<div style={{ flex: `0 0 var(--cfm-matrix-bipolar-width, 25%)`, paddingRight: '16px', boxSizing: 'border-box' }} />
|
|
210
212
|
<div style={{ flex: 1, position: 'relative', height: '20px' }}>
|
|
211
213
|
{labels.map((lbl, idx) => {
|
|
212
214
|
let leftPos = '50%';
|
|
@@ -224,23 +226,23 @@ function LikertMatrixGridLayout({
|
|
|
224
226
|
);
|
|
225
227
|
})}
|
|
226
228
|
</div>
|
|
227
|
-
<div style={{ flex:
|
|
229
|
+
<div style={{ flex: `0 0 var(--cfm-matrix-bipolar-width, 25%)`, paddingLeft: '16px', boxSizing: 'border-box' }} />
|
|
228
230
|
</div>
|
|
229
231
|
)}
|
|
230
232
|
{showColumnHeaders && (
|
|
231
233
|
<div style={{ display: 'flex', alignItems: 'flex-end', minHeight: '24px' }}>
|
|
232
|
-
<div style={{ flex: isBipolar ? '0 0 25%' : '0 0 180px', paddingRight: '16px', boxSizing: 'border-box' }} />
|
|
234
|
+
<div style={{ flex: isBipolar ? '0 0 var(--cfm-matrix-bipolar-width, 25%)' : '0 0 var(--cfm-matrix-row-width, 180px)', paddingRight: '16px', boxSizing: 'border-box' }} />
|
|
233
235
|
<div style={{ flex: 1, display: 'flex' }}>
|
|
234
236
|
<div style={{ flex: 1, display: 'grid', gridTemplateColumns: `repeat(${gridCols.length}, 1fr)` }}>
|
|
235
237
|
{gridCols.map((col) => (
|
|
236
238
|
<div key={col.id} style={{ display: 'flex', justifyContent: 'center', padding: '0 4px' }}>
|
|
237
|
-
<span style={{ fontSize: '13px', fontWeight: 500,
|
|
239
|
+
<span style={{ fontSize: '13px', fontWeight: 500, textAlign: 'center', lineHeight: 1.2, ...matrixColumnLabelStyle() }}
|
|
238
240
|
dangerouslySetInnerHTML={{ __html: transposeTable ? ('statementText' in col ? col.statementText : col.label) : ('label' in col ? col.label : (col as { statementText: string }).statementText) }} />
|
|
239
241
|
</div>
|
|
240
242
|
))}
|
|
241
243
|
</div>
|
|
242
244
|
</div>
|
|
243
|
-
{isBipolar && <div style={{ flex:
|
|
245
|
+
{isBipolar && <div style={{ flex: `0 0 var(--cfm-matrix-bipolar-width, 25%)`, paddingLeft: '16px', boxSizing: 'border-box' }} />}
|
|
244
246
|
</div>
|
|
245
247
|
)}
|
|
246
248
|
</div>
|
|
@@ -258,10 +260,10 @@ function LikertMatrixGridLayout({
|
|
|
258
260
|
{repeatColumnHeaders && renderHeader()}
|
|
259
261
|
<div style={{
|
|
260
262
|
display: 'flex', alignItems: 'center', borderRadius: '8px',
|
|
261
|
-
padding: '12px 0', backgroundColor: !repeatColumnHeaders && rowIdx % 2 === 0 ? '
|
|
263
|
+
padding: '12px 0', backgroundColor: !repeatColumnHeaders && rowIdx % 2 === 0 ? 'var(--cfm-zebra-row)' : '#fff',
|
|
262
264
|
boxSizing: 'border-box'
|
|
263
265
|
}}>
|
|
264
|
-
<div style={{ flex: isBipolar ? '0 0 25%' : '0 0 180px', padding: '0 16px', boxSizing: 'border-box' }}>
|
|
266
|
+
<div style={{ flex: isBipolar ? '0 0 var(--cfm-matrix-bipolar-width, 25%)' : '0 0 var(--cfm-matrix-row-width, 180px)', padding: '0 16px', boxSizing: 'border-box' }}>
|
|
265
267
|
<span style={{ fontSize: '14px', fontWeight: 500, color: '#111827', lineHeight: 1.4, wordWrap: 'break-word', display: 'block' }}
|
|
266
268
|
dangerouslySetInnerHTML={{ __html: transposeTable ? ('label' in rowItem ? rowItem.label : '') : ('statementText' in rowItem ? rowItem.statementText : '') }} />
|
|
267
269
|
</div>
|
|
@@ -279,32 +281,45 @@ function LikertMatrixGridLayout({
|
|
|
279
281
|
|
|
280
282
|
return (
|
|
281
283
|
<div key={colItem.id} style={{ display: 'flex', justifyContent: 'center' }}>
|
|
282
|
-
<
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
284
|
+
<button
|
|
285
|
+
type="button"
|
|
286
|
+
disabled={isDisabled}
|
|
287
|
+
onClick={() => {
|
|
288
|
+
if (isDisabled) return;
|
|
289
|
+
if (!isMultiple && selected) {
|
|
290
|
+
onCellSelect(actualRowId, undefined);
|
|
291
|
+
} else {
|
|
292
|
+
onCellSelect(actualRowId, actualColValue);
|
|
293
|
+
}
|
|
294
|
+
}}
|
|
295
|
+
style={{
|
|
296
|
+
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
|
297
|
+
opacity: isDisabled ? 0.4 : 1,
|
|
298
|
+
padding: 'var(--cfm-matrix-cell-padding, 8px)',
|
|
299
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
300
|
+
border: 'none', background: 'transparent',
|
|
301
|
+
}}
|
|
302
|
+
>
|
|
303
|
+
{isMultiple ? (
|
|
304
|
+
<div style={{
|
|
305
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
306
|
+
width: '18px', height: '18px', borderRadius: '4px',
|
|
307
|
+
border: selected ? `2px solid ${focusRingVar}` : '2px solid #d1d5db',
|
|
308
|
+
backgroundColor: selected ? cellSelectedVar : '#fff',
|
|
309
|
+
transition: 'all 0.15s',
|
|
310
|
+
}}>
|
|
311
|
+
{selected && (
|
|
312
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
|
313
|
+
<polyline points="20 6 9 17 4 12" />
|
|
314
|
+
</svg>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
) : (
|
|
318
|
+
<div style={matrixRadioRingStyle(selected)}>
|
|
319
|
+
{selected && <div style={matrixRadioDotStyle(selected)} />}
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
</button>
|
|
308
323
|
</div>
|
|
309
324
|
);
|
|
310
325
|
})}
|
|
@@ -312,7 +327,7 @@ function LikertMatrixGridLayout({
|
|
|
312
327
|
</div>
|
|
313
328
|
|
|
314
329
|
{isBipolar && (
|
|
315
|
-
<div style={{ flex: '0 0 25%', padding: '0 16px', textAlign: 'right', boxSizing: 'border-box' }}>
|
|
330
|
+
<div style={{ flex: '0 0 var(--cfm-matrix-bipolar-width, 25%)', padding: '0 16px', textAlign: 'right', boxSizing: 'border-box' }}>
|
|
316
331
|
<span style={{ fontSize: '14px', fontWeight: 500, color: '#111827', lineHeight: 1.4, wordWrap: 'break-word', display: 'block' }}
|
|
317
332
|
dangerouslySetInnerHTML={{ __html: transposeTable ? '' : ('oppositeStatementText' in rowItem ? rowItem.oppositeStatementText ?? '' : '') }} />
|
|
318
333
|
</div>
|
|
@@ -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 { cellSelectedVar, focusRingVar } 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
|
|
78
|
+
boxShadow: isOpen ? '0 0 0 3px var(--cfm-mcq-selected-bg)' : '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
|
|
130
|
+
boxShadow: isOpen ? '0 0 0 3px var(--cfm-mcq-selected-bg)' : '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 ? 'var(--cfm-mcq-selected-bg)' : '#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 = 'var(--cfm-mcq-selected-bg)'; }}
|
|
174
|
+
onMouseLeave={e => { if (!isDisabled) e.currentTarget.style.backgroundColor = isSelected ? 'var(--cfm-mcq-selected-bg)' : '#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 ${focusRingVar}` : '2px solid #d1d5db',
|
|
192
|
+
backgroundColor: isSelected ? cellSelectedVar : '#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 ? focusRingVar : '#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: focusRingVar, 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
|
)}
|
|
@@ -140,29 +140,6 @@ export default function Question({
|
|
|
140
140
|
|
|
141
141
|
{question.type === QUESTION_TYPE.NPS_SCALE && (
|
|
142
142
|
<div className="space-y-3">
|
|
143
|
-
{(question.minLabel || question.midLabel || question.maxLabel) && (
|
|
144
|
-
<div className="relative w-full h-6 text-xs font-semibold text-gray-500">
|
|
145
|
-
<span
|
|
146
|
-
className="absolute left-0 top-0 max-w-[30%] text-left leading-tight"
|
|
147
|
-
dangerouslySetInnerHTML={{ __html: question.minLabel ?? '' }}
|
|
148
|
-
/>
|
|
149
|
-
{question.midLabel && (
|
|
150
|
-
<span
|
|
151
|
-
className="absolute top-0 -translate-x-1/2 text-center max-w-[40%] leading-tight"
|
|
152
|
-
style={{
|
|
153
|
-
left: `${question.midLabelIndex !== undefined && question.options.length > 1
|
|
154
|
-
? (question.midLabelIndex / (question.options.length - 1)) * 100
|
|
155
|
-
: 50}%`,
|
|
156
|
-
}}
|
|
157
|
-
dangerouslySetInnerHTML={{ __html: question.midLabel }}
|
|
158
|
-
/>
|
|
159
|
-
)}
|
|
160
|
-
<span
|
|
161
|
-
className="absolute right-0 top-0 max-w-[30%] text-right leading-tight"
|
|
162
|
-
dangerouslySetInnerHTML={{ __html: question.maxLabel ?? '' }}
|
|
163
|
-
/>
|
|
164
|
-
</div>
|
|
165
|
-
)}
|
|
166
143
|
<RatingScale
|
|
167
144
|
questionId={question.id}
|
|
168
145
|
options={question.options}
|
|
@@ -170,6 +147,10 @@ export default function Question({
|
|
|
170
147
|
onSelect={onSelect}
|
|
171
148
|
variant="nps"
|
|
172
149
|
buttonStyle={question.buttonStyle}
|
|
150
|
+
minLabel={question.minLabel}
|
|
151
|
+
midLabel={question.midLabel}
|
|
152
|
+
maxLabel={question.maxLabel}
|
|
153
|
+
midLabelIndex={question.midLabelIndex}
|
|
173
154
|
/>
|
|
174
155
|
</div>
|
|
175
156
|
)}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
verticalListSortingStrategy,
|
|
26
26
|
} from '@dnd-kit/sortable';
|
|
27
27
|
import { CSS } from '@dnd-kit/utilities';
|
|
28
|
+
import { focusRingVar } from '@/lib/surveyUi/selectionStyles';
|
|
28
29
|
import type {
|
|
29
30
|
RankOrderAnswers,
|
|
30
31
|
RankOrderOption,
|
|
@@ -119,11 +120,37 @@ function RankOrderOptionRow({
|
|
|
119
120
|
rankBadge,
|
|
120
121
|
}: RankOrderOptionRowProps) {
|
|
121
122
|
return (
|
|
122
|
-
<div
|
|
123
|
+
<div
|
|
124
|
+
style={{
|
|
125
|
+
display: 'flex',
|
|
126
|
+
alignItems: 'center',
|
|
127
|
+
gap: 'var(--cfm-rank-item-gap, 12px)',
|
|
128
|
+
borderRadius: '8px',
|
|
129
|
+
border: '1px solid #e5e5e5',
|
|
130
|
+
backgroundColor: 'var(--cfm-rank-item-bg, #fff)',
|
|
131
|
+
padding: 'var(--cfm-rank-item-padding, 12px 16px)',
|
|
132
|
+
transition: 'background-color 0.15s',
|
|
133
|
+
}}
|
|
134
|
+
className="hover:bg-gray-50/50"
|
|
135
|
+
>
|
|
123
136
|
{rankSelect}
|
|
124
137
|
{dragHandle}
|
|
125
138
|
{rankBadge !== undefined && (
|
|
126
|
-
<span
|
|
139
|
+
<span
|
|
140
|
+
style={{
|
|
141
|
+
display: 'flex',
|
|
142
|
+
height: '28px',
|
|
143
|
+
width: '28px',
|
|
144
|
+
flexShrink: 0,
|
|
145
|
+
alignItems: 'center',
|
|
146
|
+
justifyContent: 'center',
|
|
147
|
+
borderRadius: '9999px',
|
|
148
|
+
backgroundColor: 'var(--cfm-rank-badge)',
|
|
149
|
+
fontSize: '14px',
|
|
150
|
+
fontWeight: 600,
|
|
151
|
+
color: '#fff',
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
127
154
|
{rankBadge}
|
|
128
155
|
</span>
|
|
129
156
|
)}
|
|
@@ -147,7 +174,7 @@ function DropdownRankLayout({
|
|
|
147
174
|
};
|
|
148
175
|
|
|
149
176
|
return (
|
|
150
|
-
<div className="space-y-3">
|
|
177
|
+
<div className="space-y-3" style={{ display: 'flex', flexDirection: 'column', gap: 'var(--cfm-rank-item-gap, 12px)' }}>
|
|
151
178
|
{question.options.map(option => {
|
|
152
179
|
const currentRank = rankAnswers[option.id];
|
|
153
180
|
|
|
@@ -161,7 +188,19 @@ function DropdownRankLayout({
|
|
|
161
188
|
aria-label={`Rank for ${option.optionLabel || option.id}`}
|
|
162
189
|
value={currentRank ?? ''}
|
|
163
190
|
onChange={event => handleRankChange(option.id, event.target.value)}
|
|
164
|
-
|
|
191
|
+
style={{
|
|
192
|
+
height: '40px',
|
|
193
|
+
width: '64px',
|
|
194
|
+
flexShrink: 0,
|
|
195
|
+
borderRadius: '4px',
|
|
196
|
+
border: '1px solid var(--cfm-rank-select-border, #d1d5db)',
|
|
197
|
+
backgroundColor: '#fff',
|
|
198
|
+
padding: '0 8px',
|
|
199
|
+
fontSize: '14px',
|
|
200
|
+
outline: 'none',
|
|
201
|
+
}}
|
|
202
|
+
onFocus={e => { e.currentTarget.style.borderColor = focusRingVar; }}
|
|
203
|
+
onBlur={e => { e.currentTarget.style.borderColor = 'var(--cfm-rank-select-border, #d1d5db)'; }}
|
|
165
204
|
>
|
|
166
205
|
<option value="">-</option>
|
|
167
206
|
{rankChoices.map(rank => (
|
|
@@ -218,7 +257,8 @@ function SortableRankItem({
|
|
|
218
257
|
<button
|
|
219
258
|
type="button"
|
|
220
259
|
aria-label={`Drag to reorder ${option.optionLabel || option.id}`}
|
|
221
|
-
className="flex h-10 w-8 shrink-0 cursor-grab items-center justify-center rounded
|
|
260
|
+
className="flex h-10 w-8 shrink-0 cursor-grab items-center justify-center rounded active:cursor-grabbing"
|
|
261
|
+
style={{ color: 'var(--cfm-rank-handle, #6b7280)' }}
|
|
222
262
|
{...attributes}
|
|
223
263
|
{...listeners}
|
|
224
264
|
>
|
|
@@ -316,7 +356,7 @@ function DragDropRankLayout({
|
|
|
316
356
|
return (
|
|
317
357
|
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
|
318
358
|
<SortableContext items={orderedOptionIds} strategy={verticalListSortingStrategy}>
|
|
319
|
-
<div className="space-y-3">
|
|
359
|
+
<div className="space-y-3" style={{ display: 'flex', flexDirection: 'column', gap: 'var(--cfm-rank-item-gap, 12px)' }}>
|
|
320
360
|
{orderedOptionIds.map((optionId, index) => {
|
|
321
361
|
const option = question.options.find(item => item.id === optionId);
|
|
322
362
|
if (!option) {
|