@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.
Files changed (88) hide show
  1. package/dist/cli/index.js +18 -18
  2. package/dist/cli/index.mjs +14 -14
  3. package/dist/index.d.mts +465 -580
  4. package/dist/index.d.ts +465 -580
  5. package/dist/index.js +1 -1
  6. package/dist/index.mjs +1 -1
  7. package/package.json +6 -4
  8. package/postinstall.js +96 -64
  9. package/templates/AGENT.md +15 -7
  10. package/templates/docs/00-integration/answer-logic-and-navigation.md +50 -0
  11. package/templates/docs/00-integration/aws-deploy.md +281 -0
  12. package/templates/docs/00-integration/client-integration-guide.md +351 -0
  13. package/templates/docs/00-integration/component-checklist.md +84 -0
  14. package/templates/docs/00-integration/constraints.md +65 -3
  15. package/templates/docs/00-integration/custom-field-logic-and-navigation.md +174 -0
  16. package/templates/docs/00-integration/display-logic-and-navigation.md +49 -0
  17. package/templates/docs/00-integration/end-page-logic.md +64 -0
  18. package/templates/docs/00-integration/partial-save-and-recovery.md +74 -0
  19. package/templates/docs/00-integration/placeholders-and-tokens.md +116 -0
  20. package/templates/docs/00-integration/progress.md +61 -0
  21. package/templates/docs/00-integration/question-display-variants.md +65 -0
  22. package/templates/docs/00-integration/question-numbering.md +51 -0
  23. package/templates/docs/00-integration/question-type-sdk-matrix.md +242 -0
  24. package/templates/docs/00-integration/setup.md +84 -0
  25. package/templates/docs/00-integration/skip-logic-and-navigation.md +80 -0
  26. package/templates/docs/00-integration/survey-lifecycle-analytics.md +94 -0
  27. package/templates/docs/00-integration/useSurveySDK.md +151 -23
  28. package/templates/docs/01-components/01-survey-page.md +76 -54
  29. package/templates/docs/01-components/02-question.md +77 -89
  30. package/templates/docs/01-components/03-rating-scale.md +11 -1
  31. package/templates/docs/01-components/04-star-rating-scale.md +45 -0
  32. package/templates/docs/01-components/05-csat-matrix-scale.md +20 -11
  33. package/templates/docs/01-components/06-likert-matrix-scale.md +12 -3
  34. package/templates/docs/01-components/07-slider-matrix-scale.md +11 -2
  35. package/templates/docs/01-components/08-file-upload-scale.md +10 -1
  36. package/templates/docs/01-components/09-custom-slider-track.md +2 -2
  37. package/templates/docs/01-components/11-progress-bar.md +10 -4
  38. package/templates/docs/01-components/13-matrix-dropdown.md +3 -1
  39. package/templates/docs/01-components/14-intro-page.md +34 -0
  40. package/templates/docs/01-components/15-end-page.md +31 -0
  41. package/templates/docs/01-components/16-paused-page.md +27 -0
  42. package/templates/docs/01-components/17-heatmap-scale.md +75 -0
  43. package/templates/docs/01-components/18-rank-order-scale.md +100 -0
  44. package/templates/docs/01-components/README.md +44 -36
  45. package/templates/docs/02-reference/config-field-index.md +141 -139
  46. package/templates/docs/02-reference/exports.md +149 -0
  47. package/templates/docs/02-reference/question-types/01-rating.md +31 -32
  48. package/templates/docs/02-reference/question-types/04-csat.md +51 -42
  49. package/templates/docs/02-reference/question-types/05-rating-scale.md +16 -11
  50. package/templates/docs/02-reference/question-types/06-slider.md +2 -2
  51. package/templates/docs/02-reference/question-types/07-matrix-cfm.md +44 -30
  52. package/templates/docs/02-reference/question-types/08-matrix-csat.md +4 -26
  53. package/templates/docs/02-reference/question-types/09-matrix-rating.md +29 -13
  54. package/templates/docs/02-reference/question-types/10-slider-matrix.md +15 -4
  55. package/templates/docs/02-reference/question-types/11-file-upload.md +13 -2
  56. package/templates/docs/02-reference/question-types/12-heatmap.md +59 -0
  57. package/templates/docs/02-reference/question-types/13-rank-order.md +101 -0
  58. package/templates/docs/02-reference/question-types/README.md +86 -47
  59. package/templates/docs/02-reference/routing-table.md +33 -18
  60. package/templates/docs/02-reference/value-derivation.md +63 -0
  61. package/templates/docs/03-ui-specs/00-question-shell.md +24 -80
  62. package/templates/docs/03-ui-specs/01-rating.md +12 -54
  63. package/templates/docs/03-ui-specs/02-radio.md +21 -33
  64. package/templates/docs/03-ui-specs/03-text.md +2 -2
  65. package/templates/docs/03-ui-specs/04-csat.md +38 -33
  66. package/templates/docs/03-ui-specs/05-rating-scale.md +21 -18
  67. package/templates/docs/03-ui-specs/06-slider.md +3 -1
  68. package/templates/docs/03-ui-specs/07-matrix-cfm.md +25 -22
  69. package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +18 -46
  70. package/templates/docs/03-ui-specs/09-slider-matrix.md +2 -2
  71. package/templates/docs/03-ui-specs/10-file-upload.md +2 -2
  72. package/templates/docs/03-ui-specs/11-text-and-media.md +29 -29
  73. package/templates/docs/03-ui-specs/12-survey-chrome.md +20 -14
  74. package/templates/docs/03-ui-specs/13-heatmap.md +73 -0
  75. package/templates/docs/03-ui-specs/14-rank-order.md +59 -0
  76. package/templates/docs/03-ui-specs/README.md +14 -13
  77. package/templates/docs/03-ui-specs/shared/custom-slider-track.md +3 -3
  78. package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +6 -1
  79. package/templates/docs/MANIFEST.json +454 -67
  80. package/templates/docs/index.md +84 -29
  81. package/templates/docs/templates/Question.tsx +310 -0
  82. package/templates/docs/templates/deploy-to-aws.sh +114 -0
  83. package/templates/docs/templates/heatmapCoords.ts +58 -0
  84. package/templates/docs/templates/implementation_plan.md +115 -49
  85. package/templates/docs/templates/survey-inventory.schema.json +72 -0
  86. package/templates/docs/templates/verify-agent-build.sh +53 -0
  87. package/templates/docs/01-components/04-csat-scale.md +0 -40
  88. 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
