@explorer02/cfm-survey-sdk 0.2.3 → 0.2.4
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 +140 -33
- package/dist/cli/index.mjs +140 -33
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +8 -1
- package/postinstall.js +8 -5
- package/templates/AGENT.md +60 -19
- package/templates/deploy-wizard-to-s3.sh +52 -0
- package/templates/docs/00-integration/agent-execution-flow.md +281 -0
- package/templates/docs/00-integration/agent-operating-contract.md +260 -0
- package/templates/docs/00-integration/apply-ui-config.md +365 -0
- package/templates/docs/00-integration/canonical-survey-fixtures.md +94 -0
- package/templates/docs/00-integration/client-integration-guide.md +9 -7
- package/templates/docs/00-integration/client-lib-folder.md +115 -0
- package/templates/docs/00-integration/component-checklist.md +88 -62
- package/templates/docs/00-integration/constraints.md +13 -5
- package/templates/docs/00-integration/custom-field-logic-and-navigation.md +56 -0
- package/templates/docs/00-integration/progress.md +4 -2
- package/templates/docs/00-integration/setup.md +12 -0
- package/templates/docs/00-integration/ui-customization-wizard.md +144 -0
- package/templates/docs/00-integration/useSurveySDK.md +35 -1
- package/templates/docs/00-integration/wizard-api.md +134 -0
- package/templates/docs/00-integration/wizard-config-handoff.md +246 -0
- package/templates/docs/00-integration/wizard-preview-build-guide.md +400 -0
- package/templates/docs/00-integration/wizard-question-type-styling.md +195 -0
- package/templates/docs/01-components/01-survey-page.md +18 -8
- package/templates/docs/01-components/02-question.md +2 -2
- package/templates/docs/01-components/03-rating-scale.md +12 -18
- package/templates/docs/01-components/05-csat-matrix-scale.md +18 -26
- package/templates/docs/01-components/06-likert-matrix-scale.md +19 -39
- package/templates/docs/01-components/07-slider-matrix-scale.md +48 -33
- package/templates/docs/01-components/09-custom-slider-track.md +15 -25
- package/templates/docs/01-components/10-header-footer.md +23 -4
- package/templates/docs/01-components/11-progress-bar.md +14 -6
- package/templates/docs/01-components/19-survey-sticky-chrome.md +79 -0
- package/templates/docs/01-components/README.md +2 -2
- package/templates/docs/02-reference/question-types/10-slider-matrix.md +38 -26
- package/templates/docs/02-reference/routing-table.md +1 -0
- package/templates/docs/02-reference/value-derivation.md +1 -1
- package/templates/docs/03-ui-specs/00-question-shell.md +10 -0
- package/templates/docs/03-ui-specs/01-rating.md +18 -0
- package/templates/docs/03-ui-specs/02-radio.md +16 -1
- package/templates/docs/03-ui-specs/03-text.md +12 -1
- package/templates/docs/03-ui-specs/04-csat.md +84 -50
- package/templates/docs/03-ui-specs/06-slider.md +3 -1
- package/templates/docs/03-ui-specs/07-matrix-cfm.md +71 -48
- package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +48 -18
- package/templates/docs/03-ui-specs/09-slider-matrix.md +59 -32
- package/templates/docs/03-ui-specs/10-file-upload.md +11 -0
- package/templates/docs/03-ui-specs/11-text-and-media.md +4 -0
- package/templates/docs/03-ui-specs/12-survey-chrome.md +54 -5
- package/templates/docs/03-ui-specs/13-heatmap.md +12 -1
- package/templates/docs/03-ui-specs/14-rank-order.md +11 -0
- package/templates/docs/03-ui-specs/README.md +4 -2
- package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +1 -1
- package/templates/docs/MANIFEST.json +162 -6
- package/templates/docs/index.md +116 -117
- package/templates/docs/templates/CsatMatrixScale.tsx +637 -0
- package/templates/docs/templates/FileUploadScale.tsx +262 -0
- package/templates/docs/templates/HeatmapScale.tsx +114 -0
- package/templates/docs/templates/LikertMatrixScale.tsx +414 -0
- package/templates/docs/templates/Question.tsx +3 -0
- package/templates/docs/templates/RatingScale.tsx +129 -0
- package/templates/docs/templates/SliderMatrixScale.tsx +248 -0
- package/templates/docs/templates/SurveyStickyChrome.tsx +24 -0
- package/templates/docs/templates/customFieldValues.ts +36 -0
- package/templates/docs/templates/implementation_plan.md +56 -9
- package/templates/docs/templates/labelStyles.ts +33 -0
- package/templates/docs/templates/selectionStyles.ts +100 -0
- package/templates/docs/templates/surveyUiIcons.tsx +11 -0
- package/templates/docs/templates/surveyUiScaleUtils.ts +51 -0
- package/templates/docs/templates/verify-agent-build.sh +119 -0
- package/templates/preview-harness/preview-bridge.inline.js +201 -0
- package/templates/preview-harness/previewPages.js +108 -0
- package/templates/preview-harness/previewPages.ts +298 -0
- package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +67 -0
- package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +122 -0
- package/templates/preview-harness/vite-app/src/SurveyPagePreview.tsx +75 -0
- package/templates/preview-harness/vite-app/src/fixtures/questions.ts +311 -0
- package/templates/preview-harness/vite-app/src/globals.css +16 -0
- package/templates/preview-harness/vite-app/src/mount.tsx +9 -0
- package/templates/preview-harness/vite-app/src/preview-live-overrides.css +101 -0
- package/templates/preview-harness/vite-app/stubs/next-dynamic.tsx +25 -0
- package/templates/preview-harness/vite-app/stubs/next-image.tsx +29 -0
- package/templates/previewBridge.ts +153 -0
- package/templates/survey-theme.css +295 -0
- package/templates/survey-ui-config.schema.json +213 -0
- package/templates/wizard-dist/assets/PreviewMock-DgHfrVeb.js +1 -0
- package/templates/wizard-dist/assets/TypePanel-CFVC3Ptn.js +1 -0
- package/templates/wizard-dist/assets/index-BhWM50Yu.css +1 -0
- package/templates/wizard-dist/assets/index-DYK3X1e5.js +34 -0
- package/templates/wizard-dist/assets/vendor-BwkXDkd3.js +17 -0
- package/templates/wizard-dist/index.html +20 -0
- package/templates/wizard.html +13 -1129
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Portable SliderMatrixScale — copy to src/components/SliderMatrixScale.tsx
|
|
5
|
+
* Requires: CustomSliderTrack.tsx, surveyUiScaleUtils.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import type { SliderMatrixQuestion, MatrixRowAnswers } from '@explorer02/cfm-survey-sdk';
|
|
10
|
+
import { tickColorFromIndex } from './surveyUiScaleUtils';
|
|
11
|
+
import { CustomSliderTrack } from './CustomSliderTrack';
|
|
12
|
+
|
|
13
|
+
type SliderMatrixScaleProps = {
|
|
14
|
+
question: SliderMatrixQuestion;
|
|
15
|
+
selectedValue?: MatrixRowAnswers;
|
|
16
|
+
onSelect: (value: MatrixRowAnswers) => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: SliderMatrixScaleProps) {
|
|
20
|
+
const {
|
|
21
|
+
statementRows,
|
|
22
|
+
scaleAnchorLabels: labels,
|
|
23
|
+
ticks,
|
|
24
|
+
enableInputBox,
|
|
25
|
+
displayValues,
|
|
26
|
+
hasNotApplicableOption: enableNotApplicable,
|
|
27
|
+
sliderStyle,
|
|
28
|
+
} = question;
|
|
29
|
+
|
|
30
|
+
const tickCount = ticks.count;
|
|
31
|
+
const tickValues = ticks.marks.map((mark, index) => ({
|
|
32
|
+
value: mark.tickPosition,
|
|
33
|
+
label: mark.tickLabel,
|
|
34
|
+
color: tickColorFromIndex(index, tickCount),
|
|
35
|
+
}));
|
|
36
|
+
const sliderType = sliderStyle === 'graphics' ? 'graphics' : undefined;
|
|
37
|
+
|
|
38
|
+
const handleSliderChange = (rowId: string, val: number | 'N/A') => {
|
|
39
|
+
onSelect({ ...selectedValue, [rowId]: val });
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const baseRow = statementRows[0];
|
|
43
|
+
const min = baseRow?.min ?? 0;
|
|
44
|
+
const max = baseRow?.max ?? 10;
|
|
45
|
+
const step = baseRow?.step ?? 1;
|
|
46
|
+
|
|
47
|
+
const marks: number[] = [];
|
|
48
|
+
if (tickCount && tickCount > 1) {
|
|
49
|
+
const markStep = (max - min) / (tickCount - 1);
|
|
50
|
+
for (let i = 0; i < tickCount; i++) {
|
|
51
|
+
marks.push(Number((min + i * markStep).toFixed(2)));
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
for (let i = min; i <= max; i += step) {
|
|
55
|
+
marks.push(Number(i.toFixed(2)));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const htmlStep = tickCount && tickCount > 1 ? (max - min) / (tickCount - 1) : step;
|
|
60
|
+
|
|
61
|
+
const renderHeaderRow = () => {
|
|
62
|
+
if ((!labels || labels.length === 0) && tickValues.length === 0) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return (
|
|
66
|
+
<div style={{
|
|
67
|
+
display: 'flex', width: '100%', marginBottom: '4px', alignItems: 'flex-end',
|
|
68
|
+
padding: '0 16px', boxSizing: 'border-box',
|
|
69
|
+
}}>
|
|
70
|
+
<div style={{ flex: '0 0 25%', paddingRight: '16px' }} />
|
|
71
|
+
|
|
72
|
+
<div style={{ flex: '1', position: 'relative' }}>
|
|
73
|
+
{labels && labels.length > 0 && (
|
|
74
|
+
<div style={{
|
|
75
|
+
position: 'relative', display: 'flex', justifyContent: 'space-between',
|
|
76
|
+
margin: '0 16px', marginBottom: '16px', color: '#111827', fontSize: '14px', fontWeight: 500,
|
|
77
|
+
}}>
|
|
78
|
+
{labels.map((lbl, idx) => (
|
|
79
|
+
<div key={idx} style={{ width: 0, display: 'flex', justifyContent: 'center' }}>
|
|
80
|
+
<span style={{ whiteSpace: 'nowrap' }}>{lbl}</span>
|
|
81
|
+
</div>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
<div style={{
|
|
87
|
+
position: 'relative', margin: '0 16px', marginBottom: '8px',
|
|
88
|
+
color: '#4b5563', fontSize: '14px', fontWeight: 400, height: '28px',
|
|
89
|
+
}}>
|
|
90
|
+
{tickValues.length > 0 ? (
|
|
91
|
+
tickValues.map((tv, idx) => {
|
|
92
|
+
const percentage = max !== min ? ((tv.value - min) / (max - min)) * 100 : 0;
|
|
93
|
+
const sliceWidth = tickValues.length > 1 ? 100 / (tickValues.length - 1) : 100;
|
|
94
|
+
return (
|
|
95
|
+
<div key={idx} style={{
|
|
96
|
+
position: 'absolute', left: `${percentage}%`, transform: 'translateX(-50%)',
|
|
97
|
+
width: `${sliceWidth}%`, display: 'flex', justifyContent: 'center',
|
|
98
|
+
}}>
|
|
99
|
+
{sliderType === 'graphics' ? (
|
|
100
|
+
<div style={{ wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2', fontWeight: 500, color: '#4b5563', fontSize: '13px' }}>
|
|
101
|
+
{tv.label}
|
|
102
|
+
</div>
|
|
103
|
+
) : (
|
|
104
|
+
<div style={{
|
|
105
|
+
backgroundColor: tv.color, color: 'white', padding: '4px 8px', borderRadius: '4px',
|
|
106
|
+
fontWeight: 600, wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2',
|
|
107
|
+
boxShadow: '0 1px 2px rgba(0,0,0,0.1)',
|
|
108
|
+
}}>
|
|
109
|
+
{tv.label}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
})
|
|
115
|
+
) : (
|
|
116
|
+
marks.map((m, idx) => {
|
|
117
|
+
const percentage = max !== min ? ((m - min) / (max - min)) * 100 : 0;
|
|
118
|
+
const sliceWidth = marks.length > 1 ? 100 / (marks.length - 1) : 100;
|
|
119
|
+
return (
|
|
120
|
+
<div key={idx} style={{
|
|
121
|
+
position: 'absolute', left: `${percentage}%`, transform: 'translateX(-50%)',
|
|
122
|
+
width: `${sliceWidth}%`, display: 'flex', justifyContent: 'center',
|
|
123
|
+
}}>
|
|
124
|
+
<span style={{ wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2' }}>{m}</span>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
})
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{enableInputBox && <div style={{ width: '80px', marginLeft: '16px' }} />}
|
|
133
|
+
{enableNotApplicable && (
|
|
134
|
+
<div style={{ width: '40px', marginLeft: '16px', textAlign: 'center', fontSize: '13px', fontWeight: 600, color: '#4b5563', paddingBottom: '8px' }}>
|
|
135
|
+
N/A
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
144
|
+
{renderHeaderRow()}
|
|
145
|
+
|
|
146
|
+
{statementRows.map(row => {
|
|
147
|
+
const rowVal = selectedValue[row.id];
|
|
148
|
+
const isNa = rowVal === 'N/A';
|
|
149
|
+
const val = typeof rowVal === 'number' ? rowVal : isNa ? row.min : (row.defaultValue ?? row.min);
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div key={row.id} style={{
|
|
153
|
+
display: 'flex', width: '100%', alignItems: 'center',
|
|
154
|
+
backgroundColor: '#f3f4f6', borderRadius: '12px', padding: '16px', boxSizing: 'border-box',
|
|
155
|
+
}}>
|
|
156
|
+
<div style={{ flex: '0 0 25%', paddingRight: '16px', fontSize: '15px', color: '#111827', wordWrap: 'break-word', display: 'flex', alignItems: 'center' }}>
|
|
157
|
+
{row.statementText !== 'Question' && row.statementText !== '' ? (
|
|
158
|
+
<span dangerouslySetInnerHTML={{ __html: row.statementText }} />
|
|
159
|
+
) : null}
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div style={{ flex: '1' }}>
|
|
163
|
+
<CustomSliderTrack
|
|
164
|
+
min={row.min}
|
|
165
|
+
max={row.max}
|
|
166
|
+
htmlStep={htmlStep}
|
|
167
|
+
value={val}
|
|
168
|
+
disabled={isNa}
|
|
169
|
+
displayValues={displayValues}
|
|
170
|
+
hasSelectedValue={typeof rowVal === 'number'}
|
|
171
|
+
sliderType={sliderType}
|
|
172
|
+
ticks={tickCount}
|
|
173
|
+
onChange={v => handleSliderChange(row.id, v)}
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
{enableInputBox && (
|
|
178
|
+
<div style={{
|
|
179
|
+
marginLeft: '16px', width: '80px', display: 'flex', alignItems: 'stretch',
|
|
180
|
+
border: '1px solid #d1d5db', borderRadius: '8px', overflow: 'hidden',
|
|
181
|
+
backgroundColor: isNa ? '#e5e7eb' : '#fff', boxSizing: 'border-box',
|
|
182
|
+
}}>
|
|
183
|
+
<input
|
|
184
|
+
type="text"
|
|
185
|
+
value={isNa ? '' : Math.round(Number(val))}
|
|
186
|
+
disabled={isNa}
|
|
187
|
+
onChange={e => {
|
|
188
|
+
if (e.target.value === '') return;
|
|
189
|
+
let num = Number(e.target.value);
|
|
190
|
+
if (!isNaN(num)) {
|
|
191
|
+
if (num < row.min) num = row.min;
|
|
192
|
+
if (num > row.max) num = row.max;
|
|
193
|
+
handleSliderChange(row.id, num);
|
|
194
|
+
}
|
|
195
|
+
}}
|
|
196
|
+
style={{
|
|
197
|
+
width: '50px', padding: '8px', border: 'none',
|
|
198
|
+
fontSize: '14px', outline: 'none', backgroundColor: 'transparent', textAlign: 'center',
|
|
199
|
+
}}
|
|
200
|
+
/>
|
|
201
|
+
<div style={{ display: 'flex', flexDirection: 'column', borderLeft: '1px solid #d1d5db', backgroundColor: '#f9fafb', width: '30px' }}>
|
|
202
|
+
<button
|
|
203
|
+
disabled={isNa}
|
|
204
|
+
onClick={() => {
|
|
205
|
+
const num = Number((Number(val) + htmlStep).toFixed(2));
|
|
206
|
+
if (num <= row.max) handleSliderChange(row.id, num);
|
|
207
|
+
}}
|
|
208
|
+
style={{ flex: 1, border: 'none', background: 'none', cursor: isNa ? 'not-allowed' : 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#4b5563' }}
|
|
209
|
+
>
|
|
210
|
+
<svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 5L5 1L9 5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /></svg>
|
|
211
|
+
</button>
|
|
212
|
+
<div style={{ height: '1px', backgroundColor: '#d1d5db', width: '100%' }} />
|
|
213
|
+
<button
|
|
214
|
+
disabled={isNa}
|
|
215
|
+
onClick={() => {
|
|
216
|
+
const num = Number((Number(val) - htmlStep).toFixed(2));
|
|
217
|
+
if (num >= row.min) handleSliderChange(row.id, num);
|
|
218
|
+
}}
|
|
219
|
+
style={{ flex: 1, border: 'none', background: 'none', cursor: isNa ? 'not-allowed' : 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#4b5563' }}
|
|
220
|
+
>
|
|
221
|
+
<svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 1L5 5L9 1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /></svg>
|
|
222
|
+
</button>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
|
|
227
|
+
{enableNotApplicable && (
|
|
228
|
+
<div style={{ marginLeft: '16px', width: '40px', display: 'flex', justifyContent: 'center' }}>
|
|
229
|
+
<input
|
|
230
|
+
type="checkbox"
|
|
231
|
+
checked={isNa}
|
|
232
|
+
onChange={e => {
|
|
233
|
+
if (e.target.checked) {
|
|
234
|
+
handleSliderChange(row.id, 'N/A');
|
|
235
|
+
} else {
|
|
236
|
+
handleSliderChange(row.id, row.defaultValue ?? row.min);
|
|
237
|
+
}
|
|
238
|
+
}}
|
|
239
|
+
style={{ width: '20px', height: '20px', cursor: 'pointer' }}
|
|
240
|
+
/>
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
})}
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import Header from '@/components/Header';
|
|
2
|
+
import ProgressBar from '@/components/ProgressBar';
|
|
3
|
+
|
|
4
|
+
type SurveyStickyChromeProps = {
|
|
5
|
+
progressPercentage: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sticky top chrome: brand header + progress bar.
|
|
10
|
+
* Keeps progress visible while scrolling long pages (e.g. last question below the fold).
|
|
11
|
+
*/
|
|
12
|
+
export default function SurveyStickyChrome({ progressPercentage }: SurveyStickyChromeProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
className="sticky top-0 z-30 shadow-[0_2px_8px_rgba(0,0,0,0.06)]"
|
|
16
|
+
style={{ background: 'var(--cfm-header-bg, #fff)' }}
|
|
17
|
+
>
|
|
18
|
+
<Header embedded />
|
|
19
|
+
<div className="mx-auto w-full max-w-4xl px-4 pb-5 pt-4 sm:px-6 lg:px-8">
|
|
20
|
+
<ProgressBar progressPercentage={progressPercentage} />
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable custom field values template — copy to src/lib/customFieldValues.ts
|
|
3
|
+
*
|
|
4
|
+
* Populate keys from survey logic rules where conditionEntity.entityType === 'CUSTOM_FIELD'.
|
|
5
|
+
* Values come from CRM/transaction context — NOT from survey respondent UI.
|
|
6
|
+
*
|
|
7
|
+
* See: docs/00-integration/custom-field-logic-and-navigation.md
|
|
8
|
+
* docs/00-integration/client-lib-folder.md
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { CustomFieldValueMap } from '@explorer02/cfm-survey-sdk';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* fieldType → client value shape:
|
|
15
|
+
*
|
|
16
|
+
* | fieldType | Value shape | Example |
|
|
17
|
+
* |------------------------|--------------------------------------|---------|
|
|
18
|
+
* | TEXT / TEXT_MULTI | string | 'abc' |
|
|
19
|
+
* | TEXTAREA | string | 'long text' |
|
|
20
|
+
* | NUMBER | number or numeric string | 2 or '0' |
|
|
21
|
+
* | PICKLIST | string (option id) | 'uuid-option-id' |
|
|
22
|
+
* | PICKLIST_MULTISELECT | string[] (option ids) | ['id-a', 'id-b'] |
|
|
23
|
+
* | DATETIME | epoch ms (number or numeric string) | 1781029800000 |
|
|
24
|
+
*
|
|
25
|
+
* Missing keys are treated as empty for logic evaluation.
|
|
26
|
+
*/
|
|
27
|
+
export const CUSTOM_FIELD_VALUES: CustomFieldValueMap = {
|
|
28
|
+
// Example — skip SLIDER page when CASE_SENSITIVE_TEXT equals 'abc' (integration-test survey):
|
|
29
|
+
// '_c_67199096d294be011a2b4773': 'abc',
|
|
30
|
+
|
|
31
|
+
// '_c_6a436c1244764c9d4a6ce2fc': 'var2', // TEXT
|
|
32
|
+
// '_c_6a436c3644764c9d4a6ce3f5': 2, // NUMBER
|
|
33
|
+
// '_c_6a436c9e44764c9d4a6ce717': 'option-id-uuid', // PICKLIST
|
|
34
|
+
// '_c_6a436cbb44764c9d4a6ce7e5': ['option-id'], // PICKLIST_MULTISELECT
|
|
35
|
+
// '_c_6a4386dc44208eb8b08a9056': 1781029800000, // DATETIME epoch ms
|
|
36
|
+
};
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# Survey UI Implementation Plan
|
|
2
2
|
|
|
3
3
|
> Copy this file to `./implementation_plan.md` in the project root. Fill every section before writing React code.
|
|
4
|
+
>
|
|
5
|
+
> Pipeline: [`agent-operating-contract.md`](../00-integration/agent-operating-contract.md) → [`agent-execution-flow.md`](../00-integration/agent-execution-flow.md)
|
|
4
6
|
|
|
5
7
|
## 0. Project Context
|
|
6
8
|
|
|
7
9
|
- **Framework:** [ ] Next.js App Router [ ] Vite [ ] CRA [ ] Existing app
|
|
8
10
|
- **Package import name:** (from `package.json` — `@explorer02/cfm-survey-sdk` or `@repo/sdk`)
|
|
9
11
|
- **instanceId:** (JWT — wired in SurveyPage)
|
|
10
|
-
- **Branding source:** (client prompt / mockup description
|
|
12
|
+
- **Branding source:** (client prompt / mockup description; optional wizard override via `survey-ui-config.json`)
|
|
11
13
|
|
|
12
14
|
## 1. Data Fetching & State
|
|
13
15
|
|
|
@@ -26,6 +28,7 @@
|
|
|
26
28
|
- [ ] `onAction` START / NEXT / PREVIOUS / SUBMIT wiring
|
|
27
29
|
- [ ] `onAction(SCROLL)` once on first scroll (mark as started)
|
|
28
30
|
- [ ] `onAction(RECORD_QUESTION_VIEW)` via IntersectionObserver on question wrappers
|
|
31
|
+
- [ ] `SurveyStickyChrome` (sticky Header + ProgressBar) on content phase — see `19-survey-sticky-chrome.md`
|
|
29
32
|
- [ ] `ProgressBar` bound to `state.progressPercentage` (path-scoped — see `00-integration/progress.md`)
|
|
30
33
|
- [ ] Back via `state.canGoBack`; primary label via `state.primaryButtonType`
|
|
31
34
|
- [ ] Read `00-integration/skip-logic-and-navigation.md` for skip/Back behavior
|
|
@@ -91,7 +94,34 @@ Fill from `00-integration/component-checklist.md`:
|
|
|
91
94
|
| `RankOrderScale.tsx` | `RANK_ORDER` | |
|
|
92
95
|
| `lib/heatmapCoords.ts` | `HEATMAP` | copy from `templates/heatmapCoords.ts` |
|
|
93
96
|
|
|
94
|
-
|
|
97
|
+
| `lib/surveyUiScaleUtils.ts` | NPS/matrix colors | copy from `templates/surveyUiScaleUtils.ts` |
|
|
98
|
+
| `lib/customFieldValues.ts` | CRM logic input | copy from `templates/customFieldValues.ts` when survey has `_c_…` ids |
|
|
99
|
+
| `lib/surveyUiScaleUtils.ts` | NPS/tick/matrix helpers | copy from `templates/surveyUiScaleUtils.ts` |
|
|
100
|
+
| `lib/surveyUiIcons.tsx` | CSAT emoji/star | copy from `templates/surveyUiIcons.tsx` |
|
|
101
|
+
|
|
102
|
+
## 3c. Matrix variant inventory (from fetched survey)
|
|
103
|
+
|
|
104
|
+
Read `00-integration/canonical-survey-fixtures.md`. For each matrix question note:
|
|
105
|
+
|
|
106
|
+
| questionId | type | displayStyle | statementLayout | transposeTable | selectionMode | Labels visible? |
|
|
107
|
+
|------------|------|--------------|-----------------|----------------|---------------|---------------|
|
|
108
|
+
| | | | | | | row + column/anchor |
|
|
109
|
+
|
|
110
|
+
## 3d. Custom field inventory (mandatory when `_c_…` ids found)
|
|
111
|
+
|
|
112
|
+
After fetch, scan `skipLogics`, `displayLogic`, `answerLogic`, and `endPages` for `entityType: 'CUSTOM_FIELD'`.
|
|
113
|
+
|
|
114
|
+
| `_c_` id | fieldType | used in (skip/display/answer/end) | logic field (e.g. CASE_SENSITIVE_TEXT) | CRM value | populated in `lib/customFieldValues.ts`? |
|
|
115
|
+
|----------|-----------|-----------------------------------|----------------------------------------|-----------|---------------------------------------------|
|
|
116
|
+
| | | | | | |
|
|
117
|
+
|
|
118
|
+
**Blocking gate:** if any row above is filled → copy `templates/customFieldValues.ts` → `src/lib/customFieldValues.ts` before Phase 5 BUILD.
|
|
119
|
+
|
|
120
|
+
Wire map in **both** places: `useSurveySDK({ options: { customFieldValues } })` and `<Question customFieldValues={...} />`.
|
|
121
|
+
|
|
122
|
+
See `00-integration/custom-field-logic-and-navigation.md` and `00-integration/client-lib-folder.md`.
|
|
123
|
+
|
|
124
|
+
## 3e. Logic inventory (from fetched survey)
|
|
95
125
|
|
|
96
126
|
Scan `survey.pages` for logic flags. Fill after fetch:
|
|
97
127
|
|
|
@@ -99,9 +129,7 @@ Scan `survey.pages` for logic flags. Fill after fetch:
|
|
|
99
129
|
|------------|------------|--------------|----------------------------------|---------------------------|
|
|
100
130
|
| | | | | |
|
|
101
131
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
## 3d. Per-Type SDK Integration (from fetched survey)
|
|
132
|
+
## 3f. Per-Type SDK Integration (from fetched survey)
|
|
105
133
|
|
|
106
134
|
Fill from `00-integration/question-type-sdk-matrix.md`:
|
|
107
135
|
|
|
@@ -119,7 +147,7 @@ Fill from `00-integration/question-type-sdk-matrix.md`:
|
|
|
119
147
|
| LikertMatrixScale | | `06-likert-matrix-scale.md` |
|
|
120
148
|
| SliderMatrixScale | | `07-slider-matrix-scale.md` |
|
|
121
149
|
| FileUploadScale | | `08-file-upload-scale.md` |
|
|
122
|
-
| Header / Footer /
|
|
150
|
+
| Header / Footer / SurveyStickyChrome / LanguageSelector | | `10`–`12`, `19` |
|
|
123
151
|
|
|
124
152
|
## 3e. Interaction dependencies (from survey inventory)
|
|
125
153
|
|
|
@@ -139,7 +167,7 @@ Inventory required config fields per type:
|
|
|
139
167
|
| MCQ | `selectionMode`, `minSelections`, `maxSelections`, `defaultOptionIds`, answer-logic on options |
|
|
140
168
|
| RANK_ORDER | `interactionMode`, `optionDisplay`, `requireRankAll`, `shuffleOptions` |
|
|
141
169
|
| CFM/CSAT/RATING | `gridLayout`, `displayStyle`, `selectionMode`, `hasNotApplicableOption`, `statementLayout` |
|
|
142
|
-
| SLIDER_MATRIX | `enableInputBox`, `
|
|
170
|
+
| SLIDER_MATRIX | `enableInputBox`, `hasNotApplicableOption`, `sliderStyle`, `statementRows`, `ticks` |
|
|
143
171
|
| FILE_UPLOAD | `maxFileCount`, `fileSizeLimit`, `fileSizeLimitType`, upload API configured? |
|
|
144
172
|
| HEATMAP | `maxClicksAllowed`, image URL |
|
|
145
173
|
|
|
@@ -149,8 +177,27 @@ Inventory required config fields per type:
|
|
|
149
177
|
|
|
150
178
|
## 5. Theme, Brand & Logo
|
|
151
179
|
|
|
152
|
-
- [ ] CSS-
|
|
180
|
+
- [ ] Logo: text/CSS (`data-cfm-logo-text`) **or** local `public/` image (`data-cfm-logo`) — see `wizard-preview-build-guide.md`; no external URLs
|
|
153
181
|
- [ ] Primary / hover colors from client prompt
|
|
182
|
+
- [ ] `src/styles/survey-theme.css` with `--cfm-*` CSS variables (full template set)
|
|
183
|
+
- [ ] **Wizard preview contract:** `var(--cfm-*)` in chrome/scales + `data-cfm-*` on editable text/logo — see `00-integration/wizard-preview-build-guide.md`
|
|
184
|
+
- [ ] **Wizard per-type styling:** `selectionStyles.ts`, `labelStyles.ts`, per-type tokens — see `00-integration/wizard-question-type-styling.md`
|
|
185
|
+
- [ ] **Draft** `./survey-ui-config.json` in sync with theme tokens (seeds optional wizard in Phase 6b)
|
|
186
|
+
- [ ] `previewBridge.ts` wired for wizard live preview (see `ui-customization-wizard.md`)
|
|
187
|
+
- [ ] If `survey-ui-config.json` exists: apply per `apply-ui-config.md` (Phase 5 draft or Phase 6c final)
|
|
188
|
+
|
|
189
|
+
## 5b. Wizard handoff (Phase 6b–6c — when client uses wizard)
|
|
190
|
+
|
|
191
|
+
See `wizard-config-handoff.md`.
|
|
192
|
+
|
|
193
|
+
- [ ] `customize-ui` uploaded component previews to S3
|
|
194
|
+
- [ ] Deployed wizard link sent to client: `http://43.204.26.213:3000/wizard-app/index.html?session=…&secret=…` (from CLI output)
|
|
195
|
+
- [ ] CLI `customize-ui` exited 0 — waited for client submit
|
|
196
|
+
- [ ] Read `survey-ui-config.diff.md` — paths changed: ___
|
|
197
|
+
- [ ] Applied from `survey-ui-config.final.json` (AWS) or `survey-ui-config.json` (local)
|
|
198
|
+
- [ ] Every diff path applied (count matches)
|
|
199
|
+
- [ ] `confirm-ui-config` run (AWS)
|
|
200
|
+
- [ ] Verify + build re-run after apply
|
|
154
201
|
|
|
155
202
|
## 6. Submit & Thank You
|
|
156
203
|
|
|
@@ -165,7 +212,7 @@ Inventory required config fields per type:
|
|
|
165
212
|
- [ ] `bash templates/verify-agent-build.sh` passes
|
|
166
213
|
- [ ] `grep -r "not yet implemented"` → zero matches
|
|
167
214
|
|
|
168
|
-
## 8. AWS Deploy Readiness (fill if client prompt mentions AWS / CloudFront / S3 / EC2 / Sprinklr)
|
|
215
|
+
## 8. AWS Deploy Readiness (fill if client prompt mentions AWS / CloudFront / S3 / EC2 / Sprinklr, or wizard deploy.target is aws)
|
|
169
216
|
|
|
170
217
|
- [ ] Next.js `output: "export"` + `assetPrefix: "./"` configured (see `setup.md#nextjs-aws-static-export`)
|
|
171
218
|
- [ ] `images.unoptimized: true` in `next.config.js`
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Label item styling for matrix / slider tick labels (wizard Question Types step).
|
|
3
|
+
* Copy to `src/lib/surveyUi/labelStyles.ts`.
|
|
4
|
+
*/
|
|
5
|
+
import type { CSSProperties } from 'react';
|
|
6
|
+
|
|
7
|
+
export function hintLabelStyle(): CSSProperties {
|
|
8
|
+
return {
|
|
9
|
+
color: 'var(--cfm-hint-label-color, #6b7280)',
|
|
10
|
+
backgroundColor: 'var(--cfm-hint-label-bg, transparent)',
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function matrixColumnLabelStyle(): CSSProperties {
|
|
15
|
+
return {
|
|
16
|
+
color: 'var(--cfm-matrix-column-label-color, #4b5563)',
|
|
17
|
+
backgroundColor: 'var(--cfm-matrix-column-label-bg, transparent)',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function csatColumnLabelStyle(): CSSProperties {
|
|
22
|
+
return {
|
|
23
|
+
color: 'var(--cfm-csat-column-label-color, #4b5563)',
|
|
24
|
+
backgroundColor: 'var(--cfm-csat-column-label-bg, transparent)',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function sliderTickLabelStyle(): CSSProperties {
|
|
29
|
+
return {
|
|
30
|
+
color: 'var(--cfm-slider-tick-label-color, #4b5563)',
|
|
31
|
+
backgroundColor: 'var(--cfm-slider-tick-label-bg, transparent)',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable selection styling helpers for wizard live preview + apply-ui-config.
|
|
3
|
+
* Copy to `src/lib/surveyUi/selectionStyles.ts` when building agent projects.
|
|
4
|
+
*
|
|
5
|
+
* Question card theme tokens (wizard → Theme & Colors → Question card):
|
|
6
|
+
* --cfm-matrix-selected = cell selected color (annulus fill between ring and dot)
|
|
7
|
+
* --cfm-input-focus-ring = focus ring (outer border + inner dot)
|
|
8
|
+
*/
|
|
9
|
+
import type { CSSProperties } from 'react';
|
|
10
|
+
|
|
11
|
+
export const cellSelectedVar = 'var(--cfm-matrix-selected, var(--cfm-primary))';
|
|
12
|
+
export const focusRingVar = 'var(--cfm-input-focus-ring, var(--cfm-primary))';
|
|
13
|
+
|
|
14
|
+
const unselectedRing = '#d1d5db';
|
|
15
|
+
const unselectedDot = '#d1d5db';
|
|
16
|
+
|
|
17
|
+
/** NPS / large scale radio — outer ring border + annulus fill. */
|
|
18
|
+
export function scaleRadioRingStyle(
|
|
19
|
+
isSelected: boolean,
|
|
20
|
+
opts: { pill?: boolean; size?: string } = {},
|
|
21
|
+
): CSSProperties {
|
|
22
|
+
const { pill = false, size = 'var(--cfm-nps-cell-size, 32px)' } = opts;
|
|
23
|
+
return {
|
|
24
|
+
display: 'flex',
|
|
25
|
+
alignItems: 'center',
|
|
26
|
+
justifyContent: 'center',
|
|
27
|
+
width: size,
|
|
28
|
+
height: size,
|
|
29
|
+
borderRadius: pill ? '9999px' : '50%',
|
|
30
|
+
border: `2px solid ${isSelected ? focusRingVar : unselectedRing}`,
|
|
31
|
+
backgroundColor: isSelected ? cellSelectedVar : '#fff',
|
|
32
|
+
transition: 'all 0.15s',
|
|
33
|
+
boxSizing: 'border-box',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Inner dot — focus ring color when selected. */
|
|
38
|
+
export function scaleRadioDotStyle(
|
|
39
|
+
isSelected: boolean,
|
|
40
|
+
opts: { pill?: boolean } = {},
|
|
41
|
+
): CSSProperties {
|
|
42
|
+
const { pill = false } = opts;
|
|
43
|
+
return {
|
|
44
|
+
borderRadius: '50%',
|
|
45
|
+
width: pill ? '40%' : '33%',
|
|
46
|
+
height: pill ? '40%' : '33%',
|
|
47
|
+
backgroundColor: isSelected ? focusRingVar : unselectedDot,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Compact matrix radio (CSAT / Likert / CFM grid). */
|
|
52
|
+
export function matrixRadioRingStyle(isSelected: boolean, size = '18px'): CSSProperties {
|
|
53
|
+
return {
|
|
54
|
+
display: 'flex',
|
|
55
|
+
width: size,
|
|
56
|
+
height: size,
|
|
57
|
+
alignItems: 'center',
|
|
58
|
+
justifyContent: 'center',
|
|
59
|
+
borderRadius: '50%',
|
|
60
|
+
margin: '0 auto',
|
|
61
|
+
border: `2px solid ${isSelected ? focusRingVar : unselectedRing}`,
|
|
62
|
+
backgroundColor: isSelected ? cellSelectedVar : '#fff',
|
|
63
|
+
transition: 'all 0.15s',
|
|
64
|
+
boxSizing: 'border-box',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function matrixRadioDotStyle(isSelected: boolean, size = '8px'): CSSProperties {
|
|
69
|
+
return {
|
|
70
|
+
width: size,
|
|
71
|
+
height: size,
|
|
72
|
+
borderRadius: '50%',
|
|
73
|
+
backgroundColor: isSelected ? focusRingVar : 'transparent',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Numbered / solid scale cell when selected. */
|
|
78
|
+
export function scaleCellSelectedStyle(isSelected: boolean): CSSProperties {
|
|
79
|
+
return isSelected
|
|
80
|
+
? {
|
|
81
|
+
border: `2px solid ${focusRingVar}`,
|
|
82
|
+
backgroundColor: cellSelectedVar,
|
|
83
|
+
color: '#fff',
|
|
84
|
+
}
|
|
85
|
+
: {};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function unselectedOpacityStyle(): CSSProperties {
|
|
89
|
+
return { opacity: 'var(--cfm-csat-unselected-opacity, 0.5)' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function emojiSizeStyle(): CSSProperties {
|
|
93
|
+
return {
|
|
94
|
+
fontSize: 'var(--cfm-csat-emoji-size, 28px)',
|
|
95
|
+
lineHeight: 1,
|
|
96
|
+
display: 'inline-flex',
|
|
97
|
+
alignItems: 'center',
|
|
98
|
+
justifyContent: 'center',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
FaFaceGrin,
|
|
13
13
|
FaFaceGrinWide,
|
|
14
14
|
FaFaceGrinStars,
|
|
15
|
+
FaStar,
|
|
15
16
|
} from 'react-icons/fa6';
|
|
17
|
+
import { FaRegStar } from 'react-icons/fa';
|
|
16
18
|
|
|
17
19
|
const EMOJI_COLORS = [
|
|
18
20
|
'#dc2626', '#e04832', '#ea580c', '#f59e0b', '#eab308',
|
|
@@ -50,3 +52,12 @@ export function getEmojiForIndex(index: number, total: number): React.ReactNode
|
|
|
50
52
|
const key = Math.min(11, Math.max(1, Math.round(percent * 10) + 1));
|
|
51
53
|
return CsatEmojiSet[key];
|
|
52
54
|
}
|
|
55
|
+
|
|
56
|
+
export const CsatStarIcons: { filled: React.ReactNode; empty: React.ReactNode } = {
|
|
57
|
+
filled: React.createElement(FaStar as React.ElementType, {
|
|
58
|
+
style: { color: '#e20074', fontSize: '28px' },
|
|
59
|
+
}),
|
|
60
|
+
empty: React.createElement(FaRegStar as React.ElementType, {
|
|
61
|
+
style: { color: '#d1d5db', fontSize: '28px', stroke: '#d1d5db', strokeWidth: '16px' },
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable scale utilities — copy to src/lib/surveyUiScaleUtils.ts or co-locate with components.
|
|
3
|
+
*/
|
|
4
|
+
import type { NpsScalePoint, ScaleColumn } from '@explorer02/cfm-survey-sdk';
|
|
5
|
+
|
|
6
|
+
export const SCALE_COLORS: readonly string[] = [
|
|
7
|
+
'#e2001a', '#e4251b', '#ec610a', '#f18b00', '#f7b200', '#fcd900',
|
|
8
|
+
'#d2dc00', '#9fca00', '#6bb300', '#339a00', '#008000',
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
export function getAccentColor(value: number): string {
|
|
12
|
+
return SCALE_COLORS[value] ?? '#e20074';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function tickColorFromIndex(index: number, total: number): string {
|
|
16
|
+
if (total <= 1) return '#e20074';
|
|
17
|
+
const hue = Math.round((index / (total - 1)) * 120);
|
|
18
|
+
return `hsl(${hue}, 90%, 40%)`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function nonNaScaleColumns(columns: ScaleColumn[]): ScaleColumn[] {
|
|
22
|
+
return columns.filter(col => col.id !== 'na');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function starValueFromOption(option: NpsScalePoint, index: number): number {
|
|
26
|
+
const numeric = Number(option.optionLabel);
|
|
27
|
+
return Number.isFinite(numeric) && option.optionLabel.trim() !== '' ? numeric : index;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function npsValueFromOption(option: NpsScalePoint, index: number): number {
|
|
31
|
+
const numeric = Number(option.optionLabel);
|
|
32
|
+
return Number.isFinite(numeric) && option.optionLabel.trim() !== '' ? numeric : index;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Matches SDK matrixColumnStoredValue — scale-matrix answers store column ids. */
|
|
36
|
+
export function matrixColumnStoredValue(column: { id: string; label: string }): string | null {
|
|
37
|
+
return column.id === 'na' ? null : column.id;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @deprecated Use matrixColumnStoredValue */
|
|
41
|
+
export function columnSubmitValue(column: { id: string; label: string }): string | null {
|
|
42
|
+
return matrixColumnStoredValue(column);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function columnAccentColor(column: { id: string; label: string }): string | undefined {
|
|
46
|
+
const numeric = Number(column.label);
|
|
47
|
+
if (Number.isFinite(numeric) && column.label.trim() !== '') {
|
|
48
|
+
return getAccentColor(numeric);
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|