@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,102 @@
|
|
|
1
|
+
# UI Spec: Question Shell (`Question.tsx`)
|
|
2
|
+
|
|
3
|
+
> **Canonical template:** [`templates/Question.tsx`](../templates/Question.tsx) — copy this dispatcher verbatim
|
|
4
|
+
> **Monorepo reference:** `apps/client/src/components/Question.tsx` (not available in npm installs — implement from this spec)
|
|
5
|
+
> **SDK:** All types extend `QuestionBase` from `fetchSurvey/types/base.ts`
|
|
6
|
+
> **Component blueprint:** `01-components/02-question.md`
|
|
7
|
+
> **Required files:** `00-integration/component-checklist.md`
|
|
8
|
+
|
|
9
|
+
## Matrix subType Warning (CRITICAL)
|
|
10
|
+
|
|
11
|
+
Routing on `question.type === 'matrix'` **alone renders nothing**. You MUST branch on `subType`:
|
|
12
|
+
|
|
13
|
+
- `CFM_MATRIX` → `LikertMatrixScale`
|
|
14
|
+
- `CSAT_MATRIX` | `RATING_MATRIX` → `CsatMatrixScale`
|
|
15
|
+
|
|
16
|
+
Never use a `default` fallback that shows `"not yet implemented"` — create the scale components listed in the checklist.
|
|
17
|
+
|
|
18
|
+
## Config Inventory (QuestionBase)
|
|
19
|
+
|
|
20
|
+
| Field | Implemented | UI impact |
|
|
21
|
+
|-------|-------------|-----------|
|
|
22
|
+
| `id` | ✅ | `section id={question.id}` — required for scroll-to-error |
|
|
23
|
+
| `text` | ✅ | Heading via `dangerouslySetInnerHTML` |
|
|
24
|
+
| `description` | ✅ | Optional HTML block below title |
|
|
25
|
+
| `required` | ✅ | Red `*` after title (`text-red-500`) |
|
|
26
|
+
| `requiredErrorMessage` | ✅ | Shown via SDK validation in error banner |
|
|
27
|
+
| `questionNumber` | ❌ | Not rendered in reference client |
|
|
28
|
+
| `containerMediaUrl` | ❌ | Not rendered on generic questions |
|
|
29
|
+
|
|
30
|
+
## Layout Anatomy
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
┌─ section (rounded-xl border border-gray-200 bg-white p-6 shadow-sm) ─────┐
|
|
34
|
+
│ h2: question.text (HTML) + red * if required │
|
|
35
|
+
│ [optional description HTML] │
|
|
36
|
+
│ ┌─ type-specific scale (see per-type specs) ─────────────────────┐ │
|
|
37
|
+
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
38
|
+
│ [validation error banner if validationError prop] │
|
|
39
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Dispatcher Routing
|
|
43
|
+
|
|
44
|
+
| `question.type` | Condition | Component |
|
|
45
|
+
|-----------------|-----------|-----------|
|
|
46
|
+
| `rating` | — | `RatingScale` + label row in Question |
|
|
47
|
+
| `radio` | — | Inline card radios in Question |
|
|
48
|
+
| `text` | — | Inline textarea in Question |
|
|
49
|
+
| `csat` | — | `CsatScale` + min/max labels in Question |
|
|
50
|
+
| `matrix` | `CSAT_MATRIX` \| `RATING_MATRIX` | `CsatMatrixScale` |
|
|
51
|
+
| `matrix` | `CFM_MATRIX` | `LikertMatrixScale` |
|
|
52
|
+
| `slider_matrix` | — | `SliderMatrixScale` |
|
|
53
|
+
| `file_upload` | — | `FileUploadScale` |
|
|
54
|
+
| `text_and_media` | `mediaUrl` present | Inline media in Question |
|
|
55
|
+
| `slider` | — | ❌ Not routed — see `06-slider.md` |
|
|
56
|
+
| `rating_scale` | — | ❌ Not routed — see `05-rating-scale.md` |
|
|
57
|
+
|
|
58
|
+
## Label Positioning (rating)
|
|
59
|
+
|
|
60
|
+
Rendered in Question before `RatingScale`:
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
// midLabel CSS left:
|
|
64
|
+
left: `${(midLabelIndex / (options.length - 1)) * 100}%`
|
|
65
|
+
// with -translate-x-1/2 for centering
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
minLabel: `absolute left-0`, maxLabel: `absolute right-0`, container `relative h-6 text-xs font-semibold text-gray-500`.
|
|
69
|
+
|
|
70
|
+
## Label Positioning (csat)
|
|
71
|
+
|
|
72
|
+
minLabel / maxLabel: `flex justify-between text-xs font-semibold text-gray-500` above `CsatScale`.
|
|
73
|
+
|
|
74
|
+
## Validation Error Banner
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
className="mt-4 rounded-[4px] border border-[#333333] bg-[#d9d9d9] py-3.5 px-4"
|
|
78
|
+
// text: text-[13px] font-bold text-gray-900
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Props Contract
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
type QuestionProps = {
|
|
85
|
+
question: SurveyQuestion;
|
|
86
|
+
selectedValue?: AnswerValue;
|
|
87
|
+
validationError?: string;
|
|
88
|
+
onSelect: (value: AnswerValue) => void;
|
|
89
|
+
};
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Wire `onSelect` → `onAction({ type: 'CHANGE', payload: { questionId, value } })`.
|
|
93
|
+
|
|
94
|
+
## Agent Checklist
|
|
95
|
+
|
|
96
|
+
- [ ] `id={question.id}` on outermost `<section>`
|
|
97
|
+
- [ ] HTML rendering for text and description
|
|
98
|
+
- [ ] Copied from `templates/Question.tsx` — no stub/default fallback branches
|
|
99
|
+
- [ ] Matrix routes by `subType`, not type alone
|
|
100
|
+
- [ ] `slider_matrix` → `SliderMatrixScale`; `file_upload` → `FileUploadScale`
|
|
101
|
+
- [ ] Validation error banner below scale
|
|
102
|
+
- [ ] Rating/csat endpoint labels positioned per formulas above
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# UI Spec: NPS Rating (`type: 'rating'`)
|
|
2
|
+
|
|
3
|
+
> **Reference:** `apps/client/src/components/RatingScale.tsx` + label row in `Question.tsx`
|
|
4
|
+
> **SDK type:** `RatingQuestion` — `fetchSurvey/types/rating.ts`
|
|
5
|
+
> **Component blueprint:** `01-components/03-rating-scale.md`
|
|
6
|
+
|
|
7
|
+
## Config Inventory
|
|
8
|
+
|
|
9
|
+
| Field | Implemented | UI impact |
|
|
10
|
+
|-------|-------------|-----------|
|
|
11
|
+
| `options[]` | ✅ | Badge row + radio row (dual layer) |
|
|
12
|
+
| `options[].color` | ✅ | Badge background; fallback `#e20074` |
|
|
13
|
+
| `options[].label` | ✅ | Displayed in badge |
|
|
14
|
+
| `options[].value` | ✅ | Selection value |
|
|
15
|
+
| `minLabel` | ✅ | Rendered in Question.tsx above scale |
|
|
16
|
+
| `midLabel` | ✅ | Absolute positioned in Question.tsx |
|
|
17
|
+
| `maxLabel` | ✅ | Absolute positioned in Question.tsx |
|
|
18
|
+
| `midLabelIndex` | ✅ | Mid label `left` percentage |
|
|
19
|
+
| `reverseScaleOrder` | ❌ | Document for agents when SDK sends true |
|
|
20
|
+
| `buttonVariant` | ❌ | Client always uses dual-layer radio layout |
|
|
21
|
+
|
|
22
|
+
## Layout Anatomy
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
[minLabel] [midLabel @ %] [maxLabel]
|
|
26
|
+
┌──┬──┬──┬── ... ──┬──┐ ← Row 1: colored badges (NOT clickable)
|
|
27
|
+
│0 │1 │2 │3 │ ... │10│ grid repeat(options.length), h-5 w-5 sm:h-6 w-6
|
|
28
|
+
└──┴──┴──┴── ... ──┴──┘ rounded-[3px], white bold text, option.color bg
|
|
29
|
+
┌──────────────────────────────────────────────┐
|
|
30
|
+
│ ○ ○ ● ○ ... ○ │ ← Row 2: gray track bg-[#f2f2f2]
|
|
31
|
+
└──────────────────────────────────────────────┘ h-11 sm:h-12, rounded-md
|
|
32
|
+
↑ selected cell: inner bg-[#fbe8f3]
|
|
33
|
+
radio: border-[4px] border-[#e20074] bg-white
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Config Decision Tree
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
rating
|
|
40
|
+
└── RatingScale (always dual-layer in reference client)
|
|
41
|
+
├── Row 1: display-only colored badges
|
|
42
|
+
└── Row 2: sr-only radio + custom circle per cell
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
If `buttonVariant === 'numbered'` (SDK gap): badges become clickable buttons instead — not in reference client.
|
|
46
|
+
|
|
47
|
+
If `reverseScaleOrder`: reverse `options` array before mapping both rows.
|
|
48
|
+
|
|
49
|
+
## Interactive States
|
|
50
|
+
|
|
51
|
+
| State | Badge row | Track cell | Radio circle |
|
|
52
|
+
|-------|-----------|------------|--------------|
|
|
53
|
+
| Default | Full color | Transparent inner | `border-2 border-gray-300` |
|
|
54
|
+
| Hover | — | `hover:bg-gray-200/40` | — |
|
|
55
|
+
| Selected | Unchanged | `bg-[#fbe8f3]` | `border-[4px] border-[#e20074]` |
|
|
56
|
+
|
|
57
|
+
## Tooltips
|
|
58
|
+
|
|
59
|
+
Recommended: `title={option.label}` on each track `<label>`. Not in reference client yet.
|
|
60
|
+
|
|
61
|
+
## Answer Mutation
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
onSelect(option.value) // number
|
|
65
|
+
// → onAction CHANGE { questionId, value: number }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Accessibility
|
|
69
|
+
|
|
70
|
+
- `role="radiogroup"` on wrapper
|
|
71
|
+
- Native `<input type="radio" className="sr-only">` per cell
|
|
72
|
+
- `name={questionId}` groups radios
|
|
73
|
+
|
|
74
|
+
## Agent Checklist
|
|
75
|
+
|
|
76
|
+
- [ ] Two-row grid with `gridTemplateColumns: repeat(options.length, 1fr)`
|
|
77
|
+
- [ ] Badges use `option.color`, not hardcoded NPS colors unless option lacks color
|
|
78
|
+
- [ ] Only track row is clickable
|
|
79
|
+
- [ ] min/mid/max labels in Question wrapper with midLabelIndex formula
|
|
80
|
+
- [ ] Selected state: pink inner track + thick magenta radio ring
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# UI Spec: Radio MCQ (`type: 'radio'`)
|
|
2
|
+
|
|
3
|
+
> **Reference:** `apps/client/src/components/Question.tsx` (inline)
|
|
4
|
+
> **SDK type:** `RadioQuestion` — `fetchSurvey/types/radio.ts`
|
|
5
|
+
> **Component blueprint:** `01-components/02-question.md`
|
|
6
|
+
|
|
7
|
+
## Config Inventory
|
|
8
|
+
|
|
9
|
+
| Field | Implemented | UI impact |
|
|
10
|
+
|-------|-------------|-----------|
|
|
11
|
+
| `options[]` | ✅ | One card per option |
|
|
12
|
+
| `options[].label` | ✅ | HTML via `dangerouslySetInnerHTML` |
|
|
13
|
+
| `options[].value` | ✅ | Selection value; `null` → empty string on input |
|
|
14
|
+
| `isMultiSelect` | ❌ | SDK supports; client is single-select only |
|
|
15
|
+
|
|
16
|
+
## Layout Anatomy
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
┌─ label (full-width card) ─────────────────────────────────────┐
|
|
20
|
+
│ (●) Option label HTML │ selected
|
|
21
|
+
└───────────────────────────────────────────────────────────────┘
|
|
22
|
+
┌─ label ───────────────────────────────────────────────────────┐
|
|
23
|
+
│ (○) Option label HTML │ unselected
|
|
24
|
+
└───────────────────────────────────────────────────────────────┘
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Interactive States
|
|
28
|
+
|
|
29
|
+
| State | Border | Background | Radio |
|
|
30
|
+
|-------|--------|------------|-------|
|
|
31
|
+
| Unselected | `#e5e5e5` | `white` | `border-gray-400`, empty |
|
|
32
|
+
| Unselected hover | `#e5e5e5` | `hover:bg-gray-50/50` | — |
|
|
33
|
+
| Selected | `#e20074` | `#fdf2f8` | `border-2 border-[#e20074]` + inner dot `h-2.5 w-2.5 bg-[#e20074]` |
|
|
34
|
+
|
|
35
|
+
## Structure
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
<label className="flex items-center gap-4 rounded-lg border py-4 px-5 cursor-pointer">
|
|
39
|
+
<input type="radio" name={question.id} className="sr-only" />
|
|
40
|
+
<div className="h-5 w-5 rounded-full border ..."> {/* custom radio */} </div>
|
|
41
|
+
<span dangerouslySetInnerHTML={{ __html: option.label }} />
|
|
42
|
+
</label>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Answer Mutation
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
onSelect(option.value) // string | number
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Agent Checklist
|
|
52
|
+
|
|
53
|
+
- [ ] Full-width card options with gap-3 vertical stack
|
|
54
|
+
- [ ] Custom circular radio indicator (not browser default)
|
|
55
|
+
- [ ] Selected: magenta border + pink background
|
|
56
|
+
- [ ] HTML labels
|
|
57
|
+
- [ ] If `isMultiSelect`: implement checkbox cards (SDK gap in reference)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# UI Spec: Text Field (`type: 'text'`)
|
|
2
|
+
|
|
3
|
+
> **Reference:** `apps/client/src/components/Question.tsx` (inline)
|
|
4
|
+
> **SDK type:** `TextQuestion` — `fetchSurvey/types/text.ts`
|
|
5
|
+
|
|
6
|
+
## Config Inventory
|
|
7
|
+
|
|
8
|
+
| Field | Implemented | UI impact |
|
|
9
|
+
|-------|-------------|-----------|
|
|
10
|
+
| `placeholder` | ✅ | Textarea placeholder; default `'Type your response here...'` |
|
|
11
|
+
| `maxCharacterCount` | ✅ | Counter uses this; default 1000 |
|
|
12
|
+
| `inputVariant: 'long'` | ✅ | Always textarea (4 rows) in reference |
|
|
13
|
+
| `inputVariant: 'short'` | ❌ | Should render single-line `<input>` when SDK sends `'short'` |
|
|
14
|
+
|
|
15
|
+
## Layout Anatomy
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
┌─ textarea (w-full rounded-[4px] border border-gray-300 p-4) ──┐
|
|
19
|
+
│ placeholder text │
|
|
20
|
+
│ │
|
|
21
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
22
|
+
N Zeichen verbleiben. ← counter
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Interactive States
|
|
26
|
+
|
|
27
|
+
| State | Style |
|
|
28
|
+
|-------|-------|
|
|
29
|
+
| Default | `border-gray-300` |
|
|
30
|
+
| Focus | `focus:border-[#e20074] focus:ring-1 focus:ring-[#e20074]` |
|
|
31
|
+
|
|
32
|
+
## Character Counter
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
remaining = maxCharacterCount ?? 1000 - selectedValue.length
|
|
36
|
+
// Display: "{remaining} Zeichen verbleiben." (German in reference client)
|
|
37
|
+
// Agents: use survey language for i18n
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Answer Mutation
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
onSelect(string) // onChange from textarea
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Agent Checklist
|
|
47
|
+
|
|
48
|
+
- [ ] Textarea with 4 rows for long variant
|
|
49
|
+
- [ ] Character counter bottom-right
|
|
50
|
+
- [ ] Magenta focus ring
|
|
51
|
+
- [ ] Implement `inputVariant: 'short'` as single-line input when present
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# UI Spec: CSAT (`type: 'csat'`)
|
|
2
|
+
|
|
3
|
+
> **Reference:** `apps/client/src/components/CsatScale.tsx`
|
|
4
|
+
> **SDK type:** `CsatQuestion` — `fetchSurvey/types/csat.ts`
|
|
5
|
+
> **Component blueprint:** `01-components/04-csat-scale.md`
|
|
6
|
+
|
|
7
|
+
## Config Inventory
|
|
8
|
+
|
|
9
|
+
| Field | Implemented | UI impact |
|
|
10
|
+
|-------|-------------|-----------|
|
|
11
|
+
| `options[]` | ✅ | Scale buttons or dropdown options |
|
|
12
|
+
| `buttonType` | ✅ partial | Branches below |
|
|
13
|
+
| `labelPosition` | ✅ | `'top'` → flex-col-reverse; `'hidden'` → no label |
|
|
14
|
+
| `minLabel` / `maxLabel` | ✅ | Rendered in Question.tsx above scale |
|
|
15
|
+
| `hasNotApplicable` | ❌ | N/A option in options array still renders |
|
|
16
|
+
| `reverseScaleOrder` | ❌ | Document: reverse options before map |
|
|
17
|
+
|
|
18
|
+
## Config Decision Tree
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
csat
|
|
22
|
+
├── buttonType === 'dropdown'
|
|
23
|
+
│ └── Native <select>, placeholder "Select an option"
|
|
24
|
+
│ └── Parse value 'null' → null
|
|
25
|
+
└── else (emoji | star | numbered | number | default)
|
|
26
|
+
└── flex flex-wrap gap-3 buttons
|
|
27
|
+
├── emoji → getEmojiForIndex(index, scaleLength)
|
|
28
|
+
├── star → CsatStarIcons filled/empty; labels hidden
|
|
29
|
+
├── numbered/number → mono font value
|
|
30
|
+
└── default → bold value or 'N/A' if null
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Interactive States
|
|
34
|
+
|
|
35
|
+
| State | Border | Background | Transform |
|
|
36
|
+
|-------|--------|------------|-----------|
|
|
37
|
+
| Default | `border-gray-200` | `white` | scale-100 |
|
|
38
|
+
| Hover | `border-gray-300` | `hover:bg-gray-50` | — |
|
|
39
|
+
| Selected | `border-[#e20074]` | `#fdf2f8` | `scale-110` |
|
|
40
|
+
| Star mode | transparent | transparent | no border/bg on button |
|
|
41
|
+
|
|
42
|
+
## Tooltips
|
|
43
|
+
|
|
44
|
+
Recommended: `title={option.label}` on each button. Not in reference client.
|
|
45
|
+
|
|
46
|
+
## Answer Mutation
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
onSelect(string | number | null)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Agent Checklist
|
|
53
|
+
|
|
54
|
+
- [ ] Dropdown branch with magenta focus ring
|
|
55
|
+
- [ ] Emoji/star/numbered branches per decision tree
|
|
56
|
+
- [ ] `labelPosition` controls label above/below/hidden
|
|
57
|
+
- [ ] Implement `reverseScaleOrder` and standalone `hasNotApplicable` (SDK gaps)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# UI Spec: Rating Scale (`type: 'rating_scale'`)
|
|
2
|
+
|
|
3
|
+
> **Reference:** ❌ Not routed in `Question.tsx` — reuse `CsatScale.tsx` patterns
|
|
4
|
+
> **SDK type:** `RatingScaleQuestion` — `fetchSurvey/types/ratingScale.ts`
|
|
5
|
+
> **Component blueprint:** `01-components/03-rating-scale.md`
|
|
6
|
+
|
|
7
|
+
## Config Inventory
|
|
8
|
+
|
|
9
|
+
| Field | Implemented | UI impact |
|
|
10
|
+
|-------|-------------|-----------|
|
|
11
|
+
| `options[]` | ❌ gap | Star/emoji buttons (no NPS colors) |
|
|
12
|
+
| `scaleStyle` | ❌ gap | `'star'` or `'emoji'` |
|
|
13
|
+
| `minLabel` / `midLabel` / `maxLabel` | ❌ gap | Same formulas as NPS in Question shell |
|
|
14
|
+
| `midLabelIndex` | ❌ gap | `(midLabelIndex / (options.length - 1)) * 100`% |
|
|
15
|
+
| `reverseScaleOrder` | ❌ gap | Reverse options before render |
|
|
16
|
+
|
|
17
|
+
## Expected UI (from CsatScale + rating labels)
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
[minLabel] [midLabel @ %] [maxLabel]
|
|
21
|
+
┌────┬────┬────┬────┬────┐
|
|
22
|
+
│ ★ │ ★ │ ★ │ ★ │ ★ │ scaleStyle: 'star'
|
|
23
|
+
└────┴────┴────┴────┴────┘
|
|
24
|
+
OR emoji row from getEmojiForIndex when scaleStyle: 'emoji'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Reuse CsatScale selected styling: `border-[#e20074] bg-[#fdf2f8] scale-110`.
|
|
28
|
+
|
|
29
|
+
## Config Decision Tree
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
rating_scale
|
|
33
|
+
├── scaleStyle === 'star' → CsatStarIcons per option
|
|
34
|
+
├── scaleStyle === 'emoji' → getEmojiForIndex
|
|
35
|
+
└── else → numbered badges (no option.color)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Answer Mutation
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
onSelect(number)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Agent Checklist
|
|
45
|
+
|
|
46
|
+
- [ ] Add dispatcher branch in Question for `rating_scale`
|
|
47
|
+
- [ ] No traffic-light NPS colors on options
|
|
48
|
+
- [ ] min/mid/max labels with midLabelIndex formula
|
|
49
|
+
- [ ] Match CsatScale interactive states
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# UI Spec: Standalone Slider (`type: 'slider'`)
|
|
2
|
+
|
|
3
|
+
> **Reference:** ❌ Not routed in `Question.tsx` — reuse `CustomSliderTrack.tsx`
|
|
4
|
+
> **SDK type:** `SliderQuestion` — `fetchSurvey/types/slider.ts`
|
|
5
|
+
> **Component blueprint:** `01-components/09-custom-slider-track.md`
|
|
6
|
+
|
|
7
|
+
## Config Inventory
|
|
8
|
+
|
|
9
|
+
| Field | Implemented | UI impact |
|
|
10
|
+
|-------|-------------|-----------|
|
|
11
|
+
| `min` / `max` / `step` | ❌ gap | Track range |
|
|
12
|
+
| `defaultValue` | ❌ gap | Initial thumb position |
|
|
13
|
+
| `minLabel` / `maxLabel` | ❌ gap | End labels above track |
|
|
14
|
+
| `enableInputBox` | ❌ gap | See SliderMatrixScale input pattern |
|
|
15
|
+
| `displayValues` | ❌ gap | Pass to CustomSliderTrack |
|
|
16
|
+
|
|
17
|
+
## Layout Anatomy
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
[minLabel] [maxLabel]
|
|
21
|
+
┌──────────────────────────────────────────────────┐
|
|
22
|
+
│ ───●──────────────── (CustomSliderTrack) │
|
|
23
|
+
└──────────────────────────────────────────────────┘
|
|
24
|
+
[optional number input + steppers if enableInputBox]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Reference Patterns
|
|
28
|
+
|
|
29
|
+
Copy from [`09-slider-matrix.md`](09-slider-matrix.md) single-row layout and [`shared/custom-slider-track.md`](shared/custom-slider-track.md).
|
|
30
|
+
|
|
31
|
+
## Answer Mutation
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
onSelect(number)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Agent Checklist
|
|
38
|
+
|
|
39
|
+
- [ ] Add Question dispatcher for `slider`
|
|
40
|
+
- [ ] CustomSliderTrack with min/max/step
|
|
41
|
+
- [ ] Optional input box with clamp min/max
|
|
42
|
+
- [ ] End labels flex justify-between
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# UI Spec: CFM Matrix (`type: 'matrix'`, `subType: 'CFM_MATRIX'`)
|
|
2
|
+
|
|
3
|
+
> **Implement from this spec** (npm installs). Monorepo reference: `apps/client/src/components/LikertMatrixScale.tsx`
|
|
4
|
+
> **SDK type:** `MatrixQuestion` — `fetchSurvey/types/matrix.ts`
|
|
5
|
+
> **Component blueprint:** `01-components/06-likert-matrix-scale.md`
|
|
6
|
+
|
|
7
|
+
## Config Inventory
|
|
8
|
+
|
|
9
|
+
| Field | Implemented | UI impact |
|
|
10
|
+
|-------|-------------|-----------|
|
|
11
|
+
| `rows[]`, `columns[]` | ✅ | Grid / carousel / dropdown data |
|
|
12
|
+
| `matrixFormat` | ✅ | Orchestrator routing |
|
|
13
|
+
| `answerType` | ✅ | `'multiple'` → checkboxes |
|
|
14
|
+
| `matrixType: 'bipolar'` | ✅ | 25% left + 25% right columns |
|
|
15
|
+
| `labels[]` | ✅ | Anchor labels above scale |
|
|
16
|
+
| `transposeTable` | ✅ | Swap row/col iteration |
|
|
17
|
+
| `repeatScale` | ✅ | Re-render header per row |
|
|
18
|
+
| `enableScale` | ✅ | Show/hide column header row |
|
|
19
|
+
| `hasNotApplicable` | ✅ | N/A column with mutex |
|
|
20
|
+
|
|
21
|
+
## Config Decision Tree
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
CFM_MATRIX
|
|
25
|
+
├── matrixFormat === 'dropdown' → LikertMatrixDropdownLayout (MatrixDropdown per row)
|
|
26
|
+
├── matrixFormat === 'carousel' → LikertMatrixCarouselLayout (one row, prev/next, blue dots)
|
|
27
|
+
└── else → LikertMatrixGridLayout
|
|
28
|
+
├── transposeTable ? columns×rows : rows×columns
|
|
29
|
+
├── repeatScale ? header per row : single header
|
|
30
|
+
├── enableScale ? column headers : hidden
|
|
31
|
+
└── matrixType === 'bipolar' ? 25% | scale | 25% layout
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Interactive Behaviors
|
|
35
|
+
|
|
36
|
+
| Behavior | Condition | Action |
|
|
37
|
+
|----------|-----------|--------|
|
|
38
|
+
| Click selected radio again | single-select grid/carousel | Clear row (`undefined`) |
|
|
39
|
+
| N/A selected | any | Other cols opacity 0.4, disabled |
|
|
40
|
+
| Multi N/A | answerType multiple | Sets `[null]` exclusively |
|
|
41
|
+
| Multi toggle | answerType multiple | Add/remove from array |
|
|
42
|
+
|
|
43
|
+
## Grid Layout
|
|
44
|
+
|
|
45
|
+
- Row label column: `180px` (likert) or `25%` (bipolar)
|
|
46
|
+
- Zebra rows: even rows `rgba(249,250,251,0.8)`
|
|
47
|
+
- Native inputs: `accentColor: #e20074`, 18×18px
|
|
48
|
+
|
|
49
|
+
## Carousel
|
|
50
|
+
|
|
51
|
+
- Blue active dot: `#2563eb` (Likert only)
|
|
52
|
+
- Card: white, border, shadow, centered row text
|
|
53
|
+
|
|
54
|
+
## Answer Mutation
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
onSelect(MatrixAnswerMap) // Record<rowId, value | value[] | null>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Agent Checklist
|
|
61
|
+
|
|
62
|
+
- [ ] All three matrixFormat branches
|
|
63
|
+
- [ ] transposeTable + repeatScale + enableScale
|
|
64
|
+
- [ ] Bipolar 25/50/25 layout with rightText
|
|
65
|
+
- [ ] Single-select deselect on re-click
|
|
66
|
+
- [ ] N/A mutex with opacity 0.4
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# UI Spec: CSAT / Rating Matrix (`subType: 'CSAT_MATRIX' | 'RATING_MATRIX'`)
|
|
2
|
+
|
|
3
|
+
> **Implement from this spec** (npm installs). Monorepo reference: `apps/client/src/components/CsatMatrixScale.tsx`
|
|
4
|
+
> **SDK type:** `MatrixQuestion` — `fetchSurvey/types/matrix.ts`
|
|
5
|
+
> **Component blueprint:** `01-components/05-csat-matrix-scale.md`
|
|
6
|
+
|
|
7
|
+
## Config Inventory
|
|
8
|
+
|
|
9
|
+
| Field | Implemented | UI impact |
|
|
10
|
+
|-------|-------------|-----------|
|
|
11
|
+
| `buttonType` | ✅ | Grid vs vertical list routing |
|
|
12
|
+
| `matrixFormat` | ✅ | carousel vs standard |
|
|
13
|
+
| `hasNotApplicable` | ✅ | Checkbox below row (vertical) or N/A column (grid) |
|
|
14
|
+
| `reverseScaleOrder` | ✅ | Flip emoji/star/number index |
|
|
15
|
+
| `labels[]` | ✅ | Anchor row above columns |
|
|
16
|
+
| `columns[].color` | ✅ | RATING_MATRIX badge colors |
|
|
17
|
+
|
|
18
|
+
## Config Decision Tree
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
CSAT_MATRIX | RATING_MATRIX
|
|
22
|
+
├── matrixFormat === 'carousel' → CsatMatrixCarousel
|
|
23
|
+
├── buttonType in (dropdown, selectbox, radio) → CsatMatrixVerticalList
|
|
24
|
+
│ ├── dropdown → MatrixDropdown placeholder "Select..."
|
|
25
|
+
│ ├── selectbox → full-width cards #fdf2f8 selected
|
|
26
|
+
│ └── else → card radio list
|
|
27
|
+
└── else → CsatMatrixGrid
|
|
28
|
+
├── emoji → getEmojiForIndex + hover scale + ring when selected
|
|
29
|
+
├── star → opacity 0.5 unselected; filled/empty icons
|
|
30
|
+
├── numbered → 40×40 badge; selected magenta fill
|
|
31
|
+
└── graphics → CustomSliderTrack per row (not in grid — carousel only)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Grid Structure
|
|
35
|
+
|
|
36
|
+
- Row label: `180px` fixed column
|
|
37
|
+
- N/A column: `56px` when `hasNotApplicable`
|
|
38
|
+
- Alternating row backgrounds
|
|
39
|
+
- Column headers OR `labels[]` anchor row with percentage positioning
|
|
40
|
+
|
|
41
|
+
## Carousel
|
|
42
|
+
|
|
43
|
+
- Pink active dot expands: `20px × 8px` vs `8px` inactive
|
|
44
|
+
- Column labels below cells
|
|
45
|
+
- Emoji tooltip via hover state (`hoveredCell`)
|
|
46
|
+
- Graphics mode: slider maps column index
|
|
47
|
+
|
|
48
|
+
## Tooltips
|
|
49
|
+
|
|
50
|
+
`title={col.label}` recommended on grid cells (emoji hover uses scale transform).
|
|
51
|
+
|
|
52
|
+
## Answer Mutation
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
onSelect(MatrixAnswerMap)
|
|
56
|
+
// hasNotApplicable: null for N/A; undefined clears
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Agent Checklist
|
|
60
|
+
|
|
61
|
+
- [ ] scaleColumns = columns.filter(c => c.value !== null)
|
|
62
|
+
- [ ] All buttonType × matrixFormat branches
|
|
63
|
+
- [ ] reverseScaleOrder on emoji/star/numbered
|
|
64
|
+
- [ ] Carousel pink dot indicators
|
|
65
|
+
- [ ] Vertical selectbox cards match standalone radio styling
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# UI Spec: Slider Matrix (`type: 'slider_matrix'`)
|
|
2
|
+
|
|
3
|
+
> **Implement from this spec** (npm installs). Monorepo reference: `apps/client/src/components/SliderMatrixScale.tsx`
|
|
4
|
+
> **SDK type:** `SliderMatrixQuestion` — `fetchSurvey/types/sliderMatrix.ts`
|
|
5
|
+
> **Component blueprint:** `01-components/07-slider-matrix-scale.md`
|
|
6
|
+
> **Shared:** [`shared/custom-slider-track.md`](shared/custom-slider-track.md)
|
|
7
|
+
|
|
8
|
+
## Config Inventory
|
|
9
|
+
|
|
10
|
+
| Field | Implemented | UI impact |
|
|
11
|
+
|-------|-------------|-----------|
|
|
12
|
+
| `rows[]` (min/max/step) | ✅ | Per-row slider range |
|
|
13
|
+
| `labels[]` | ✅ | Header anchor labels |
|
|
14
|
+
| `tickValues[]` | ✅ | Colored badges above track |
|
|
15
|
+
| `ticks` | ✅ | Discrete marks + htmlStep |
|
|
16
|
+
| `sliderType: 'graphics'` | ✅ | Plain text tick labels; emoji thumb |
|
|
17
|
+
| `enableInputBox` | ✅ | 80px input + stepper buttons |
|
|
18
|
+
| `displayValues` | ✅ | Passed to CustomSliderTrack |
|
|
19
|
+
| `enableNotApplicable` | ✅ | Checkbox; value `'N/A'` string |
|
|
20
|
+
|
|
21
|
+
## Layout Anatomy
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
[header: labels[] + tickValues badges OR computed marks]
|
|
25
|
+
┌─ row band (#f3f4f6, rounded-xl) ─────────────────────────────┐
|
|
26
|
+
│ 25% statement │ CustomSliderTrack │ 25% rightText? │ input? │ N/A? │
|
|
27
|
+
└───────────────────────────────────────────────────────────────┘
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Header Tick Positioning
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
percentage = ((tv.value - min) / (max - min)) * 100
|
|
34
|
+
sliceWidth = tickValues.length > 1 ? 100 / (tickValues.length - 1) : 100
|
|
35
|
+
// Colored badge: tv.color bg, white text, rounded 4px
|
|
36
|
+
// graphics: plain text label instead of colored badge
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## N/A Behavior
|
|
40
|
+
|
|
41
|
+
- Checked → `rowVal === 'N/A'`; slider disabled, input grayed
|
|
42
|
+
- Unchecked → reset to `defaultValue ?? min`
|
|
43
|
+
|
|
44
|
+
## Input Box
|
|
45
|
+
|
|
46
|
+
- Clamp to row.min / row.max on change
|
|
47
|
+
- Steppers use `htmlStep` increment/decrement
|
|
48
|
+
|
|
49
|
+
## Answer Mutation
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
onSelect(Record<string, number | null | 'N/A'>)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Agent Checklist
|
|
56
|
+
|
|
57
|
+
- [ ] Shared header from first row min/max/step
|
|
58
|
+
- [ ] Gray row bands with 25% statement columns
|
|
59
|
+
- [ ] CustomSliderTrack per row with correct htmlStep
|
|
60
|
+
- [ ] tickValues colored badges (non-graphics)
|
|
61
|
+
- [ ] N/A mutex disables slider + input
|