- | RatingQuestion // 'rating'
11
- | RadioQuestion // 'radio'
12
- | TextQuestion // 'text'
13
- | CsatQuestion // 'csat'
14
- | RatingScaleQuestion // 'rating_scale'
15
- | SliderQuestion // 'slider'
16
- | MatrixQuestion // 'matrix' (check subType)
17
- | RatingMatrixQuestion // 'rating_matrix'
18
- | SliderMatrixQuestion // 'slider_matrix'
19
- | FileUploadQuestion // 'file_upload'
20
- | TextAndMediaQuestion; // 'text_and_media'
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
- ## Shared `QuestionBase` Properties
22
+ Use `QUESTION_TYPE` from the SDK for dispatch comparisons.
24
23
 
25
- | Property | Type | Notes |
26
- |----------|------|-------|
27
- | `id` | `string` | **MUST** be DOM `id` on wrapper |
28
- | `text` | `string` | HTML — use `dangerouslySetInnerHTML` |
29
- | `description` | `string?` | HTML |
30
- | `required` | `boolean?` | |
31
- | `requiredErrorMessage` | `string?` | Custom validation message |
32
- | `questionNumber` | `number?` | 1-indexed |
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
- ## radio {#radio}
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
- - **Type:** `RadioQuestion` `type: 'radio'`
39
- - **Answer:** `string | number` (option id or value)
40
- - **UI:** Inline MCQ radio cards in `Question.tsx` see `01-components/02-question.md`
41
- - **No separate reference doc needed**
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
- ## text {#text}
63
+ ## TEXTFIELD {#textfield}
44
64
 
45
- - **Type:** `TextQuestion` — `type: 'text'`
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
- ## text_and_media {#text_and_media}
68
+ ### SDK Integration Checklist (agent)
51
69
 
