@explorer02/cfm-survey-sdk 0.1.9 → 0.2.1
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/package.json +1 -1
- package/postinstall.js +5 -0
- package/templates/AGENT.md +4 -2
- package/templates/docs/00-integration/component-checklist.md +61 -0
- package/templates/docs/00-integration/constraints.md +17 -0
- package/templates/docs/01-components/01-survey-page.md +2 -0
- package/templates/docs/01-components/02-question.md +43 -56
- package/templates/docs/01-components/03-rating-scale.md +2 -0
- package/templates/docs/01-components/04-csat-scale.md +3 -1
- package/templates/docs/01-components/05-csat-matrix-scale.md +2 -0
- package/templates/docs/01-components/06-likert-matrix-scale.md +2 -0
- package/templates/docs/01-components/07-slider-matrix-scale.md +2 -0
- package/templates/docs/01-components/08-file-upload-scale.md +2 -0
- package/templates/docs/01-components/09-custom-slider-track.md +2 -0
- package/templates/docs/01-components/10-header-footer.md +3 -1
- package/templates/docs/01-components/11-progress-bar.md +2 -0
- package/templates/docs/01-components/12-language-selector.md +2 -0
- package/templates/docs/01-components/13-matrix-dropdown.md +2 -0
- package/templates/docs/01-components/README.md +4 -2
- package/templates/docs/02-reference/config-field-index.md +158 -0
- package/templates/docs/02-reference/routing-table.md +16 -16
- package/templates/docs/03-ui-specs/00-question-shell.md +102 -0
- package/templates/docs/03-ui-specs/01-rating.md +80 -0
- package/templates/docs/03-ui-specs/02-radio.md +57 -0
- package/templates/docs/03-ui-specs/03-text.md +51 -0
- package/templates/docs/03-ui-specs/04-csat.md +57 -0
- package/templates/docs/03-ui-specs/05-rating-scale.md +49 -0
- package/templates/docs/03-ui-specs/06-slider.md +42 -0
- package/templates/docs/03-ui-specs/07-matrix-cfm.md +66 -0
- package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +65 -0
- package/templates/docs/03-ui-specs/09-slider-matrix.md +61 -0
- package/templates/docs/03-ui-specs/10-file-upload.md +54 -0
- package/templates/docs/03-ui-specs/11-text-and-media.md +49 -0
- package/templates/docs/03-ui-specs/12-survey-chrome.md +60 -0
- package/templates/docs/03-ui-specs/README.md +80 -0
- package/templates/docs/03-ui-specs/shared/custom-slider-track.md +52 -0
- package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +38 -0
- package/templates/docs/MANIFEST.json +27 -1
- package/templates/docs/index.md +28 -13
- package/templates/docs/templates/Question.tsx +243 -0
- package/templates/docs/templates/implementation_plan.md +40 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CANONICAL QUESTION DISPATCHER TEMPLATE
|
|
3
|
+
*
|
|
4
|
+
* Agents: copy this file to your project's components folder (e.g. src/components/Question.tsx).
|
|
5
|
+
* Then create EVERY imported scale component listed below — see:
|
|
6
|
+
* docs/00-integration/component-checklist.md
|
|
7
|
+
* docs/03-ui-specs/00-question-shell.md
|
|
8
|
+
*
|
|
9
|
+
* RULES:
|
|
10
|
+
* - Do NOT add a default/fallback branch that renders "not yet implemented" or similar stub text.
|
|
11
|
+
* - matrix MUST route by question.subType — type alone is NOT enough.
|
|
12
|
+
* - If a question type appears in the fetched survey, its branch MUST render a real interactive component.
|
|
13
|
+
*
|
|
14
|
+
* Replace @explorer02/cfm-survey-sdk with your package.json import name if different.
|
|
15
|
+
*/
|
|
16
|
+
'use client';
|
|
17
|
+
|
|
18
|
+
import React from 'react';
|
|
19
|
+
import type { SurveyQuestion, AnswerValue } from '@explorer02/cfm-survey-sdk';
|
|
20
|
+
import { CsatScale } from './CsatScale';
|
|
21
|
+
import { CsatMatrixScale } from './CsatMatrixScale';
|
|
22
|
+
import { SliderMatrixScale } from './SliderMatrixScale';
|
|
23
|
+
import { LikertMatrixScale } from './LikertMatrixScale';
|
|
24
|
+
import { FileUploadScale } from './FileUploadScale';
|
|
25
|
+
import RatingScale from './RatingScale';
|
|
26
|
+
// rating_scale / slider gaps — create these if survey contains those types:
|
|
27
|
+
// import { CustomSliderTrack } from './CustomSliderTrack';
|
|
28
|
+
|
|
29
|
+
type QuestionProps = {
|
|
30
|
+
question: SurveyQuestion;
|
|
31
|
+
selectedValue?: AnswerValue;
|
|
32
|
+
validationError?: string;
|
|
33
|
+
onSelect: (value: AnswerValue) => void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default function Question({ question, selectedValue, validationError, onSelect }: QuestionProps) {
|
|
37
|
+
return (
|
|
38
|
+
<section id={question.id} className="space-y-4 rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
|
|
39
|
+
<h2 className="text-lg font-medium leading-relaxed text-gray-900">
|
|
40
|
+
<span dangerouslySetInnerHTML={{ __html: question.text }} />
|
|
41
|
+
{question.required && <span className="text-red-500 ml-1">*</span>}
|
|
42
|
+
</h2>
|
|
43
|
+
{question.description && (
|
|
44
|
+
<div
|
|
45
|
+
className="text-sm text-gray-600 mt-2 leading-relaxed"
|
|
46
|
+
dangerouslySetInnerHTML={{ __html: question.description }}
|
|
47
|
+
/>
|
|
48
|
+
)}
|
|
49
|
+
|
|
50
|
+
{question.type === 'rating' && (
|
|
51
|
+
<div className="space-y-3">
|
|
52
|
+
{(question.minLabel || question.midLabel || question.maxLabel) && (
|
|
53
|
+
<div className="relative w-full h-6 text-xs font-semibold text-gray-500">
|
|
54
|
+
<span
|
|
55
|
+
className="absolute left-0 top-0 max-w-[30%] text-left leading-tight"
|
|
56
|
+
dangerouslySetInnerHTML={{ __html: question.minLabel ?? '' }}
|
|
57
|
+
/>
|
|
58
|
+
{question.midLabel && (
|
|
59
|
+
<span
|
|
60
|
+
className="absolute top-0 -translate-x-1/2 text-center max-w-[40%] leading-tight"
|
|
61
|
+
style={{
|
|
62
|
+
left: `${question.midLabelIndex !== undefined && question.options.length > 1
|
|
63
|
+
? (question.midLabelIndex / (question.options.length - 1)) * 100
|
|
64
|
+
: 50
|
|
65
|
+
}%`,
|
|
66
|
+
}}
|
|
67
|
+
dangerouslySetInnerHTML={{ __html: question.midLabel }}
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
70
|
+
<span
|
|
71
|
+
className="absolute right-0 top-0 max-w-[30%] text-right leading-tight"
|
|
72
|
+
dangerouslySetInnerHTML={{ __html: question.maxLabel ?? '' }}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
<RatingScale
|
|
77
|
+
questionId={question.id}
|
|
78
|
+
options={question.options}
|
|
79
|
+
selectedValue={selectedValue}
|
|
80
|
+
onSelect={onSelect}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
{question.type === 'radio' && (
|
|
86
|
+
<div className="space-y-3">
|
|
87
|
+
{question.options.map(option => {
|
|
88
|
+
const isSelected = selectedValue === option.value;
|
|
89
|
+
return (
|
|
90
|
+
<label
|
|
91
|
+
key={String(option.value)}
|
|
92
|
+
className={`flex items-center gap-4 rounded-lg border py-4 px-5 cursor-pointer transition-all ${isSelected ? 'border-[#e20074] bg-[#fdf2f8]' : 'border-[#e5e5e5] bg-white hover:bg-gray-50/50'
|
|
93
|
+
}`}
|
|
94
|
+
>
|
|
95
|
+
<input
|
|
96
|
+
type="radio"
|
|
97
|
+
name={question.id}
|
|
98
|
+
value={option.value === null ? '' : option.value}
|
|
99
|
+
checked={isSelected}
|
|
100
|
+
onChange={() => onSelect(option.value)}
|
|
101
|
+
className="sr-only"
|
|
102
|
+
/>
|
|
103
|
+
<div
|
|
104
|
+
className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full border transition-all ${isSelected ? 'border-2 border-[#e20074] bg-white' : 'border-gray-400 bg-white'
|
|
105
|
+
}`}
|
|
106
|
+
>
|
|
107
|
+
{isSelected && <div className="h-2.5 w-2.5 rounded-full bg-[#e20074]" />}
|
|
108
|
+
</div>
|
|
109
|
+
<span
|
|
110
|
+
className="text-[15px] font-medium text-gray-900 leading-tight"
|
|
111
|
+
dangerouslySetInnerHTML={{ __html: option.label }}
|
|
112
|
+
/>
|
|
113
|
+
</label>
|
|
114
|
+
);
|
|
115
|
+
})}
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
{question.type === 'text' && (
|
|
120
|
+
<div className="space-y-1.5">
|
|
121
|
+
<textarea
|
|
122
|
+
rows={4}
|
|
123
|
+
value={(selectedValue as string | number | null) ?? ''}
|
|
124
|
+
onChange={e => onSelect(e.target.value)}
|
|
125
|
+
placeholder={question.placeholder || 'Type your response here...'}
|
|
126
|
+
className="w-full rounded-[4px] border border-gray-300 p-4 text-sm outline-none transition-colors focus:border-[#e20074] focus:ring-1 focus:ring-[#e20074]"
|
|
127
|
+
/>
|
|
128
|
+
<div className="text-right text-xs text-gray-500 pr-1">
|
|
129
|
+
{Math.max(
|
|
130
|
+
0,
|
|
131
|
+
(question.maxCharacterCount ?? 1000) - (typeof selectedValue === 'string' ? selectedValue.length : 0)
|
|
132
|
+
)}{' '}
|
|
133
|
+
Zeichen verbleiben.
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
{question.type === 'csat' && (
|
|
139
|
+
<div className="space-y-3">
|
|
140
|
+
{(question.minLabel || question.maxLabel) && (
|
|
141
|
+
<div className="flex w-full justify-between text-xs font-semibold text-gray-500">
|
|
142
|
+
{question.minLabel && <span dangerouslySetInnerHTML={{ __html: question.minLabel }} />}
|
|
143
|
+
{question.maxLabel && <span dangerouslySetInnerHTML={{ __html: question.maxLabel }} />}
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
<CsatScale
|
|
147
|
+
question={question}
|
|
148
|
+
selectedValue={selectedValue as string | number | null}
|
|
149
|
+
onSelect={onSelect}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{/* matrix: subType is REQUIRED — never route on type alone */}
|
|
155
|
+
{question.type === 'matrix' && (question.subType === 'CSAT_MATRIX' || question.subType === 'RATING_MATRIX') && (
|
|
156
|
+
<div className="space-y-3">
|
|
157
|
+
<CsatMatrixScale
|
|
158
|
+
question={question}
|
|
159
|
+
selectedValue={typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue) ? selectedValue : {}}
|
|
160
|
+
onSelect={onSelect}
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
|
|
165
|
+
{question.type === 'matrix' && question.subType === 'CFM_MATRIX' && (
|
|
166
|
+
<div className="space-y-3">
|
|
167
|
+
<LikertMatrixScale
|
|
168
|
+
question={question}
|
|
169
|
+
selectedValue={typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue) ? selectedValue : {}}
|
|
170
|
+
onSelect={onSelect}
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
{question.type === 'slider_matrix' && (
|
|
176
|
+
<div className="space-y-3">
|
|
177
|
+
<SliderMatrixScale
|
|
178
|
+
question={question}
|
|
179
|
+
selectedValue={typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue) ? selectedValue : {}}
|
|
180
|
+
onSelect={onSelect}
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
{question.type === 'file_upload' && (
|
|
186
|
+
<div className="space-y-3">
|
|
187
|
+
<FileUploadScale
|
|
188
|
+
question={question}
|
|
189
|
+
selectedValue={selectedValue}
|
|
190
|
+
onSelect={onSelect}
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
|
|
195
|
+
{question.type === 'text_and_media' && question.mediaUrl && (
|
|
196
|
+
<div className={`mt-4 flex w-full ${
|
|
197
|
+
question.mediaAlignment === 'TOP_CENTER' || question.mediaAlignment === 'BOTTOM_CENTER'
|
|
198
|
+
? 'justify-center'
|
|
199
|
+
: question.mediaAlignment === 'TOP_RIGHT' || question.mediaAlignment === 'BOTTOM_RIGHT'
|
|
200
|
+
? 'justify-end'
|
|
201
|
+
: 'justify-start'
|
|
202
|
+
}`}>
|
|
203
|
+
<div
|
|
204
|
+
style={{ width: question.mediaSize ? `${question.mediaSize}%` : '100%' }}
|
|
205
|
+
className="flex flex-col gap-2"
|
|
206
|
+
>
|
|
207
|
+
{question.mediaMimeType?.startsWith('video/') || question.mediaMimeType === 'VIDEO' ? (
|
|
208
|
+
<video controls src={question.mediaUrl} className="max-w-full rounded-md shadow-sm" />
|
|
209
|
+
) : (
|
|
210
|
+
<img src={question.mediaUrl} alt={question.mediaTitle || ''} className="max-w-full rounded-md shadow-sm" />
|
|
211
|
+
)}
|
|
212
|
+
{question.mediaTitle && (
|
|
213
|
+
<p className="text-sm text-gray-500 text-center">{question.mediaTitle}</p>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
{/* rating_scale / slider: implement per 03-ui-specs/05-rating-scale.md and 06-slider.md if present in survey */}
|
|
220
|
+
{question.type === 'rating_scale' && (
|
|
221
|
+
<div className="space-y-3">
|
|
222
|
+
<CsatScale
|
|
223
|
+
question={{ ...question, type: 'csat', buttonType: question.scaleStyle === 'star' ? 'star' : 'emoji' }}
|
|
224
|
+
selectedValue={selectedValue as number | null}
|
|
225
|
+
onSelect={onSelect}
|
|
226
|
+
/>
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
{question.type === 'slider' && (
|
|
231
|
+
<div className="space-y-3">
|
|
232
|
+
{/* Wire CustomSliderTrack — min/max/step from question; see 03-ui-specs/06-slider.md */}
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
|
|
236
|
+
{validationError && (
|
|
237
|
+
<div className="mt-4 rounded-[4px] border border-[#333333] bg-[#d9d9d9] py-3.5 px-4 text-left">
|
|
238
|
+
<p className="text-[13px] sm:text-sm font-bold text-gray-900 leading-tight">{validationError}</p>
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
</section>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
@@ -42,6 +42,20 @@ Exhaustive switch — check each:
|
|
|
42
42
|
- [ ] `dangerouslySetInnerHTML` for `question.text` / `description`
|
|
43
43
|
- [ ] `validationError` banner per question
|
|
44
44
|
- [ ] `id={question.id}` on every question wrapper
|
|
45
|
+
- [ ] Copied from `docs/templates/Question.tsx` — no stub/default fallback branches
|
|
46
|
+
|
|
47
|
+
## 3b. Required Component Files (from fetched survey)
|
|
48
|
+
|
|
49
|
+
Fill from `00-integration/component-checklist.md` — every file below that applies to your survey **must exist** before Phase 5:
|
|
50
|
+
|
|
51
|
+
| Component file | Required if survey contains | Created? |
|
|
52
|
+
|----------------|----------------------------|----------|
|
|
53
|
+
| `LikertMatrixScale.tsx` | `matrix` + `CFM_MATRIX` | |
|
|
54
|
+
| `CsatMatrixScale.tsx` | `matrix` + `CSAT_MATRIX` or `RATING_MATRIX` | |
|
|
55
|
+
| `SliderMatrixScale.tsx` | `slider_matrix` | **blocking** |
|
|
56
|
+
| `FileUploadScale.tsx` | `file_upload` | **blocking** |
|
|
57
|
+
| `MatrixDropdown.tsx` | matrix with dropdown/selectbox modes | |
|
|
58
|
+
| `CustomSliderTrack.tsx` | `slider_matrix` or CSAT matrix `graphics` | |
|
|
45
59
|
|
|
46
60
|
## 4. Component Architecture Checklist
|
|
47
61
|
|
|
@@ -59,6 +73,20 @@ Exhaustive switch — check each:
|
|
|
59
73
|
| LanguageSelector | | `01-components/12-language-selector.md` | |
|
|
60
74
|
| MatrixDropdown | | `01-components/13-matrix-dropdown.md` | |
|
|
61
75
|
|
|
76
|
+
## 4b. Per-Question Config & UI Coverage
|
|
77
|
+
|
|
78
|
+
Fill one row per question in the fetched survey:
|
|
79
|
+
|
|
80
|
+
| questionId | type | subType | configs detected | uiSpec read | ref component | branches implemented |
|
|
81
|
+
|------------|------|---------|------------------|-------------|---------------|---------------------|
|
|
82
|
+
| | | | | | | |
|
|
83
|
+
|
|
84
|
+
- **configs detected:** non-default fields from `02-reference/config-field-index.md` (e.g. `matrixFormat: carousel`, `buttonType: emoji`)
|
|
85
|
+
- **uiSpec read:** path from `MANIFEST.json` `uiSpecs` (matrix: use `matrix_{subType}`)
|
|
86
|
+
- **ref component:** monorepo: `apps/client/src/components/{Component}.tsx`; npm: implement from ui-spec only
|
|
87
|
+
- **branches implemented:** list decision-tree branches from the ui-spec (e.g. dropdown vs grid vs carousel)
|
|
88
|
+
- **blocking types:** `matrix`, `slider_matrix`, `file_upload` must show interactive UI, not placeholder text
|
|
89
|
+
|
|
62
90
|
## 5. Theme, Brand & Logo
|
|
63
91
|
|
|
64
92
|
- [ ] CSS-only logo (no `<img>`, no `public/` uploads)
|
|
@@ -76,3 +104,15 @@ Exhaustive switch — check each:
|
|
|
76
104
|
- [ ] No imports from `.../src/` internal paths
|
|
77
105
|
- [ ] All mutations via `onAction` — no direct state mutation
|
|
78
106
|
- [ ] `npm run build` passes
|
|
107
|
+
|
|
108
|
+
## 7b. Stub-Free Verification
|
|
109
|
+
|
|
110
|
+
Per question type in the fetched survey, confirm interactive UI renders (not grey placeholder text):
|
|
111
|
+
|
|
112
|
+
| type | subType | Renders real component? | No "not yet implemented" text? |
|
|
113
|
+
|------|---------|---------------------------|--------------------------------|
|
|
114
|
+
| | | | |
|
|
115
|
+
|
|
116
|
+
- [ ] `grep -r "not yet implemented" src/ components/ app/` returns **zero matches**
|
|
117
|
+
- [ ] Matrix questions route by `subType` in `Question.tsx`
|
|
118
|
+
- [ ] `slider_matrix` shows sliders; `file_upload` shows dropzone
|