@explorer02/cfm-survey-sdk 0.2.1 → 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 -69
  9. package/templates/AGENT.md +14 -8
  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 +45 -22
  14. package/templates/docs/00-integration/constraints.md +49 -4
  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 +61 -58
  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 +42 -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 +19 -88
  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 +24 -21
  69. package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +17 -45
  70. package/templates/docs/03-ui-specs/09-slider-matrix.md +1 -1
  71. package/templates/docs/03-ui-specs/10-file-upload.md +1 -1
  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 +13 -13
  77. package/templates/docs/03-ui-specs/shared/custom-slider-track.md +2 -2
  78. package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +5 -0
  79. package/templates/docs/MANIFEST.json +451 -69
  80. package/templates/docs/index.md +80 -29
  81. package/templates/docs/templates/Question.tsx +221 -154
  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 +108 -69
  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
@@ -13,9 +13,7 @@ import {
13
13
  type UseSurveySDKReturn,
14
14
  type SurveyQuestion,
15
15
  type AnswerValue,
16
- type MatrixAnswerMap,
17
- CsatEmojiSet,
18
- getEmojiForIndex,
16
+ type MatrixRowAnswers,
19
17
  } from '@explorer02/cfm-survey-sdk'; // or @repo/sdk in monorepo
20
18
  ```
21
19
 
@@ -28,12 +26,14 @@ Ask the client: *"Please provide your survey instance ID (JWT token)."*
28
26
  ## Hook Signature
29
27
 
30
28
  ```typescript