52
- - **Type:** `TextAndMediaQuestion` `type: 'text_and_media'`
53
- - **Answer:** none display-only, skipped in validation and submission
54
- - **UI:** Inline `<video>` or `<img>` from `question.mediaUrl` no scale component
55
- - **No separate reference doc needed**
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` | `rating` | NPS / numeric scales |
64
- | `04-csat.md` | `csat` | CSAT emoji/star (≤5 options) |
65
- | `05-rating-scale.md` | `rating_scale` | Star/emoji rating |
66
- | `06-slider.md` | `slider` | Single range slider |
67
- | `07-matrix-cfm.md` | `matrix` + `CFM_MATRIX` | Likert text grid |
68
- | `08-matrix-csat.md` | `matrix` + CSAT/RATING | Satisfaction grid |
69
- | `09-matrix-rating.md` | `rating_matrix` | NPS-style grid |
70
- | `10-slider-matrix.md` | `slider_matrix` | Slider grid |
71
- | `11-file-upload.md` | `file_upload` | File attachments |
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. For **UI implementation** (config branches, tokens, states), read `03-ui-specs/`. For wiring, read `01-components/`. For data shapes, read `02-reference/question-types/`. For field audit, read `config-field-index.md`.
4
-
5
- | `question.type` | `question.subType` | Component | Answer Shape | Reference Doc | UI Spec |
6
- |-----------------|-------------------|-----------|--------------|---------------|---------|
7
- | `'rating'` | | `RatingScale` | `number` | `question-types/01-rating.md` | `03-ui-specs/01-rating.md` |
8
- | `'radio'` | | Inline in `Question` | `string \| number` | `question-types/README.md#radio` | `03-ui-specs/02-radio.md` |
9
- | `'text'` | | Inline in `Question` | `string` | `question-types/README.md#text` | `03-ui-specs/03-text.md` |
10
- | `'csat'` | | `CsatScale` | `number \| null` | `question-types/04-csat.md` | `03-ui-specs/04-csat.md` |
11
- | `'rating_scale'` | | `RatingScale` | `number` | `question-types/05-rating-scale.md` | `03-ui-specs/05-rating-scale.md` |
12
- | `'slider'` | | `CustomSliderTrack` | `number` | `question-types/06-slider.md` | `03-ui-specs/06-slider.md` |
13
- | `'matrix'` | `'CFM_MATRIX'` | `LikertMatrixScale` | `MatrixAnswerMap` | `question-types/07-matrix-cfm.md` | `03-ui-specs/07-matrix-cfm.md` |
14
- | `'matrix'` | `'CSAT_MATRIX'` | `CsatMatrixScale` | `MatrixAnswerMap` | `question-types/08-matrix-csat.md` | `03-ui-specs/08-matrix-csat-rating.md` |
15
- | `'matrix'` | `'RATING_MATRIX'` | `CsatMatrixScale` | `MatrixAnswerMap` | `question-types/08-matrix-csat.md` | `03-ui-specs/08-matrix-csat-rating.md` |
16
- | `'rating_matrix'` | | `CsatMatrixScale` | `MatrixAnswerMap` | `question-types/09-matrix-rating.md` | `03-ui-specs/08-matrix-csat-rating.md` |
17
- | `'slider_matrix'` | | `SliderMatrixScale` | `MatrixAnswerMap` | `question-types/10-slider-matrix.md` | `03-ui-specs/09-slider-matrix.md` |
18
- | `'file_upload'` | — | `FileUploadScale` | `UploadedFile[]` | `question-types/11-file-upload.md` | `03-ui-specs/10-file-upload.md` |
19
- | `'text_and_media'` | | Inline in `Question` | (no answer) | `question-types/README.md#text_and_media` | `03-ui-specs/11-text-and-media.md` |
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
- > **Reference:** `apps/client/src/components/Question.tsx`
4
- > **SDK:** All types extend `QuestionBase` from `fetchSurvey/types/base.ts`
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
- ## Config Inventory (QuestionBase)
7
+ ## Matrix routing (CRITICAL)
8
8
 
9
- | Field | Implemented | UI impact |
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
- minLabel: `absolute left-0`, maxLabel: `absolute right-0`, container `relative h-6 text-xs font-semibold text-gray-500`.
11
+ - `CFM_MATRIX` `LikertMatrixScale`
12
+ - `CSAT_MATRIX` | `RATING_MATRIX` → `CsatMatrixScale`
13
+ - `SLIDER_MATRIX` → `SliderMatrixScale`
58
14
 
59
- ## Label Positioning (csat)
15
+ Never use a `default` fallback that shows `"not yet implemented"`.
60
16
 
61
- minLabel / maxLabel: `flex justify-between text-xs font-semibold text-gray-500` above `CsatScale`.
17
+ ## Shell fields (duplicated on every question type)
62
18
 
63
- ## Validation Error Banner
64
-
65
- ```tsx
66
- className="mt-4 rounded-[4px] border border-[#333333] bg-[#d9d9d9] py-3.5 px-4"
67
- // text: text-[13px] font-bold text-gray-900
68
- ```
69
-
70
- ## Props Contract
71
-
72
- ```typescript
73
- type QuestionProps = {
74
- question: SurveyQuestion;
75
- selectedValue?: AnswerValue;
76
- validationError?: string;
77
- onSelect: (value: AnswerValue) => void;
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: 'rating'`)
1
+ # UI Spec: NPS Rating (`type: 'NPS_SCALE'`)
2
2
 
3
- > **Reference:** `apps/client/src/components/RatingScale.tsx` + label row in `Question.tsx`
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[].color` | ✅ | Badge background; fallback `#e20074` |
13
- | `options[].label` | ✅ | Displayed in badge |
14
- | `options[].value` | ✅ | Selection value |
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
- onSelect(option.value) // number
65
- // → onAction CHANGE { questionId, value: number }
26
+ const value = npsValueFromOption(option, index);
27
+ onSelect(value) // number
28
+ // → onAction CHANGE { questionId, answerValue: number }
66
29
  ```
67
30
 
68
- ## Accessibility
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