@explorer02/cfm-survey-sdk 0.2.0 → 0.2.2
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 +18 -18
- package/dist/cli/index.mjs +14 -14
- package/dist/index.d.mts +465 -580
- package/dist/index.d.ts +465 -580
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +6 -4
- package/postinstall.js +96 -64
- package/templates/AGENT.md +15 -7
- package/templates/docs/00-integration/answer-logic-and-navigation.md +50 -0
- package/templates/docs/00-integration/aws-deploy.md +281 -0
- package/templates/docs/00-integration/client-integration-guide.md +351 -0
- package/templates/docs/00-integration/component-checklist.md +84 -0
- package/templates/docs/00-integration/constraints.md +65 -3
- package/templates/docs/00-integration/custom-field-logic-and-navigation.md +174 -0
- package/templates/docs/00-integration/display-logic-and-navigation.md +49 -0
- package/templates/docs/00-integration/end-page-logic.md +64 -0
- package/templates/docs/00-integration/partial-save-and-recovery.md +74 -0
- package/templates/docs/00-integration/placeholders-and-tokens.md +116 -0
- package/templates/docs/00-integration/progress.md +61 -0
- package/templates/docs/00-integration/question-display-variants.md +65 -0
- package/templates/docs/00-integration/question-numbering.md +51 -0
- package/templates/docs/00-integration/question-type-sdk-matrix.md +242 -0
- package/templates/docs/00-integration/setup.md +84 -0
- package/templates/docs/00-integration/skip-logic-and-navigation.md +80 -0
- package/templates/docs/00-integration/survey-lifecycle-analytics.md +94 -0
- package/templates/docs/00-integration/useSurveySDK.md +151 -23
- package/templates/docs/01-components/01-survey-page.md +76 -54
- package/templates/docs/01-components/02-question.md +77 -89
- package/templates/docs/01-components/03-rating-scale.md +11 -1
- package/templates/docs/01-components/04-star-rating-scale.md +45 -0
- package/templates/docs/01-components/05-csat-matrix-scale.md +20 -11
- package/templates/docs/01-components/06-likert-matrix-scale.md +12 -3
- package/templates/docs/01-components/07-slider-matrix-scale.md +11 -2
- package/templates/docs/01-components/08-file-upload-scale.md +10 -1
- package/templates/docs/01-components/09-custom-slider-track.md +2 -2
- package/templates/docs/01-components/11-progress-bar.md +10 -4
- package/templates/docs/01-components/13-matrix-dropdown.md +3 -1
- package/templates/docs/01-components/14-intro-page.md +34 -0
- package/templates/docs/01-components/15-end-page.md +31 -0
- package/templates/docs/01-components/16-paused-page.md +27 -0
- package/templates/docs/01-components/17-heatmap-scale.md +75 -0
- package/templates/docs/01-components/18-rank-order-scale.md +100 -0
- package/templates/docs/01-components/README.md +44 -36
- package/templates/docs/02-reference/config-field-index.md +141 -139
- package/templates/docs/02-reference/exports.md +149 -0
- package/templates/docs/02-reference/question-types/01-rating.md +31 -32
- package/templates/docs/02-reference/question-types/04-csat.md +51 -42
- package/templates/docs/02-reference/question-types/05-rating-scale.md +16 -11
- package/templates/docs/02-reference/question-types/06-slider.md +2 -2
- package/templates/docs/02-reference/question-types/07-matrix-cfm.md +44 -30
- package/templates/docs/02-reference/question-types/08-matrix-csat.md +4 -26
- package/templates/docs/02-reference/question-types/09-matrix-rating.md +29 -13
- package/templates/docs/02-reference/question-types/10-slider-matrix.md +15 -4
- package/templates/docs/02-reference/question-types/11-file-upload.md +13 -2
- package/templates/docs/02-reference/question-types/12-heatmap.md +59 -0
- package/templates/docs/02-reference/question-types/13-rank-order.md +101 -0
- package/templates/docs/02-reference/question-types/README.md +86 -47
- package/templates/docs/02-reference/routing-table.md +33 -18
- package/templates/docs/02-reference/value-derivation.md +63 -0
- package/templates/docs/03-ui-specs/00-question-shell.md +24 -80
- package/templates/docs/03-ui-specs/01-rating.md +12 -54
- package/templates/docs/03-ui-specs/02-radio.md +21 -33
- package/templates/docs/03-ui-specs/03-text.md +2 -2
- package/templates/docs/03-ui-specs/04-csat.md +38 -33
- package/templates/docs/03-ui-specs/05-rating-scale.md +21 -18
- package/templates/docs/03-ui-specs/06-slider.md +3 -1
- package/templates/docs/03-ui-specs/07-matrix-cfm.md +25 -22
- package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +18 -46
- package/templates/docs/03-ui-specs/09-slider-matrix.md +2 -2
- package/templates/docs/03-ui-specs/10-file-upload.md +2 -2
- package/templates/docs/03-ui-specs/11-text-and-media.md +29 -29
- package/templates/docs/03-ui-specs/12-survey-chrome.md +20 -14
- package/templates/docs/03-ui-specs/13-heatmap.md +73 -0
- package/templates/docs/03-ui-specs/14-rank-order.md +59 -0
- package/templates/docs/03-ui-specs/README.md +14 -13
- package/templates/docs/03-ui-specs/shared/custom-slider-track.md +3 -3
- package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +6 -1
- package/templates/docs/MANIFEST.json +454 -67
- package/templates/docs/index.md +84 -29
- package/templates/docs/templates/Question.tsx +310 -0
- package/templates/docs/templates/deploy-to-aws.sh +114 -0
- package/templates/docs/templates/heatmapCoords.ts +58 -0
- package/templates/docs/templates/implementation_plan.md +115 -49
- package/templates/docs/templates/survey-inventory.schema.json +72 -0
- package/templates/docs/templates/verify-agent-build.sh +53 -0
- package/templates/docs/01-components/04-csat-scale.md +0 -40
- package/templates/docs/02-question-types/01-rating.md +0 -44
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Rank Order Question (`RANK_ORDER`)
|
|
2
|
+
|
|
3
|
+
> **SDK type:** `RankOrderQuestion` in `fetchSurvey/types/rankOrder.ts`
|
|
4
|
+
> **UI spec:** `03-ui-specs/14-rank-order.md`
|
|
5
|
+
|
|
6
|
+
## API Source
|
|
7
|
+
|
|
8
|
+
- `answerType: "RANK_ORDER"`
|
|
9
|
+
- `answerDetails[]` → options (text + optional `answerAsset`)
|
|
10
|
+
- `questionConfigs`:
|
|
11
|
+
- `RANK_ORDER_ANSWER_TYPE` → `DROPDOWN` | `DRAG_AND_DROP`
|
|
12
|
+
- `OPTION_TYPE` → `TEXT_ONLY` | `IMAGE_ONLY` | `TEXT_AND_IMAGE`
|
|
13
|
+
- `REQUIRE_RANK_ALL` → boolean
|
|
14
|
+
- `SHUFFLE_ORDER` → boolean
|
|
15
|
+
- `additionalInfo.hasOptionLevelAnswers: true` → submit uses `optionLevelAnswers`
|
|
16
|
+
|
|
17
|
+
## Question Fields
|
|
18
|
+
|
|
19
|
+
| Field | Type | API config |
|
|
20
|
+
|-------|------|------------|
|
|
21
|
+
| `type` | `'RANK_ORDER'` | `answerType` |
|
|
22
|
+
| `options` | `RankOrderOption[]` | `answerDetails[]` |
|
|
23
|
+
| `interactionMode` | `'dropdown' \| 'dragAndDrop'` | `RANK_ORDER_ANSWER_TYPE` |
|
|
24
|
+
| `optionDisplay` | `'textOnly' \| 'imageOnly' \| 'textAndImage'` | `OPTION_TYPE` |
|
|
25
|
+
| `requireRankAll` | `boolean` | `REQUIRE_RANK_ALL` |
|
|
26
|
+
| `shuffleOptions` | `boolean` | `SHUFFLE_ORDER` |
|
|
27
|
+
|
|
28
|
+
### RankOrderOption
|
|
29
|
+
|
|
30
|
+
| Field | Source |
|
|
31
|
+
|-------|--------|
|
|
32
|
+
| `id` | `answerDetails[].id` |
|
|
33
|
+
| `optionLabel` | `answerDetails[].answerText` |
|
|
34
|
+
| `imageUrl` | `answerAsset.source` |
|
|
35
|
+
| `previewImageUrl` | `answerAsset.previewImageUrl` |
|
|
36
|
+
| `answerLogic` | option `controlConfig.displayLogic` |
|
|
37
|
+
|
|
38
|
+
## Answer Shape
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
/** optionId → 1-based rank. Partial map allowed when requireRankAll is false. */
|
|
42
|
+
type RankOrderAnswers = Record<string, number>;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Stored in `state.answers[questionId]`.
|
|
46
|
+
|
|
47
|
+
## Submit Shape
|
|
48
|
+
|
|
49
|
+
Both fields are populated for rank-order questions:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// Ordered option ids from rank 1 → N
|
|
53
|
+
questionToAnswers[questionId] = ['optionIdRank1', 'optionIdRank2', ...];
|
|
54
|
+
|
|
55
|
+
// Per-option rank (API key uses -:- separator)
|
|
56
|
+
optionLevelAnswers[`${questionId}-:-${optionId}`] = [String(rank)];
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Logic Field: `RANK_ORDER`
|
|
60
|
+
|
|
61
|
+
Conditions target a **specific option** via `details.subConditionEntity` (`entityType: 'OPTION'`).
|
|
62
|
+
|
|
63
|
+
| UI label | API operator | Values | Semantics |
|
|
64
|
+
|----------|--------------|--------|-----------|
|
|
65
|
+
| Ranked as | `EQUALS` | rank number | Subject option rank equals value |
|
|
66
|
+
| Ranked more than | `GT` | rank number | Subject rank > value |
|
|
67
|
+
| Ranked less than | `LT` | rank number | Subject rank < value |
|
|
68
|
+
| Ranked above | `BEFORE` | reference option id | Subject rank < reference rank |
|
|
69
|
+
| Ranked below | `AFTER` | reference option id | Subject rank > reference rank |
|
|
70
|
+
|
|
71
|
+
Also supports `EXISTS` / `MISSING` for the whole question answer.
|
|
72
|
+
|
|
73
|
+
Works in skip, display, answer, question variant, and end-page logic via the shared `evaluateCondition` pipeline.
|
|
74
|
+
|
|
75
|
+
## SDK Helpers (client-safe)
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import {
|
|
79
|
+
assignRankWithoutDuplicates,
|
|
80
|
+
buildRankOrderFromOrderedOptionIds,
|
|
81
|
+
getOrderedOptionIdsFromRanks,
|
|
82
|
+
getVisibleRankOrderOptionsForAnswers,
|
|
83
|
+
normalizeRankOrderAnswers,
|
|
84
|
+
} from '@explorer02/cfm-survey-sdk';
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Component
|
|
88
|
+
|
|
89
|
+
`RankOrderScale` — see `01-components/18-rank-order-scale.md`
|
|
90
|
+
|
|
91
|
+
## SDK Integration Checklist (agent)
|
|
92
|
+
|
|
93
|
+
- [ ] Type literal: `QUESTION_TYPE.RANK_ORDER`
|
|
94
|
+
- [ ] Answer stored as: `RankOrderAnswers` (`Record<optionId, rank>`)
|
|
95
|
+
- [ ] onAction CHANGE payload: `{ questionId, answerValue: RankOrderAnswers }` after reorder
|
|
96
|
+
- [ ] Visibility helper: `getVisibleRankOrderOptionsForAnswers(question, answers, allQuestions, customFieldValues?)`
|
|
97
|
+
- [ ] Custom fields: 4th arg on rank visibility helper when logic references CRM fields
|
|
98
|
+
- [ ] Logic features: skip ✓ display ✓ answer ✓ (options) variants ✓ numbering ✓
|
|
99
|
+
- [ ] Performance: prefer `option.imageUrl` over `previewImageUrl`; install `@dnd-kit/*` for drag mode
|
|
100
|
+
- [ ] Value utils: `assignRankWithoutDuplicates`, `buildRankOrderFromOrderedOptionIds`, `getOrderedOptionIdsFromRanks`, `normalizeRankOrderAnswers`
|
|
101
|
+
- [ ] Matrix row: [`question-type-sdk-matrix.md#rank_order`](../../00-integration/question-type-sdk-matrix.md#rank_order)
|
|
@@ -1,71 +1,110 @@
|
|
|
1
1
|
# Question Types Reference
|
|
2
2
|
|
|
3
3
|
> Read **on demand** — only for types present in the fetched survey.
|
|
4
|
-
> UI implementation: `01-components/`. Routing table: `02-reference/routing-table.md`.
|
|
5
4
|
|
|
6
|
-
## Discriminated Union
|
|
5
|
+
## Discriminated Union (11 types)
|
|
7
6
|
|
|
8
7
|
```typescript
|
|
9
8
|
type SurveyQuestion =
|
|
10
|
-
|
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
| CsatQuestion // '
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
9
|
+
| McqQuestion // 'MCQ'
|
|
10
|
+
| NpsQuestion // 'NPS_SCALE'
|
|
11
|
+
| TextInputQuestion // 'TEXTFIELD'
|
|
12
|
+
| CsatQuestion // 'CSAT_MATRIX'
|
|
13
|
+
| CfmMatrixQuestion // 'CFM_MATRIX'
|
|
14
|
+
| RatingMatrixQuestion // 'RATING_MATRIX'
|
|
15
|
+
| SliderMatrixQuestion // 'SLIDER_MATRIX'
|
|
16
|
+
| FileUploadQuestion // 'FILE_UPLOAD'
|
|
17
|
+
| TextAndMediaQuestion // 'TEXT_AND_MEDIA'
|
|
18
|
+
| HeatmapQuestion // 'HEATMAP'
|
|
19
|
+
| RankOrderQuestion; // 'RANK_ORDER'
|
|
21
20
|
```
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
Use `QUESTION_TYPE` from the SDK for dispatch comparisons.
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
|
32
|
-
|
|
24
|
+
Each type is **standalone** — open its file in exported SDK types for the complete field list.
|
|
25
|
+
|
|
26
|
+
> **Legacy:** `StarRatingQuestion` / `SliderQuestion` mapper types are not in the `SurveyQuestion` union. API RATING/SLIDER pages map into `NPS_SCALE`, matrix display styles, or `SLIDER_MATRIX`.
|
|
27
|
+
|
|
28
|
+
## Shared field names (duplicated per type file)
|
|
29
|
+
|
|
30
|
+
| Property | Meaning |
|
|
31
|
+
|----------|---------|
|
|
32
|
+
| `id` | DOM `id` on wrapper |
|
|
33
|
+
| `questionText` | Main prompt (HTML) |
|
|
34
|
+
| `questionDescription?` | Supplementary HTML |
|
|
35
|
+
| `questionHint?` | Hint text |
|
|
36
|
+
| `required` | Mandatory flag |
|
|
37
|
+
| `requiredErrorMessage?` | Custom validation message |
|
|
38
|
+
| `questionNumber?` | 1-indexed position |
|
|
39
|
+
| `skipLogics?` | Forward skip rules evaluated on page leave |
|
|
40
|
+
| `displayLogic?` | Question-level show/hide (dynamic or page-level) |
|
|
41
|
+
| `displayVariants?` | Alternate `questionText` prompts selected by condition (SDK resolves on `currentQuestions`; see [`question-display-variants.md`](../../00-integration/question-display-variants.md)) |
|
|
42
|
+
| `answerLogic?` | On `McqOption`, `StatementRow`, or `ScaleColumn` — sub-entity show/hide (see [`answer-logic-and-navigation.md`](../../00-integration/answer-logic-and-navigation.md)) |
|
|
33
43
|
|
|
34
44
|
---
|
|
35
45
|
|
|
36
|
-
##
|
|
46
|
+
## MCQ {#mcq}
|
|
47
|
+
|
|
48
|
+
- **Type:** `McqQuestion` — `type: 'MCQ'`
|
|
49
|
+
- **Answer:** `string` or `string[]` (`option.id` values)
|
|
50
|
+
- **Options:** `McqOption[]` with `id`, `optionLabel`
|
|
51
|
+
|
|
52
|
+
### SDK Integration Checklist (agent)
|
|
37
53
|
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
54
|
+
- [ ] Type literal: `QUESTION_TYPE.MCQ`
|
|
55
|
+
- [ ] Answer stored as: `option.id` (string/number) or array when multi-select
|
|
56
|
+
- [ ] onAction CHANGE payload: `{ questionId, answerValue: optionId | optionId[] }`
|
|
57
|
+
- [ ] Visibility helper: `getVisibleMcqOptionsForAnswers(question, answers, allQuestions, customFieldValues?)`
|
|
58
|
+
- [ ] Custom fields: 4th arg when logic references CRM fields
|
|
59
|
+
- [ ] Logic features: skip ✓ display ✓ answer ✓ (options) variants ✓ numbering ✓
|
|
60
|
+
- [ ] Performance: filter options via helper before render
|
|
61
|
+
- [ ] Matrix row: [`question-type-sdk-matrix.md#mcq`](../../00-integration/question-type-sdk-matrix.md#mcq)
|
|
42
62
|
|
|
43
|
-
##
|
|
63
|
+
## TEXTFIELD {#textfield}
|
|
44
64
|
|
|
45
|
-
- **Type:** `
|
|
65
|
+
- **Type:** `TextInputQuestion` — `type: 'TEXTFIELD'`
|
|
46
66
|
- **Answer:** `string`
|
|
47
|
-
- **UI:** Inline `<textarea>` or `<input>` in `Question.tsx`
|
|
48
|
-
- **No separate reference doc needed**
|
|
49
67
|
|
|
50
|
-
|
|
68
|
+
### SDK Integration Checklist (agent)
|
|
51
69
|
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
70
|
+
- [ ] Type literal: `QUESTION_TYPE.TEXTFIELD`
|
|
71
|
+
- [ ] Answer stored as: `string`
|
|
72
|
+
- [ ] onAction CHANGE payload: `{ questionId, answerValue: string }`
|
|
73
|
+
- [ ] Visibility helper: N/A
|
|
74
|
+
- [ ] Custom fields: skip/display only
|
|
75
|
+
- [ ] Logic features: skip ✓ display ✓ answer ✗ variants ✓ numbering ✓
|
|
76
|
+
- [ ] Performance: debounce long textarea onChange if partial save enabled
|
|
77
|
+
- [ ] Matrix row: [`question-type-sdk-matrix.md#textfield`](../../00-integration/question-type-sdk-matrix.md#textfield)
|
|
78
|
+
|
|
79
|
+
## TEXT_AND_MEDIA {#text_and_media}
|
|
80
|
+
|
|
81
|
+
- **Type:** `TextAndMediaQuestion` — `type: 'TEXT_AND_MEDIA'`
|
|
82
|
+
- **Answer:** none — display-only
|
|
83
|
+
|
|
84
|
+
### SDK Integration Checklist (agent)
|
|
85
|
+
|
|
86
|
+
- [ ] Type literal: `QUESTION_TYPE.TEXT_AND_MEDIA`
|
|
87
|
+
- [ ] Answer stored as: none — no CHANGE action
|
|
88
|
+
- [ ] onAction: user advances with NEXT only
|
|
89
|
+
- [ ] Visibility helper: N/A
|
|
90
|
+
- [ ] Custom fields: skip/display only
|
|
91
|
+
- [ ] Logic features: skip ✓ display ✓ answer ✗ variants ✓ numbering ✓
|
|
92
|
+
- [ ] Performance: lazy-load media; respect `media.alignment`
|
|
93
|
+
- [ ] Matrix row: [`question-type-sdk-matrix.md#text_and_media`](../../00-integration/question-type-sdk-matrix.md#text_and_media)
|
|
56
94
|
|
|
57
95
|
---
|
|
58
96
|
|
|
59
97
|
## Detailed Reference Docs
|
|
60
98
|
|
|
61
|
-
| File | Type | When to read |
|
|
62
|
-
|
|
63
|
-
| `01-rating.md` | `
|
|
64
|
-
| `04-csat.md` | `
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
|
|
99
|
+
| File | Type literal | When to read |
|
|
100
|
+
|------|--------------|--------------|
|
|
101
|
+
| `01-rating.md` | `NPS_SCALE` | NPS 0–10 scales |
|
|
102
|
+
| `04-csat.md` | `CSAT_MATRIX` | CSAT matrix grid |
|
|
103
|
+
| `07-matrix-cfm.md` | `CFM_MATRIX` | CFM matrix grid |
|
|
104
|
+
| `09-matrix-rating.md` | `RATING_MATRIX` | NPS-style rating matrix |
|
|
105
|
+
| `10-slider-matrix.md` | `SLIDER_MATRIX` | Slider grid |
|
|
106
|
+
| `11-file-upload.md` | `FILE_UPLOAD` | File attachments |
|
|
107
|
+
| `12-heatmap.md` | `HEATMAP` | Click-map heatmap |
|
|
108
|
+
| `13-rank-order.md` | `RANK_ORDER` | Rank order options |
|
|
109
|
+
|
|
110
|
+
Legacy docs (deprecated API mappings): `05-rating-scale.md`, `06-slider.md`
|
|
@@ -1,35 +1,50 @@
|
|
|
1
1
|
# Question Type → Component Routing
|
|
2
2
|
|
|
3
|
-
> Canonical mapping.
|
|
4
|
-
|
|
5
|
-
| `question.type` |
|
|
6
|
-
|
|
7
|
-
| `
|
|
8
|
-
| `
|
|
9
|
-
| `
|
|
10
|
-
| `
|
|
11
|
-
| `
|
|
12
|
-
| `
|
|
13
|
-
| `
|
|
14
|
-
| `
|
|
15
|
-
| `
|
|
16
|
-
| `
|
|
17
|
-
| `
|
|
18
|
-
|
|
19
|
-
|
|
3
|
+
> Canonical mapping. Dispatch on `question.type` only — each matrix variant is its own type (no `subType`).
|
|
4
|
+
|
|
5
|
+
| `question.type` | SDK Type | Component | Answer Shape | Reference Doc | UI Spec |
|
|
6
|
+
|-----------------|----------|-----------|--------------|---------------|---------|
|
|
7
|
+
| `MCQ` | `McqQuestion` | Inline in `Question` | `string \| string[]` | `question-types/README.md#mcq` | `03-ui-specs/02-radio.md` |
|
|
8
|
+
| `TEXTFIELD` | `TextInputQuestion` | Inline in `Question` | `string` | `question-types/README.md#textfield` | `03-ui-specs/03-text.md` |
|
|
9
|
+
| `NPS_SCALE` | `NpsQuestion` | `RatingScale` | `number` | `question-types/01-rating.md` | `03-ui-specs/01-rating.md` |
|
|
10
|
+
| `CFM_MATRIX` | `CfmMatrixQuestion` | `LikertMatrixScale` | `MatrixRowAnswers` | `question-types/07-matrix-cfm.md` | `03-ui-specs/07-matrix-cfm.md` |
|
|
11
|
+
| `CSAT_MATRIX` | `CsatQuestion` | `CsatMatrixScale` | `MatrixRowAnswers` | `question-types/04-csat.md` | `03-ui-specs/04-csat.md` |
|
|
12
|
+
| `RATING_MATRIX` | `RatingMatrixQuestion` | `CsatMatrixScale` | `MatrixRowAnswers` | `question-types/09-matrix-rating.md` | `03-ui-specs/08-matrix-csat-rating.md` |
|
|
13
|
+
| `SLIDER_MATRIX` | `SliderMatrixQuestion` | `SliderMatrixScale` | `MatrixRowAnswers` | `question-types/10-slider-matrix.md` | `03-ui-specs/09-slider-matrix.md` |
|
|
14
|
+
| `FILE_UPLOAD` | `FileUploadQuestion` | `FileUploadScale` | `UploadedFileAnswer[]` | `question-types/11-file-upload.md` | `03-ui-specs/10-file-upload.md` |
|
|
15
|
+
| `TEXT_AND_MEDIA` | `TextAndMediaQuestion` | Inline in `Question` | (no answer) | `question-types/README.md#text_and_media` | `03-ui-specs/11-text-and-media.md` |
|
|
16
|
+
| `HEATMAP` | `HeatmapQuestion` | `HeatmapScale` | `HeatmapAnswer` | `question-types/12-heatmap.md` | `03-ui-specs/13-heatmap.md` |
|
|
17
|
+
| `RANK_ORDER` | `RankOrderQuestion` | `RankOrderScale` | `RankOrderAnswers` | `question-types/13-rank-order.md` | `03-ui-specs/14-rank-order.md` |
|
|
18
|
+
|
|
19
|
+
Use `QUESTION_TYPE` from the SDK for dispatch:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { QUESTION_TYPE } from '@explorer02/cfm-survey-sdk';
|
|
23
|
+
|
|
24
|
+
{question.type === QUESTION_TYPE.NPS_SCALE && <RatingScale ... />}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
> **Legacy note:** API RATING/SLIDER page types are mapped into `NPS_SCALE`, matrix display styles, or `SLIDER_MATRIX` — not separate `question.type` values.
|
|
28
|
+
|
|
29
|
+
## SDK hook contract
|
|
30
|
+
|
|
31
|
+
- Read answers from `state.answers` (`SurveyAnswers`)
|
|
32
|
+
- Dispatch `{ type: 'CHANGE', payload: { questionId, answerValue } }`
|
|
33
|
+
- Pass `allAnswers` and `allQuestions` to `Question` for placeholder resolution
|
|
20
34
|
|
|
21
35
|
## Component Blueprint Docs
|
|
22
36
|
|
|
23
37
|
| Component | Blueprint |
|
|
24
38
|
|-----------|-----------|
|
|
25
39
|
| `SurveyPage` | `01-components/01-survey-page.md` |
|
|
40
|
+
| `IntroPage` / `EndPage` / `PausedPage` | `01-components/14-intro-page.md`, `15-end-page.md`, `16-paused-page.md` |
|
|
26
41
|
| `Question` | `01-components/02-question.md` |
|
|
27
42
|
| `RatingScale` | `01-components/03-rating-scale.md` |
|
|
28
|
-
| `CsatScale` | `01-components/04-csat-scale.md` |
|
|
29
43
|
| `CsatMatrixScale` | `01-components/05-csat-matrix-scale.md` |
|
|
30
44
|
| `LikertMatrixScale` | `01-components/06-likert-matrix-scale.md` |
|
|
31
45
|
| `SliderMatrixScale` | `01-components/07-slider-matrix-scale.md` |
|
|
32
46
|
| `FileUploadScale` | `01-components/08-file-upload-scale.md` |
|
|
47
|
+
| `HeatmapScale` | `01-components/17-heatmap-scale.md` |
|
|
33
48
|
| `CustomSliderTrack` | `01-components/09-custom-slider-track.md` |
|
|
34
49
|
| `Header` / `Footer` | `01-components/10-header-footer.md` |
|
|
35
50
|
| `ProgressBar` | `01-components/11-progress-bar.md` |
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Client-Side Value Derivation
|
|
2
|
+
|
|
3
|
+
> SDK types expose **display** fields only for scale options and matrix columns. Submit values are derived at submit/partial-save time.
|
|
4
|
+
|
|
5
|
+
## Matrix columns (`ScaleColumn`)
|
|
6
|
+
|
|
7
|
+
**Import from SDK** (preferred):
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { matrixColumnStoredValue } from '@explorer02/cfm-survey-sdk';
|
|
11
|
+
// monorepo: import { matrixColumnStoredValue } from '@repo/sdk';
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
type ScaleColumn = { id: string; label: string };
|
|
16
|
+
|
|
17
|
+
// matrixColumnStoredValue(column) → column.id, except N/A → null
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- **Stored matrix answers** use `matrixColumnStoredValue(col)` → always the column **`id`** (string), except N/A → `null`
|
|
21
|
+
- Matrix answers: `MatrixRowAnswers = Record<statementRowId, columnId | columnId[] | null>`
|
|
22
|
+
- **Submit / partial save**: SDK `resolveSubmitValues` maps stored column ids → API label or numeric string (id-only lookup)
|
|
23
|
+
- N/A column: SDK adds `{ id: 'na', label: 'N/A' }` when `hasNotApplicableOption` is true
|
|
24
|
+
- Filter N/A from scale pickers: `scaleColumns.filter(col => col.id !== 'na')`
|
|
25
|
+
- Slider matrix rows still store numeric slider positions (unchanged)
|
|
26
|
+
|
|
27
|
+
Monorepo client mirror: `apps/client/src/lib/surveyUi/scaleValues.ts` exports the same helper for matrix UI components.
|
|
28
|
+
|
|
29
|
+
## NPS / star rating options
|
|
30
|
+
|
|
31
|
+
Options are `{ id: string; optionLabel: string }` — no `optionValue` on the type.
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
function npsValueFromOption(option: NpsScalePoint, index: number): number {
|
|
35
|
+
const numeric = Number(option.optionLabel);
|
|
36
|
+
return Number.isFinite(numeric) && option.optionLabel.trim() !== '' ? numeric : index;
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
NPS questions use `question.type === 'NPS_SCALE'`. Store a **number** in `SurveyAnswers`. Badge colors: traffic-light table keyed by numeric value (0–10).
|
|
41
|
+
|
|
42
|
+
## MCQ
|
|
43
|
+
|
|
44
|
+
- Selected answer: **`option.id`** (string), not a separate `optionValue` field
|
|
45
|
+
|
|
46
|
+
## Rank order
|
|
47
|
+
|
|
48
|
+
- Stored answer: **`RankOrderAnswers`** — `Record<optionId, rankNumber>` (1-based)
|
|
49
|
+
- Dropdown: use `assignRankWithoutDuplicates(current, optionId, rank)` before `onSelect`
|
|
50
|
+
- Drag-and-drop: use `buildRankOrderFromOrderedOptionIds(orderedOptionIds)` after reorder
|
|
51
|
+
- Submit / partial save:
|
|
52
|
+
- `questionToAnswers[questionId]` → ordered **option id** array (rank 1 → N)
|
|
53
|
+
- `optionLevelAnswers[`${questionId}-:-${optionId}`]` → `[String(rank)]`
|
|
54
|
+
- Filter visible options: `getVisibleRankOrderOptionsForAnswers(question, answers, allQuestions, customFieldValues?)`
|
|
55
|
+
|
|
56
|
+
## Media
|
|
57
|
+
|
|
58
|
+
- Shell side media: `question.containerMedia?.url`, `containerMedia?.mimeType`, etc.
|
|
59
|
+
- Text & media block: `question.media?.url`, `question.media?.title`, etc.
|
|
60
|
+
|
|
61
|
+
## Shared types (exported)
|
|
62
|
+
|
|
63
|
+
`StatementRow`, `ScaleColumn`, `ContainerMedia`, `QuestionMedia`, `SurveyAnswers`, `matrixColumnStoredValue` — see [`exports.md`](exports.md).
|
|
@@ -1,89 +1,33 @@
|
|
|
1
1
|
# UI Spec: Question Shell (`Question.tsx`)
|
|
2
2
|
|
|
3
|
-
> **
|
|
4
|
-
> **SDK:**
|
|
3
|
+
> **Canonical template:** [`templates/Question.tsx`](../templates/Question.tsx) — copy this dispatcher verbatim
|
|
4
|
+
> **SDK:** Each question type is standalone — see `fetchSurvey/types/*.ts`
|
|
5
5
|
> **Component blueprint:** `01-components/02-question.md`
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Matrix routing (CRITICAL)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|-------|-------------|-----------|
|
|
11
|
-
| `id` | ✅ | `section id={question.id}` — required for scroll-to-error |
|
|
12
|
-
| `text` | ✅ | Heading via `dangerouslySetInnerHTML` |
|
|
13
|
-
| `description` | ✅ | Optional HTML block below title |
|
|
14
|
-
| `required` | ✅ | Red `*` after title (`text-red-500`) |
|
|
15
|
-
| `requiredErrorMessage` | ✅ | Shown via SDK validation in error banner |
|
|
16
|
-
| `questionNumber` | ❌ | Not rendered in reference client |
|
|
17
|
-
| `containerMediaUrl` | ❌ | Not rendered on generic questions |
|
|
18
|
-
|
|
19
|
-
## Layout Anatomy
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
┌─ section (rounded-xl border border-gray-200 bg-white p-6 shadow-sm) ─────┐
|
|
23
|
-
│ h2: question.text (HTML) + red * if required │
|
|
24
|
-
│ [optional description HTML] │
|
|
25
|
-
│ ┌─ type-specific scale (see per-type specs) ─────────────────────┐ │
|
|
26
|
-
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
27
|
-
│ [validation error banner if validationError prop] │
|
|
28
|
-
└─────────────────────────────────────────────────────────────────────────┘
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Dispatcher Routing
|
|
32
|
-
|
|
33
|
-
| `question.type` | Condition | Component |
|
|
34
|
-
|-----------------|-----------|-----------|
|
|
35
|
-
| `rating` | — | `RatingScale` + label row in Question |
|
|
36
|
-
| `radio` | — | Inline card radios in Question |
|
|
37
|
-
| `text` | — | Inline textarea in Question |
|
|
38
|
-
| `csat` | — | `CsatScale` + min/max labels in Question |
|
|
39
|
-
| `matrix` | `CSAT_MATRIX` \| `RATING_MATRIX` | `CsatMatrixScale` |
|
|
40
|
-
| `matrix` | `CFM_MATRIX` | `LikertMatrixScale` |
|
|
41
|
-
| `slider_matrix` | — | `SliderMatrixScale` |
|
|
42
|
-
| `file_upload` | — | `FileUploadScale` |
|
|
43
|
-
| `text_and_media` | `mediaUrl` present | Inline media in Question |
|
|
44
|
-
| `slider` | — | ❌ Not routed — see `06-slider.md` |
|
|
45
|
-
| `rating_scale` | — | ❌ Not routed — see `05-rating-scale.md` |
|
|
46
|
-
|
|
47
|
-
## Label Positioning (rating)
|
|
48
|
-
|
|
49
|
-
Rendered in Question before `RatingScale`:
|
|
50
|
-
|
|
51
|
-
```tsx
|
|
52
|
-
// midLabel CSS left:
|
|
53
|
-
left: `${(midLabelIndex / (options.length - 1)) * 100}%`
|
|
54
|
-
// with -translate-x-1/2 for centering
|
|
55
|
-
```
|
|
9
|
+
Each matrix variant has its own `question.type` — **no `subType`**:
|
|
56
10
|
|
|
57
|
-
|
|
11
|
+
- `CFM_MATRIX` → `LikertMatrixScale`
|
|
12
|
+
- `CSAT_MATRIX` | `RATING_MATRIX` → `CsatMatrixScale`
|
|
13
|
+
- `SLIDER_MATRIX` → `SliderMatrixScale`
|
|
58
14
|
|
|
59
|
-
|
|
15
|
+
Never use a `default` fallback that shows `"not yet implemented"`.
|
|
60
16
|
|
|
61
|
-
|
|
17
|
+
## Shell fields (duplicated on every question type)
|
|
62
18
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
};
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Wire `onSelect` → `onAction({ type: 'CHANGE', payload: { questionId, value } })`.
|
|
82
|
-
|
|
83
|
-
## Agent Checklist
|
|
84
|
-
|
|
85
|
-
- [ ] `id={question.id}` on outermost `<section>`
|
|
86
|
-
- [ ] HTML rendering for text and description
|
|
87
|
-
- [ ] Exhaustive type switch — all 12 types handled or explicitly noted as gap
|
|
88
|
-
- [ ] Validation error banner below scale
|
|
89
|
-
- [ ] Rating/csat endpoint labels positioned per formulas above
|
|
19
|
+
| Field | Implemented | UI impact |
|
|
20
|
+
|-------|-------------|-----------|
|
|
21
|
+
| `id` | ✅ | `section id={question.id}` — scroll-to-error |
|
|
22
|
+
| `questionText` | ✅ | Heading via `dangerouslySetInnerHTML`; when the survey defines `displayVariants`, the SDK resolves the active variant text on `state.currentQuestions` before render |
|
|
23
|
+
| `questionDescription` | ✅ | Optional HTML below title |
|
|
24
|
+
| `questionHint` | ❌ | Optional hint below description |
|
|
25
|
+
| `required` | ✅ | Red `*` after title |
|
|
26
|
+
| `requiredErrorMessage` | ✅ | Validation error banner |
|
|
27
|
+
| `questionNumber` | ✅ | Prefix before title (e.g. `3.`); SDK resolves active-path ordinals on `state.currentQuestions` (see [`question-numbering.md`](../../00-integration/question-numbering.md)) |
|
|
28
|
+
| `containerMedia` | ❌ | Side media — `containerMedia?.url` etc.; not in reference client |
|
|
29
|
+
|
|
30
|
+
## SDK hook
|
|
31
|
+
|
|
32
|
+
- Answers: `state.answers` (not `state.values`)
|
|
33
|
+
- Change action: `{ type: 'CHANGE', payload: { questionId, answerValue } }`
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
# UI Spec: NPS Rating (`type: '
|
|
1
|
+
# UI Spec: NPS Rating (`type: 'NPS_SCALE'`)
|
|
2
2
|
|
|
3
|
-
> **
|
|
4
|
-
> **SDK type:** `RatingQuestion` — `fetchSurvey/types/rating.ts`
|
|
3
|
+
> **SDK type:** `NpsQuestion` — `fetchSurvey/types/nps.ts`
|
|
5
4
|
> **Component blueprint:** `01-components/03-rating-scale.md`
|
|
5
|
+
> **Value derivation:** `02-reference/value-derivation.md`
|
|
6
6
|
|
|
7
7
|
## Config Inventory
|
|
8
8
|
|
|
9
9
|
| Field | Implemented | UI impact |
|
|
10
10
|
|-------|-------------|-----------|
|
|
11
11
|
| `options[]` | ✅ | Badge row + radio row (dual layer) |
|
|
12
|
-
| `options[].
|
|
13
|
-
| `options[].
|
|
14
|
-
|
|
|
12
|
+
| `options[].optionLabel` | ✅ | Displayed in badge |
|
|
13
|
+
| `options[].id` | ✅ | Stable option id (display only) |
|
|
14
|
+
| Submit value | ✅ derived | Parse `optionLabel` as number or use index — see value-derivation |
|
|
15
|
+
| Badge color | ✅ derived | Traffic-light table from numeric value |
|
|
15
16
|
| `minLabel` | ✅ | Rendered in Question.tsx above scale |
|
|
16
17
|
| `midLabel` | ✅ | Absolute positioned in Question.tsx |
|
|
17
18
|
| `maxLabel` | ✅ | Absolute positioned in Question.tsx |
|
|
@@ -19,62 +20,19 @@
|
|
|
19
20
|
| `reverseScaleOrder` | ❌ | Document for agents when SDK sends true |
|
|
20
21
|
| `buttonVariant` | ❌ | Client always uses dual-layer radio layout |
|
|
21
22
|
|
|
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
23
|
## Answer Mutation
|
|
62
24
|
|
|
63
25
|
```typescript
|
|
64
|
-
|
|
65
|
-
//
|
|
26
|
+
const value = npsValueFromOption(option, index);
|
|
27
|
+
onSelect(value) // number
|
|
28
|
+
// → onAction CHANGE { questionId, answerValue: number }
|
|
66
29
|
```
|
|
67
30
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
- `role="radiogroup"` on wrapper
|
|
71
|
-
- Native `<input type="radio" className="sr-only">` per cell
|
|
72
|
-
- `name={questionId}` groups radios
|
|
31
|
+
See [`02-reference/value-derivation.md`](../02-reference/value-derivation.md).
|
|
73
32
|
|
|
74
33
|
## Agent Checklist
|
|
75
34
|
|
|
35
|
+
- [ ] Derive submit value and badge color in UI — types omit `optionValue` / `accentColor`
|
|
76
36
|
- [ ] Two-row grid with `gridTemplateColumns: repeat(options.length, 1fr)`
|
|
77
|
-
- [ ] Badges use `option.color`, not hardcoded NPS colors unless option lacks color
|
|
78
37
|
- [ ] Only track row is clickable
|
|
79
38
|
- [ ] min/mid/max labels in Question wrapper with midLabelIndex formula
|
|
80
|
-
- [ ] Selected state: pink inner track + thick magenta radio ring
|