31
- const { surveyQueryResults, submitSurveyResults, state, onAction } = useSurveySDK({
29
+ const { surveyQueryResults, submitSurveyResults, partialSaveResults, state, onAction } = useSurveySDK({
32
30
  options: {
33
31
  instanceId: string, // JWT from client
34
32
  language: string | undefined, // Active language code
35
33
  placeholders: Record<string, string>, // Token replacements — MUST be stable ref
34
+ customFieldValues?: CustomFieldValueMap, // CRM/transaction custom field values — MUST be stable ref
36
35
  debug: boolean, // Console logging
36
+ savePartialResponse: boolean | { debounceMs?: number }, // Optional partial save (default 1500ms)
37
37
  },
38
38
  });
39
39
  ```
@@ -42,9 +42,52 @@ Define `placeholders` **outside** the component to avoid infinite re-fetch loops
42
42
 
43
43
  ```typescript
44
44
  const SURVEY_PLACEHOLDERS = { FIRST_NAME: 'John' };
45
- // inside component: options: { placeholders: SURVEY_PLACEHOLDERS }
46
45
  ```
47
46
 
47
+ Define `customFieldValues` **outside** the component (same stable-ref rule as `placeholders`):
48
+
49
+ ```typescript
50
+ import type { CustomFieldValueMap } from '@explorer02/cfm-survey-sdk';
51
+
52
+ const CUSTOM_FIELD_VALUES: CustomFieldValueMap = {
53
+ '_c_6a436c1244764c9d4a6ce2fc': 'q5', // TEXT — string
54
+ '_c_6a436c3644764c9d4a6ce3f5': 2, // NUMBER — number or numeric string
55
+ '_c_6a436c9e44764c9d4a6ce717': 'option-id', // PICKLIST — option id string
56
+ '_c_6a436cbb44764c9d4a6ce7e5': ['opt-a'], // PICKLIST_MULTISELECT — option id array
57
+ '_c_6a4386dc44208eb8b08a9056': 1781029800000, // DATETIME — epoch ms (recommended)
58
+ };
59
+ ```
60
+
61
+ Wrap hook options in `useMemo` inside the component:
62
+
63
+ ```typescript
64
+ const surveyOptions = useMemo(
65
+ () => ({
66
+ instanceId,
67
+ language: selectedLanguage,
68
+ placeholders: SURVEY_PLACEHOLDERS,
69
+ customFieldValues: CUSTOM_FIELD_VALUES,
70
+ }),
71
+ [selectedLanguage]
72
+ );
73
+
74
+ useSurveySDK({ options: surveyOptions });
75
+ ```
76
+
77
+ Keys are the `_c_…` custom field ids from survey logic rules (`conditionEntity.id` in skip/display/answer logic). The SDK evaluates skip, display, and answer logic against these values when passed via hook options.
78
+
79
+ | Custom field type | Client value shape |
80
+ |---|---|
81
+ | TEXT / TEXT_MULTI / TEXTAREA | `string` |
82
+ | NUMBER | `number` or numeric string (`0` is a valid value) |
83
+ | PICKLIST | `string` (option id) |
84
+ | PICKLIST_MULTISELECT | `string[]` (option ids) |
85
+ | DATETIME | epoch ms (`number` or numeric string); ISO date strings also accepted |
86
+
87
+ Missing ids in the map are treated as empty for logic evaluation.
88
+
89
+ When the survey uses custom-field logic, also pass the same map into `Question.tsx` visibility helpers — see [`custom-field-logic-and-navigation.md`](custom-field-logic-and-navigation.md).
90
+
48
91
  ## Return Shape
49
92
 
50
93
  | Namespace | Field | Type | Purpose |
@@ -55,42 +98,129 @@ const SURVEY_PLACEHOLDERS = { FIRST_NAME: 'John' };
55
98
  | `submitSurveyResults` | `data` | `SubmitResult \| null` | Success payload |
56
99
  | | `error` | `Error \| null` | Submit failure |
57
100
  | | `isLoading` | `boolean` | Submit in progress |
58
- | `state` | `currentPageIndex` | `number` | 0-based page index |
59
- | | `currentQuestions` | `SurveyQuestion[]` | Questions on current page |
60
- | | `values` | `Record<string, AnswerValue>` | All answers so far |
101
+ | `partialSaveResults` | `data` | `SubmitResult \| null` | Last partial save success payload (when enabled) |
102
+ | | `error` | `Error \| null` | Partial save failure |
103
+ | | `isLoading` | `boolean` | Partial save in progress |
104
+ | `state` | `phase` | `SurveyPhase` | Survey flow phase (see below) |
105
+ | | `currentPageIndex` | `number` | 0-based page index (when `phase.kind === 'content'`) |
106
+ | | `currentQuestions` | `SurveyQuestion[]` | Questions on current page; `questionText` and `questionNumber` resolved by SDK |
107
+ | | `answers` | `SurveyAnswers` | All answers so far |
61
108
  | | `validationErrors` | `Record<string, string>` | Per-question error messages |
62
- | | `progressPercentage` | `number` | 0–100 completion |
109
+ | | `progressPercentage` | `number` | 0–100 path-scoped completion (see `progress.md`) |
110
+ | | `totalQuestionsCount` | `number` | Answerable questions on active path (progress denominator) |
111
+ | | `answeredQuestionsCount` | `number` | Earned credits on active path (progress numerator) |
112
+ | | `canGoBack` | `boolean` | Show Back button |
113
+ | | `primaryButtonType` | `'next' \| 'submit'` | Primary nav label; SDK maps `NEXT` → `SUBMIT` when `'submit'` |
114
+ | | `specialPage` | `SpecialPage \| undefined` | Intro / end / paused page content when applicable |
63
115
  | `onAction` | — | `(action: SurveyAction) => void` | **Only** way to mutate state |
64
116
 
117
+ ### Survey phases (`state.phase.kind`)
118
+
119
+ | Phase | When | UI |
120
+ |-------|------|-----|
121
+ | `intro` | Survey has intro page | Render intro; dispatch `START` |
122
+ | `content` | Active questions | Question map + nav |
123
+ | `end` | Successful submit | End/thank-you page from `specialPage` |
124
+ | `paused` | Survey paused flag | Paused message (read-only) |
125
+ | `submitted` | Already filled | Terminal message |
126
+ | `expired` | Survey expired | Terminal message |
127
+ | `quota_fulfilled` | Quota reached | Terminal message |
128
+
65
129
  ## Action Dispatching
66
130
 
67
131
  Never mutate `state` directly. Use `onAction`:
68
132
 
69
133
  ```typescript
70
134
  // Answer a question
71
- onAction({ type: ACTIONS.CHANGE, payload: { questionId: 'q1', value: 4 } });
135
+ onAction({ type: ACTIONS.CHANGE, payload: { questionId: 'q1', answerValue: 4 } });
72
136
 
73
137
  // Matrix answer
74
- onAction({ type: ACTIONS.CHANGE, payload: { questionId: 'q2', value: { row1: 5, row2: 2 } } });
138
+ onAction({ type: ACTIONS.CHANGE, payload: { questionId: 'q2', answerValue: { row1: 5, row2: 2 } } });
139
+
140
+ // Intro → content
141
+ onAction({ type: ACTIONS.START });
75
142
 
76
143
  // Navigate
77
- onAction({ type: ACTIONS.NEXT }); // Validates current page; auto-SUBMIT on last page
78
- onAction({ type: ACTIONS.PREVIOUS });
79
- onAction({ type: ACTIONS.SUBMIT }); // Validates ALL pages, then submits
144
+ onAction({ type: ACTIONS.NEXT }); // Validates current page; maps to SUBMIT when primaryButtonType is 'submit'
145
+ onAction({ type: ACTIONS.PREVIOUS }); // Uses pageHistory (skip-aware Back)
146
+ onAction({ type: ACTIONS.SUBMIT }); // Validates non-skipped pages, then submits
147
+
148
+ // Lifecycle (client detects scroll / visibility, SDK handles API calls)
149
+ onAction({ type: ACTIONS.SCROLL }); // First scroll — marks instance as started (once)
150
+ onAction({
151
+ type: ACTIONS.RECORD_QUESTION_VIEW,
152
+ payload: { questionIds: ['q1'] }, // Question entered viewport
153
+ });
80
154
  ```
81
155
 
82
- String literals also work: `'NEXT'`, `'PREVIOUS'`, `'SUBMIT'`, `'CHANGE'`.
156
+ String literals also work: `'START'`, `'NEXT'`, `'PREVIOUS'`, `'SUBMIT'`, `'CHANGE'`, `'SCROLL'`, `'RECORD_QUESTION_VIEW'`.
83
157
 
84
158
  ## Validation
85
159
 
86
- - Required questions must be answered before `NEXT` or `SUBMIT`
160
+ - Required questions must be answered before `NEXT` or `SUBMIT` on the **current page**
161
+ - `NEXT` validates current page only; skip logic may bypass intermediate pages
162
+ - `SUBMIT` validates **non-skipped pages** only (pages bypassed by skip logic are excluded)
87
163
  - `null` is a valid answer (N/A option)
88
- - `text_and_media` is display-only — always skipped
164
+ - `TEXT_AND_MEDIA` is display-only — always skipped for validation
89
165
  - On failure, SDK scrolls to first error via `document.getElementById(questionId)` — **requires `id={question.id}` on question wrappers**
90
166
 
167
+ See [`skip-logic-and-navigation.md`](skip-logic-and-navigation.md) for skip, Back, and answer-sanitize behavior.
168
+
91
169
  ## Submission
92
170
 
93
- Triggered by `onAction({ type: ACTIONS.SUBMIT })` or `NEXT` on the last page. While submitting, `submitSurveyResults.isLoading` is `true`. On success, show thank-you UI when `submitSurveyResults.data` is set.
171
+ Triggered by `onAction({ type: ACTIONS.SUBMIT })` or `NEXT` when `primaryButtonType === 'submit'`. While submitting, `submitSurveyResults.isLoading` is `true`. On success, render end UI when `state.phase.kind === 'end'` (and `state.specialPage` for content). `submitSurveyResults.data` confirms API success.
172
+
173
+ ## Mark as Started
174
+
175
+ On the **first scroll or answer change**, the client should dispatch `onAction({ type: ACTIONS.SCROLL })` (once, on first scroll) and/or the SDK fires automatically on `CHANGE`. Both call a one-time GET to `markResponseInstanceAsStarted`. Fire-and-forget — errors do not block the UI.
176
+
177
+ ```typescript
178
+ useEffect(() => {
179
+ const onScroll = () => onAction({ type: ACTIONS.SCROLL });
180
+ window.addEventListener('scroll', onScroll, { passive: true, once: true });
181
+ return () => window.removeEventListener('scroll', onScroll);
182
+ }, [onAction]);
183
+ ```
184
+
185
+ ## Question Activity (VIEWED)
186
+
187
+ When a question enters the viewport, dispatch `RECORD_QUESTION_VIEW` with its id(s). The SDK POSTs to `recordQuestionActivityV2` with deduplication (each question recorded once). Requires `id={question.id}` on question wrappers.
188
+
189
+ ```typescript
190
+ useEffect(() => {
191
+ const observer = new IntersectionObserver(entries => {
192
+ for (const entry of entries) {
193
+ if (entry.isIntersecting && entry.target.id) {
194
+ onAction({
195
+ type: ACTIONS.RECORD_QUESTION_VIEW,
196
+ payload: { questionIds: [entry.target.id] },
197
+ });
198
+ }
199
+ }
200
+ }, { threshold: 0.5 });
201
+
202
+ state.currentQuestions.forEach(q => {
203
+ const el = document.getElementById(q.id);
204
+ if (el) observer.observe(el);
205
+ });
206
+
207
+ return () => observer.disconnect();
208
+ }, [state.currentQuestions, onAction]);
209
+ ```
210
+
211
+ ## Partial Save
212
+
213
+ When `savePartialResponse: true` (or `{ debounceMs: 1500 }`) is passed in options, the SDK debounces answer changes and POSTs partial responses with `surveySubmitted=false`. Partial saves begin only after the first user interaction (not MCQ default seeding). Monitor `partialSaveResults.isLoading` / `partialSaveResults.error` if you need UI feedback.
214
+
215
+ ```typescript
216
+ useSurveySDK({
217
+ options: {
218
+ instanceId,
219
+ savePartialResponse: true,
220
+ // or: savePartialResponse: { debounceMs: 2000 },
221
+ },
222
+ });
223
+ ```
94
224
 
95
225
  ## Language & Placeholders
96
226
 
@@ -98,10 +228,8 @@ Triggered by `onAction({ type: ACTIONS.SUBMIT })` or `NEXT` on the last page. Wh
98
228
  - `survey.languages` lists available languages for `LanguageSelector`
99
229
  - Placeholders replace `{{FIRST_NAME}}` tokens in question text at map time
100
230
 
101
- ## CSAT Icons (from package)
231
+ ## CSAT / rating emoji UI (client-implemented)
102
232
 
103
- ```typescript
104
- import { CsatEmojiSet, getEmojiForIndex, CsatStarIcons } from '@explorer02/cfm-survey-sdk';
105
- ```
233
+ CSAT emoji and star icons are **not** exported from the SDK. Implement client-side with `react-icons` (or your icon library). Map column index to sentiment icons proportionally for non-5-point scales.
106
234
 
107
- Use `getEmojiForIndex(index, totalOptions)` for proportional emoji mapping on non-5-point scales. Install `react-icons` in the client project.
235
+ See `03-ui-specs/04-csat.md` and [`question-type-sdk-matrix.md#csat_matrix`](question-type-sdk-matrix.md#csat_matrix).
@@ -3,91 +3,113 @@
3
3
  > **UI spec (authoritative):** `03-ui-specs/12-survey-chrome.md`
4
4
  > **Reference implementation:** `apps/client/src/components/SurveyPage.tsx`
5
5
  > **Target Component**: `SurveyPage`
6
- > **Role**: The main entry point that wires the UI to the `useSurveySDK` hook, manages loading/error states, orchestrates pagination, and controls submission.
6
+ > **Role**: Wires `useSurveySDK` to the UI loading/error states, phase routing, pagination, submission.
7
7
 
8
8
  ## Core Responsibility
9
- This component MUST act as the state machine for the entire survey flow. It should never render questions directly—it delegates that to `Question.tsx`.
10
9
 
11
- ## Setup & Wiring (CRITICAL)
10
+ Act as the **phase state machine** for the survey. Never render questions directly — delegate to `Question.tsx`.
12
11
 
13
- The component MUST invoke `useSurveySDK`, passing the **instanceId** from the client prompt.
12
+ ## Setup & Wiring (CRITICAL)
14
13
 
15
14
  ```tsx
15
+ import type { CustomFieldValueMap } from '@explorer02/cfm-survey-sdk';
16
+
16
17
  const SURVEY_PLACEHOLDERS = { FIRST_NAME: 'Customer' }; // Must be outside component!
18
+ const CUSTOM_FIELD_VALUES: CustomFieldValueMap = {}; // Populate when survey uses custom-field logic
17
19
 
18
20
  export default function SurveyPage() {
19
- const [selectedLanguage, setSelectedLanguage] = useState<string | undefined>("");
21
+ const [selectedLanguage, setSelectedLanguage] = useState<string | undefined>();
20
22
 
21
- const { surveyQueryResults, submitSurveyResults, state, onAction } = useSurveySDK({
22
- options: {
23
- instanceId: 'YOUR_INSTANCE_ID_HERE', // From client prompt
23
+ const surveyOptions = useMemo(
24
+ () => ({
25
+ instanceId: 'YOUR_INSTANCE_ID_HERE',
24
26
  language: selectedLanguage,
25
27
  placeholders: SURVEY_PLACEHOLDERS,
28
+ customFieldValues: CUSTOM_FIELD_VALUES,
26
29
  debug: true,
27
- }
30
+ }),
31
+ [selectedLanguage]
32
+ );
33
+
34
+ const { surveyQueryResults, submitSurveyResults, state, onAction } = useSurveySDK({
35
+ options: surveyOptions,
28
36
  });
29
-
37
+
30
38
  const survey = surveyQueryResults.data;
31
-
32
- // ... state machine rendering
39
+ const allQuestions = useMemo(
40
+ () => survey?.pages.flatMap(page => page.questions) ?? [],
41
+ [survey]
42
+ );
43
+ // ... phase routing below
33
44
  }
34
45
  ```
35
46
 
36
- ## The 5 State Machine Renders (MANDATORY)
47
+ See [`client-integration-guide.md`](../00-integration/client-integration-guide.md) for lifecycle effects (`SCROLL`, `RECORD_QUESTION_VIEW`).
37
48
 
38
- You MUST structure your render logic to strictly handle these 5 states in order to avoid UI crashes when data is not yet available.
49
+ ## Render Order (MANDATORY)
39
50
 
40
- ### 1. Loading State
41
- If `surveyQueryResults.isLoading` is true, render a Skeleton or Spinner.
42
- *Include your CSS-only Header and Footer around the spinner.*
51
+ Handle in this order:
43
52
 
44
- ### 2. Error State
45
- If `surveyQueryResults.error` is true, render a red Error banner containing `surveyQueryResults.error.message`.
53
+ ### 1. Loading / Error / Empty
46
54
 
47
- ### 3. Empty State
48
- If `!survey`, render a generic "Survey not found" message.
55
+ | Condition | UI |
56
+ |-----------|-----|
57
+ | `surveyQueryResults.isLoading` | Spinner + Header/Footer |
58
+ | `surveyQueryResults.error` | Error message |
59
+ | `!survey` | "No survey data" |
49
60
 
50
- ### 4. Submitted State
51
- If `submitSurveyResults.data` exists, the survey is complete.
52
- *Render the Thank You Screen (e.g., "Thank you for your feedback!"). Do not render the question list.*
61
+ ### 2. Special phases (`state.phase`)
53
62
 
54
- ### 5. Active Survey State
55
- If the above 4 conditions are met, render the active survey page:
56
- 1. `Header` (Logo)
57
- 2. `ProgressBar` (Pass `state.progressPercentage`)
58
- 3. `LanguageSelector` (Pass `survey.languages`, `survey.language`, `setSelectedLanguage`)
59
- 4. **The Question Map** (See below)
60
- 5. **Navigation Buttons** (See below)
63
+ | `phase.kind` | Component | Action |
64
+ |--------------|-----------|--------|
65
+ | `intro` | `IntroPage` | `onAction({ type: 'START' })` |
66
+ | `paused` | `PausedPage` | Read-only |
67
+ | `end` | `EndPage` | Thank-you from `state.specialPage` |
68
+ | `submitted` | Terminal message | Already filled |
69
+ | `expired` | Terminal message | Survey expired |
70
+ | `quota_fulfilled` | Terminal message | Quota reached |
71
+
72
+ See `14-intro-page.md`, `15-end-page.md`, `16-paused-page.md`.
73
+
74
+ ### 3. Active content (`phase.kind === 'content'`)
75
+
76
+ 1. `Header`
77
+ 2. `ProgressBar` (`state.progressPercentage`) — see `00-integration/progress.md`
78
+ 3. `LanguageSelector`
79
+ 4. Question map (below)
80
+ 5. Navigation buttons (below)
61
81
  6. `Footer`
62
82
 
63
83
  ## The Question Map
64
84
 
65
- You MUST iterate over `state.currentQuestions` and dispatch to `Question.tsx`.
85
+ Pass **all answers and questions** for placeholder resolution (`{{SURVEY_QUESTION-:-qId}}`):
66
86
 
67
87
  ```tsx
68
- <div className="space-y-12">
69
- {state.currentQuestions.map(question => (
70
- <Question
71
- key={question.id}
72
- question={question}
73
- selectedValue={state.values[question.id]}
74
- validationError={state.validationErrors[question.id]}
75
- onSelect={value => onAction({
76
- type: 'CHANGE',
77
- payload: { questionId: question.id, value }
78
- })}
79
- />
80
- ))}
81
- </div>
88
+ {state.currentQuestions.map(question => (
89
+ <Question
90
+ key={question.id}
91
+ question={question}
92
+ selectedValue={state.answers[question.id]}
93
+ validationError={state.validationErrors[question.id]}
94
+ allAnswers={state.answers}
95
+ allQuestions={allQuestions}
96
+ customFieldValues={CUSTOM_FIELD_VALUES}
97
+ onSelect={answerValue =>
98
+ onAction({ type: 'CHANGE', payload: { questionId: question.id, answerValue } })
99
+ }
100
+ />
101
+ ))}
82
102
  ```
83
103
 
84
- ## Navigation Buttons Logic
104
+ ## Navigation Buttons
85
105
 
86
- You MUST conditionally render "Back" and "Next/Submit" buttons.
87
-
88
- - **Back Button**: Hide if `state.currentPageIndex === 0`.
89
- - `onClick={() => onAction({ type: 'PREVIOUS' })}`
90
- - **Next/Submit Button**:
106
+ - **Back:** Render when `state.canGoBack` `onAction({ type: 'PREVIOUS' })`
107
+ - **Primary:** Label from `state.primaryButtonType` (`'next'` | `'submit'`)
108
+ - Always dispatch `onAction({ type: 'NEXT' })` — SDK maps to submit when `primaryButtonType === 'submit'`
91
109
  - `disabled={submitSurveyResults.isLoading}`
92
- - If `state.currentPageIndex < survey.pages.length - 1`, label is "Next", action is `type: 'NEXT'`.
93
- - If on the last page, label is "Submit", action is `type: 'NEXT'` (the SDK intercepts NEXT on the last page to trigger submission automatically!).
110
+
111
+ Do **not** use `currentPageIndex === 0` alone to hide Back intro page and skip-aware history use `canGoBack`.
112
+
113
+ ## Scroll & question activity
114
+
115
+ Wire `SCROLL` (once) and `RECORD_QUESTION_VIEW` as documented in `useSurveySDK.md`.
@@ -2,9 +2,8 @@
2
2
 
3
3
  > **UI spec (authoritative):** `03-ui-specs/00-question-shell.md`
4
4
  > **Canonical template:** [`templates/Question.tsx`](../templates/Question.tsx) — copy verbatim, then create imported components
5
- > **Monorepo reference:** `apps/client/src/components/Question.tsx` (not available in npm installs)
5
+ > **Monorepo reference:** `apps/client/src/components/Question.tsx`
6
6
  > **Target Component**: `Question`
7
- > **Role**: Wrapper card component that routes to the correct scale component based on `question.type`.
8
7
 
9
8
  ## Start Here (CRITICAL)
10
9
 
@@ -12,71 +11,40 @@
12
11
  2. Create every imported scale component — see [`component-checklist.md`](../00-integration/component-checklist.md).
13
12
  3. Read `03-ui-specs/` for each type in your fetched survey **before** editing the dispatcher.
14
13
 
15
- **Do NOT** write a partial skeleton with `default` fallbacks or `"not yet implemented"` messages.
14
+ **Do NOT** write partial skeletons with `default` fallbacks or `"not yet implemented"` messages.
16
15
 
17
- ## Core Responsibility
18
- Render a visually separated container (card) for each question. Handle rich text parsing, required asterisks, description text, and error validation messages. Most importantly, it MUST act as an exhaustive switch statement dispatching to ALL supported question types.
16
+ ## Dispatcher (11 type literals)
19
17
 
20
- ## Exhaustive Dispatcher Logic (CRITICAL)
18
+ Use `QUESTION_TYPE` from the SDK:
21
19
 
22
- The agent MUST handle EVERY SINGLE ONE of these `question.type` values. Failure to handle them will result in blank UI or broken surveys.
20
+ | `question.type` | Renders |
21
+ |-----------------|---------|
22
+ | `NPS_SCALE` | `RatingScale` (NPS) |
23
+ | `MCQ` | Inline radio/checkbox |
24
+ | `TEXTFIELD` | Inline textarea/input |
25
+ | `CFM_MATRIX` | `LikertMatrixScale` |
26
+ | `CSAT_MATRIX`, `RATING_MATRIX` | `CsatMatrixScale` |
27
+ | `SLIDER_MATRIX` | `SliderMatrixScale` |
28
+ | `FILE_UPLOAD` | `FileUploadScale` |
29
+ | `TEXT_AND_MEDIA` | Media only (no answer) |
30
+ | `HEATMAP` | `HeatmapScale` |
31
+ | `RANK_ORDER` | `RankOrderScale` |
23
32
 
24
- 1. **`rating`**: Renders `RatingScale`. This covers NPS scales.
25
- 2. **`csat`**: Renders `CsatScale`. This covers CSAT specific faces/icons.
26
- 3. **`radio`**: Renders inline MCQ radio buttons.
27
- 4. **`text`**: Renders an inline `<textarea>` or `<input>`.
28
- 5. **`matrix`** — **requires `subType`**:
29
- - If `subType === 'CFM_MATRIX'`, render `LikertMatrixScale`
30
- - If `subType === 'CSAT_MATRIX'` or `'RATING_MATRIX'`, render `CsatMatrixScale`
31
- - Routing on `type === 'matrix'` alone renders **nothing**
32
- 6. **`slider_matrix`**: Renders `SliderMatrixScale`
33
- 7. **`file_upload`**: Renders `FileUploadScale`
34
- 8. **`text_and_media`**: Renders inline `<video>` or `<img>` based on `question.mediaUrl`. No answer input.
35
- 9. **`rating_scale`**: Star/emoji scale — see `03-ui-specs/05-rating-scale.md`
36
- 10. **`slider`**: Standalone slider — see `03-ui-specs/06-slider.md`
37
-
38
- ## Forbidden Anti-Patterns
33
+ **Never** branch on `subType`.
39
34
 
40
35
  ```tsx
41
- // FORBIDDEN never ship this
42
- default:
43
- return <p>Question type "{question.type}" is not yet implemented in this client.</p>;
44
-
45
- // ❌ FORBIDDEN — matrix without subType
46
- {question.type === 'matrix' && <SomeMatrix />}
36
+ import { QUESTION_TYPE } from '@explorer02/cfm-survey-sdk';
47
37
 
48
- // REQUIRED matrix with subType
49
- {question.type === 'matrix' && question.subType === 'CFM_MATRIX' && <LikertMatrixScale ... />}
50
- {question.type === 'matrix' && (question.subType === 'CSAT_MATRIX' || question.subType === 'RATING_MATRIX') && <CsatMatrixScale ... />}
38
+ {question.type === QUESTION_TYPE.CFM_MATRIX && <LikertMatrixScale ... />}
51
39
  ```
52
40
 
53
- ## Container UI Structure
54
-
55
- Every question MUST be wrapped in a standard HTML `<section>` to visually separate it.
56
-
57
- 1. **Wrapper styling**:
58
- - Border: `border border-gray-200`
59
- - Radius: `rounded-xl`
60
- - Background: `bg-white`
61
- - Padding: `p-6 md:p-8`
62
- - Shadow & Hover: `shadow-sm transition-all hover:shadow-md`
63
- - **Crucial**: ID must be set to `question.id` (`id={question.id}`) so scroll-to-error works!
64
-
65
- 2. **Heading (`question.text`)**:
66
- - MUST use `dangerouslySetInnerHTML={{ __html: question.text }}` because text from Sprinklr contains rich HTML (bold, links, italics).
67
- - If `question.required === true`, append a red asterisk `*`.
41
+ ## Placeholder resolution
68
42
 
69
- 3. **Description (`question.description`)**:
70
- - Render conditionally if `question.description` exists.
71
- - MUST use `dangerouslySetInnerHTML`.
72
- - Style with slightly smaller text, greyish color (e.g., `text-sm text-gray-600 mt-2`).
43
+ Pass `allAnswers` and `allQuestions` so question text can reference prior answers:
73
44
 
74
- 4. **Validation Error Banner**:
75
- - If `validationError` prop is passed, render a highly visible banner below the scale.
76
-
77
- ## Hover Tooltips & Micro-interactions
78
- - Focus states on text inputs MUST use the brand primary color (e.g., magenta `#e20074` for Telekom).
79
- - Radio button wrappers should have a subtle background hover effect (e.g., `hover:bg-pink-50`).
45
+ ```tsx
46
+ resolveSurveyQuestionPlaceholders(question.questionText, allAnswers, questionsById)
47
+ ```
80
48
 
81
49
  ## Props Contract
82
50
 
@@ -85,8 +53,43 @@ type QuestionProps = {
85
53
  question: SurveyQuestion;
86
54
  selectedValue?: AnswerValue;
87
55
  validationError?: string;
88
- onSelect: (value: AnswerValue) => void;
56
+ allAnswers: SurveyAnswers;
57
+ allQuestions: SurveyQuestion[];
58
+ onSelect: (answerValue: AnswerValue) => void;
89
59
  };
90
60
  ```
91
61
 
92
- Wire `onSelect` → `onAction({ type: 'CHANGE', payload: { questionId, value } })`.
62
+ Wire `onSelect` → `onAction({ type: 'CHANGE', payload: { questionId, answerValue } })`.
63
+
64
+ ## Container UI Structure
65
+
66
+ - Wrapper: `<section id={question.id} ...>` — **required** for scroll-to-error
67
+ - `questionText` / `questionDescription`: `dangerouslySetInnerHTML`
68
+ - Required asterisk when `question.required`
69
+ - Validation error banner when `validationError` is set
70
+
71
+ ## Matrix value storage
72
+
73
+ Store matrix cell values as column **`id`** via `matrixColumnStoredValue(col)` — see [`value-derivation.md`](../02-reference/value-derivation.md).
74
+
75
+ ## SDK Integration Checklist — inline types (agent)
76
+
77
+ ### MCQ (`QUESTION_TYPE.MCQ`)
78
+
79
+ - [ ] Filter options with `getVisibleMcqOptionsForAnswers` before render
80
+ - [ ] Store `option.id`; support `isMultiSelect` array shape when configured
81
+ - [ ] onAction CHANGE: `{ questionId, answerValue: optionId | optionId[] }`
82
+
83
+ ### TEXTFIELD (`QUESTION_TYPE.TEXTFIELD`)
84
+
85
+ - [ ] Render `inputVariant` (`short` single-line vs `long` textarea)
86
+ - [ ] onAction CHANGE: `{ questionId, answerValue: string }`
87
+ - [ ] Debounce long text if partial save enabled
88
+
89
+ ### TEXT_AND_MEDIA (`QUESTION_TYPE.TEXT_AND_MEDIA`)
90
+
91
+ - [ ] No answer input — no CHANGE action
92
+ - [ ] Render media block when `question.media?.url` present
93
+ - [ ] User advances with NEXT
94
+
95
+ See [`question-type-sdk-matrix.md`](../00-integration/question-type-sdk-matrix.md) for all 11 types.
@@ -3,7 +3,7 @@
3
3
  > **UI spec (authoritative):** `03-ui-specs/01-rating.md` (NPS) · `03-ui-specs/05-rating-scale.md` (star/emoji gap)
4
4
  > **Reference implementation:** `apps/client/src/components/RatingScale.tsx`
5
5
  > **Target Component**: `RatingScale`
6
- > **Handles**: `type: 'rating'`, `type: 'rating_scale'`
6
+ > **Handles**: `type: 'NPS_SCALE'` (via `QUESTION_TYPE.NPS_SCALE`)
7
7
 
8
8
  ## Core Responsibility
9
9
  Render a horizontal sequence of numbered badges.
@@ -28,3 +28,13 @@ The UI must exactly match enterprise standards:
28
28
  3. **Selected State**: The selected badge MUST pop out prominently: `border-2 border-[#e20074] shadow-md scale-105 opacity-100 font-bold`.
29
29
  4. **Hover State**: `hover:scale-105 hover:border-[#e20074] transition-all cursor-pointer`.
30
30
  5. **Tooltips**: Every badge MUST have `title={option.label}` applied for accessibility and semantic context (e.g. hovering '10' shows 'Extremely Likely').
31
+
32
+ ## SDK Integration Checklist (agent)
33
+
34
+ - [ ] Type literal: `QUESTION_TYPE.NPS_SCALE`
35
+ - [ ] Answer stored as: `number`
36
+ - [ ] onAction CHANGE: `{ questionId, answerValue: number }`
37
+ - [ ] Visibility helper: `getVisibleScaleColumnsForAnswers` when scale points have answer logic
38
+ - [ ] Custom fields: 4th arg on scale visibility helper when needed
39
+ - [ ] Logic features: skip ✓ display ✓ answer ✓ variants ✓ numbering ✓
40
+ - [ ] Matrix row: [`question-type-sdk-matrix.md`](../00-integration/question-type-sdk-matrix.md#nps_scale)
@@ -0,0 +1,45 @@
1
+ # Star Rating Scale Architectural Blueprint
2
+
3
+ > **DEPRECATED:** API RATING pages map to `NPS_SCALE` or matrix display styles — not a separate `question.type`. Keep for historical reference only.
4
+ >
5
+ > **UI spec (authoritative):** `03-ui-specs/05-rating-scale.md`
6
+ > **SDK type:** `StarRatingQuestion` — `type: 'star_rating'`
7
+ > **Target Component**: `RatingScale` (star/emoji variant)
8
+ > **Handles**: `type: 'star_rating'` only — not `CSAT_MATRIX` (that is a matrix grid)
9
+
10
+ ## Core Responsibility
11
+
12
+ Render star or emoji rating scales ensuring that active selections are unmistakably highlighted using bounding boxes or strong border accents, NEVER just by changing the SVG fill color.
13
+
14
+ ## State Management & Mapping
15
+
16
+ - **Emojis**: `scaleStyle === 'emoji'`
17
+ - Map values using client-side emoji icons (react-icons) proportional to scale length.
18
+ - **Stars**: `scaleStyle === 'star'`
19
+ - Active stars (index <= selectedValue) use filled star icons from react-icons.
20
+ - **Numbers**: Derive display/submit value from `optionLabel` (see `02-reference/value-derivation.md`).
21
+
22
+ ## UX Bounding Box Rules (CRITICAL)
23
+
24
+ ### For Emojis & Stars:
25
+
26
+ - **Unselected**: `inline-flex p-2 border-2 border-transparent opacity-40 hover:opacity-100 hover:scale-110 transition-all cursor-pointer`.
27
+ - **Selected**: The specific selected icon MUST be wrapped in a prominent square box with rounded corners.
28
+ - **CSS**: `border-2 border-[#e20074] rounded-lg p-2 bg-pink-50 shadow-sm scale-110 opacity-100`.
29
+
30
+ ### For Numbered Badges:
31
+
32
+ - **Shape**: MUST be square with rounded corners (`rounded-lg w-12 h-12`). Do not use full circles.
33
+ - **Selected**: `border-2 border-[#e20074] opacity-100 font-bold scale-105 shadow-md`.
34
+ - **Unselected**: `opacity-30 border-transparent`.
35
+
36
+ ## Bounding Boxes inside Matrices
37
+
38
+ When emojis or stars are rendered inside `CsatMatrixScale`, the exact same bounding box logic applies. See `05-csat-matrix-scale.md`.
39
+
40
+ ## Tooltips & Label Positions
41
+
42
+ - **Tooltips**: Every emoji, star, and badge MUST have `title={optionLabel}` on hover.
43
+ - **Labels (`minLabel`, `midLabel`, `maxLabel`)**:
44
+ - Render in `Question.tsx` above the scale (same pattern as `nps`).
45
+ - `midLabel` position: `style={{ left: \`\${(midLabelIndex / (options.length - 1)) * 100}%\` }}`.