@explorer02/cfm-survey-sdk 0.2.8 → 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 +6 -6
- 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 -0
- package/templates/docs/00-integration/wizard-config-handoff.md +11 -9
- package/templates/docs/00-integration/wizard-post-customize-build.md +6 -0
- package/templates/docs/00-integration/wizard-question-type-styling.md +4 -3
- package/templates/docs/00-integration/wizard-troubleshooting.md +20 -4
- package/templates/docs/index.md +1 -1
- 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/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
|
@@ -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) {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
scaleRadioDotStyle,
|
|
7
7
|
scaleCellSelectedStyle,
|
|
8
8
|
} from '@/lib/surveyUi/selectionStyles';
|
|
9
|
+
import { hintLabelStyle } from '@/lib/surveyUi/labelStyles';
|
|
9
10
|
import { getAccentColor, npsValueFromOption, starValueFromOption } from './surveyUiScaleUtils';
|
|
10
11
|
import { getEmojiForIndex } from './surveyUiIcons';
|
|
11
12
|
|
|
@@ -16,6 +17,10 @@ type RatingScaleProps = {
|
|
|
16
17
|
onSelect: (value: AnswerValue) => void;
|
|
17
18
|
variant?: 'nps' | 'star';
|
|
18
19
|
buttonStyle?: 'standard' | 'numbered' | 'emoji';
|
|
20
|
+
minLabel?: string;
|
|
21
|
+
midLabel?: string;
|
|
22
|
+
maxLabel?: string;
|
|
23
|
+
midLabelIndex?: number;
|
|
19
24
|
};
|
|
20
25
|
|
|
21
26
|
function resolveBadgeColor(index: number, value: number): string {
|
|
@@ -44,6 +49,10 @@ export default function RatingScale({
|
|
|
44
49
|
onSelect,
|
|
45
50
|
variant = 'nps',
|
|
46
51
|
buttonStyle = 'numbered',
|
|
52
|
+
minLabel,
|
|
53
|
+
midLabel,
|
|
54
|
+
maxLabel,
|
|
55
|
+
midLabelIndex,
|
|
47
56
|
}: RatingScaleProps) {
|
|
48
57
|
const resolvedOptions = options.map((option, index) => {
|
|
49
58
|
const value =
|
|
@@ -63,9 +72,37 @@ export default function RatingScale({
|
|
|
63
72
|
gap: 'var(--cfm-nps-cell-gap, 2px)',
|
|
64
73
|
};
|
|
65
74
|
|
|
75
|
+
const hintRow = (minLabel || midLabel || maxLabel) ? (
|
|
76
|
+
<div className="relative mb-3 h-6 w-full text-xs font-semibold">
|
|
77
|
+
<span
|
|
78
|
+
className="absolute left-0 top-0 max-w-[30%] text-left leading-tight"
|
|
79
|
+
style={hintLabelStyle()}
|
|
80
|
+
dangerouslySetInnerHTML={{ __html: minLabel ?? '' }}
|
|
81
|
+
/>
|
|
82
|
+
{midLabel && (
|
|
83
|
+
<span
|
|
84
|
+
className="absolute top-0 -translate-x-1/2 max-w-[40%] text-center leading-tight"
|
|
85
|
+
style={{
|
|
86
|
+
...hintLabelStyle(),
|
|
87
|
+
left: `${midLabelIndex !== undefined && columnCount > 1
|
|
88
|
+
? (midLabelIndex / (columnCount - 1)) * 100
|
|
89
|
+
: 50}%`,
|
|
90
|
+
}}
|
|
91
|
+
dangerouslySetInnerHTML={{ __html: midLabel }}
|
|
92
|
+
/>
|
|
93
|
+
)}
|
|
94
|
+
<span
|
|
95
|
+
className="absolute right-0 top-0 max-w-[30%] text-right leading-tight"
|
|
96
|
+
style={hintLabelStyle()}
|
|
97
|
+
dangerouslySetInnerHTML={{ __html: maxLabel ?? '' }}
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
) : null;
|
|
101
|
+
|
|
66
102
|
if (buttonStyle === 'emoji') {
|
|
67
103
|
return (
|
|
68
104
|
<div role="radiogroup" className="w-full overflow-x-auto" data-cfm-nps-area>
|
|
105
|
+
{hintRow}
|
|
69
106
|
<div className="min-w-0" style={gridStyle}>
|
|
70
107
|
{resolvedOptions.map((option, index) => {
|
|
71
108
|
const isSelected = selectedValue === option.value;
|
|
@@ -90,6 +127,7 @@ export default function RatingScale({
|
|
|
90
127
|
|
|
91
128
|
return (
|
|
92
129
|
<div role="radiogroup" className="w-full overflow-x-auto" data-cfm-nps-area>
|
|
130
|
+
{hintRow}
|
|
93
131
|
<div className="mb-3 min-w-0" style={gridStyle}>
|
|
94
132
|
{resolvedOptions.map((option, index) => {
|
|
95
133
|
const isSelected = selectedValue === option.value;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import React from 'react';
|
|
9
9
|
import type { SliderMatrixQuestion, MatrixRowAnswers } from '@explorer02/cfm-survey-sdk';
|
|
10
|
+
import { sliderTickLabelStyle } from '@/lib/surveyUi/labelStyles';
|
|
10
11
|
import { tickColorFromIndex } from './surveyUiScaleUtils';
|
|
11
12
|
import { CustomSliderTrack } from './CustomSliderTrack';
|
|
12
13
|
|
|
@@ -97,7 +98,7 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
|
|
|
97
98
|
width: `${sliceWidth}%`, display: 'flex', justifyContent: 'center',
|
|
98
99
|
}}>
|
|
99
100
|
{sliderType === 'graphics' ? (
|
|
100
|
-
<div style={{ wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2', fontWeight: 500,
|
|
101
|
+
<div style={{ wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2', fontWeight: 500, fontSize: '13px', ...sliderTickLabelStyle() }}>
|
|
101
102
|
{tv.label}
|
|
102
103
|
</div>
|
|
103
104
|
) : (
|
|
@@ -121,7 +122,7 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
|
|
|
121
122
|
position: 'absolute', left: `${percentage}%`, transform: 'translateX(-50%)',
|
|
122
123
|
width: `${sliceWidth}%`, display: 'flex', justifyContent: 'center',
|
|
123
124
|
}}>
|
|
124
|
-
<span style={{ wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2' }}>{m}</span>
|
|
125
|
+
<span style={{ wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2', ...sliderTickLabelStyle() }}>{m}</span>
|
|
125
126
|
</div>
|
|
126
127
|
);
|
|
127
128
|
})
|
|
@@ -151,7 +152,7 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
|
|
|
151
152
|
return (
|
|
152
153
|
<div key={row.id} style={{
|
|
153
154
|
display: 'flex', width: '100%', alignItems: 'center',
|
|
154
|
-
backgroundColor: '
|
|
155
|
+
backgroundColor: 'var(--cfm-slider-row-band)', borderRadius: '12px', padding: '16px', boxSizing: 'border-box',
|
|
155
156
|
}}>
|
|
156
157
|
<div style={{ flex: '0 0 25%', paddingRight: '16px', fontSize: '15px', color: '#111827', wordWrap: 'break-word', display: 'flex', alignItems: 'center' }}>
|
|
157
158
|
{row.statementText !== 'Question' && row.statementText !== '' ? (
|
|
@@ -102,6 +102,23 @@ if ! grep -q "layoutFlags" "$PREVIEW_BRIDGE"; then
|
|
|
102
102
|
fi
|
|
103
103
|
echo "OK: previewBridge.ts present with layoutFlags"
|
|
104
104
|
|
|
105
|
+
# Scale templates must consume wizard CSS vars (not only RatingScale)
|
|
106
|
+
SCALE_CFM_COUNT=0
|
|
107
|
+
for candidate in src/components/LikertMatrixScale.tsx components/LikertMatrixScale.tsx app/components/LikertMatrixScale.tsx; do
|
|
108
|
+
if [ -f "$candidate" ] && grep -q -- '--cfm-' "$candidate"; then SCALE_CFM_COUNT=1; break; fi
|
|
109
|
+
done
|
|
110
|
+
if [ "$SCALE_CFM_COUNT" -eq 0 ]; then
|
|
111
|
+
echo "WARN: LikertMatrixScale.tsx missing --cfm-* vars — copy from docs/templates"
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
SCALE_CSAT_COUNT=0
|
|
115
|
+
for candidate in src/components/CsatMatrixScale.tsx components/CsatMatrixScale.tsx app/components/CsatMatrixScale.tsx; do
|
|
116
|
+
if [ -f "$candidate" ] && grep -q -- '--cfm-' "$candidate"; then SCALE_CSAT_COUNT=1; break; fi
|
|
117
|
+
done
|
|
118
|
+
if [ "$SCALE_CSAT_COUNT" -eq 0 ]; then
|
|
119
|
+
echo "WARN: CsatMatrixScale.tsx missing --cfm-* vars — copy from docs/templates"
|
|
120
|
+
fi
|
|
121
|
+
|
|
105
122
|
REQUIRED_TYPES=(MCQ TEXTFIELD NPS_SCALE CFM_MATRIX CSAT_MATRIX RATING_MATRIX SLIDER_MATRIX FILE_UPLOAD TEXT_AND_MEDIA HEATMAP RANK_ORDER)
|
|
106
123
|
QUESTION_FILE=""
|
|
107
124
|
for candidate in src/components/Question.tsx components/Question.tsx app/components/Question.tsx; do
|
|
@@ -176,6 +176,16 @@
|
|
|
176
176
|
el.setAttribute('placeholder', patch.previewState.textfieldPlaceholder || '');
|
|
177
177
|
});
|
|
178
178
|
}
|
|
179
|
+
if (patch.previewState && patch.previewState.hintMinText !== undefined) {
|
|
180
|
+
document.querySelectorAll('[data-cfm-hint-min]').forEach(function (el) {
|
|
181
|
+
el.textContent = patch.previewState.hintMinText || '';
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (patch.previewState && patch.previewState.hintMaxText !== undefined) {
|
|
185
|
+
document.querySelectorAll('[data-cfm-hint-max]').forEach(function (el) {
|
|
186
|
+
el.textContent = patch.previewState.hintMaxText || '';
|
|
187
|
+
});
|
|
188
|
+
}
|
|
179
189
|
}
|
|
180
190
|
|
|
181
191
|
function handleMessage(event) {
|
|
@@ -3,8 +3,9 @@ export type PreviewPageDef = { key: string; title: string; body: string };
|
|
|
3
3
|
|
|
4
4
|
const sharedStyles = `
|
|
5
5
|
.cfm-header { height:var(--cfm-header-height); background:var(--cfm-header-bg); border-bottom:1px solid var(--cfm-header-border); display:flex; align-items:center; padding:0 var(--cfm-header-padding-x); box-shadow:0 2px 8px rgba(0,0,0,0.06); }
|
|
6
|
-
.cfm-header-
|
|
7
|
-
.cfm-header-
|
|
6
|
+
.cfm-header-brand { display:flex; align-items:center; gap:var(--cfm-header-brand-gap,16px); }
|
|
7
|
+
.cfm-header-logo { width:var(--cfm-header-logo-width); height:var(--cfm-header-logo-height); object-fit:contain; padding:var(--cfm-header-logo-padding); background:var(--cfm-header-logo-bg,transparent); }
|
|
8
|
+
.cfm-header-company { font-size:var(--cfm-header-company-size); font-weight:var(--cfm-header-company-weight); color:var(--cfm-header-company-color); }
|
|
8
9
|
.cfm-footer { background:var(--cfm-footer-bg); color:var(--cfm-footer-text); padding:var(--cfm-footer-padding-y) var(--cfm-footer-padding-x); display:flex; flex-wrap:wrap; gap:16px; justify-content:space-between; align-items:flex-end; }
|
|
9
10
|
.cfm-footer-links { display:flex; gap:24px; flex-wrap:wrap; }
|
|
10
11
|
.cfm-footer-links a { color:var(--cfm-footer-link); text-decoration:none; font-size:12px; }
|
|
@@ -28,7 +29,7 @@ const sharedStyles = `
|
|
|
28
29
|
.cfm-slider-track { height:8px; background:var(--cfm-slider-track); border-radius:4px; position:relative; margin:24px 0; }
|
|
29
30
|
.cfm-slider-thumb { width:20px; height:20px; background:var(--cfm-slider-thumb); border-radius:50%; position:absolute; top:-6px; left:60%; }
|
|
30
31
|
.cfm-tick { width:var(--cfm-slider-tick-size); height:var(--cfm-slider-tick-size); background:var(--cfm-slider-tick); border-radius:4px; font-size:11px; display:flex; align-items:center; justify-content:center; color:white; }
|
|
31
|
-
.cfm-dropzone { border:2px dashed var(--cfm-upload-
|
|
32
|
+
.cfm-dropzone { border:2px dashed var(--cfm-upload-border-color); border-radius:var(--cfm-border-radius); padding:var(--cfm-upload-padding); background:var(--cfm-upload-fill); text-align:center; color:var(--cfm-text); }
|
|
32
33
|
.cfm-input { width:100%; height:var(--cfm-input-height); border:1px solid var(--cfm-input-border); border-radius:var(--cfm-input-radius); padding:0 var(--cfm-input-padding); font-family:inherit; }
|
|
33
34
|
.cfm-input:focus { outline:2px solid var(--cfm-input-focus-ring); }
|
|
34
35
|
.cfm-rank-item { background:var(--cfm-rank-item-bg); padding:var(--cfm-rank-item-padding); margin-bottom:var(--cfm-rank-item-gap); border-radius:var(--cfm-border-radius); display:flex; align-items:center; gap:12px; }
|
|
@@ -59,10 +60,7 @@ export const PREVIEW_PAGE_DEFS: PreviewPageDef[] = [
|
|
|
59
60
|
{
|
|
60
61
|
key: 'header',
|
|
61
62
|
title: 'Header',
|
|
62
|
-
body: `<header class="cfm-header"
|
|
63
|
-
<img data-cfm-logo class="cfm-header-logo" src="" alt="Logo" style="display:none" />
|
|
64
|
-
<span data-cfm-company class="cfm-header-company">Company Name</span>
|
|
65
|
-
</header>`,
|
|
63
|
+
body: `<header class="cfm-header"><div class="cfm-header-brand"><img data-cfm-logo class="cfm-header-logo" src="" alt="Logo" style="display:none" /><span data-cfm-company class="cfm-header-company"></span></div></header>`,
|
|
66
64
|
},
|
|
67
65
|
{
|
|
68
66
|
key: 'footer',
|
|
@@ -65,12 +65,17 @@ function QuestionPreviewInner({ question, initialValue }: QuestionPreviewProps)
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
const matrixTypes = new Set(['CFM_MATRIX', 'CSAT_MATRIX', 'RATING_MATRIX', 'SLIDER_MATRIX']);
|
|
69
|
+
if ('statementRows' in q && matrixTypes.has(q.type)) {
|
|
70
|
+
const rows = q.statementRows ?? [];
|
|
71
|
+
if (previewState.multiStatement) {
|
|
72
|
+
if (rows.length === 1) {
|
|
73
|
+
const extra = { id: 'r2', statementText: 'Second statement row' };
|
|
74
|
+
q = { ...q, statementRows: [...rows, extra] } as typeof q;
|
|
75
|
+
}
|
|
76
|
+
} else if (rows.length > 1) {
|
|
77
|
+
q = { ...q, statementRows: rows.slice(0, 1) } as typeof q;
|
|
78
|
+
}
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
if (previewState.rankVariant === 'drag' && q.type === 'RANK_ORDER') {
|