@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
@@ -4,23 +4,36 @@
4
4
 
5
5
  Master guide for AI coding agents. Machine-readable index: `MANIFEST.json`.
6
6
 
7
+ **Installed package path:** `node_modules/@explorer02/cfm-survey-sdk/templates/docs/` (docs ship inside `templates/` — no postinstall copy needed).
8
+
7
9
  ## Documentation Layout
8
10
 
9
11
  ```
10
- docs/
12
+ templates/docs/ ← installed npm package layout
13
+ packages/sdk/templates/docs/ ← monorepo layout
11
14
  ├── index.md ← YOU ARE HERE
12
15
  ├── MANIFEST.json ← Phase + question-type routing (for tooling)
13
16
  ├── 00-integration/ ← Setup, hook contract, constraints
14
- ├── 01-components/ ← Wiring blueprints (read ALL before coding)
15
- ├── 02-reference/ ← Routing table + config-field-index (on demand)
16
- ├── 03-ui-specs/ ← Authoritative UI specs (reference-client driven)
17
+ ├── 01-components/ ← Wiring blueprints (read per type from MANIFEST)
18
+ ├── 02-reference/ ← Routing table, exports, config-field-index
19
+ ├── 03-ui-specs/ ← Authoritative UI specs
17
20
  └── templates/
18
21
  ├── implementation_plan.md
22
+ ├── survey-inventory.schema.json
23
+ ├── verify-agent-build.sh
24
+ ├── deploy-to-aws.sh
25
+ ├── heatmapCoords.ts
19
26
  └── Question.tsx ← Canonical dispatcher (copy before building)
20
27
  ```
21
28
 
22
29
  **Import rule:** Use whatever package name appears in the project's `package.json` (`@explorer02/cfm-survey-sdk` or `@repo/sdk`).
23
30
 
31
+ ## Do Not Read
32
+
33
+ - Internal SDK mapper/source paths (`packages/sdk/src/**` or `node_modules/.../src/**`)
34
+ - `cfm-sdk init`, `survey-config.json`, or any setup wizard
35
+ - Stale `apps/client` code without verifying type literals against `MANIFEST.json` `typeLiterals`
36
+
24
37
  ---
25
38
 
26
39
  ## Client Inputs (3 only)
@@ -37,7 +50,9 @@ docs/
37
50
 
38
51
  ### PHASE 0: ORIENT
39
52
 
40
- - Read this file and `AGENT.md`
53
+ - Read this file and `AGENT.md`:
54
+ - **Monorepo:** `packages/sdk/templates/AGENT.md`
55
+ - **Installed npm:** `node_modules/@explorer02/cfm-survey-sdk/templates/AGENT.md`
41
56
  - Detect framework: read `package.json` — Next.js / Vite / CRA (see `00-integration/setup.md`)
42
57
  - Extract branding and layout from client prompt
43
58
  - For existing apps: read project structure; do not blindly scaffold
@@ -56,60 +71,93 @@ Ask: *"Please provide your survey instance ID (JWT token)."*
56
71
 
57
72
  ### PHASE 3: SDK CONTRACT
58
73
 
59
- Read: `00-integration/useSurveySDK.md`
74
+ Read: `00-integration/client-integration-guide.md` (start here), `00-integration/question-type-sdk-matrix.md`, `00-integration/useSurveySDK.md`, `02-reference/exports.md`, `00-integration/skip-logic-and-navigation.md`, `00-integration/display-logic-and-navigation.md`, `00-integration/question-display-variants.md`, `00-integration/question-numbering.md`, `00-integration/placeholders-and-tokens.md`, `00-integration/partial-save-and-recovery.md`, `00-integration/survey-lifecycle-analytics.md`, `00-integration/end-page-logic.md`, `00-integration/answer-logic-and-navigation.md`, `00-integration/custom-field-logic-and-navigation.md`, `00-integration/progress.md`, `02-reference/value-derivation.md`
60
75
 
61
- Understand `useSurveySDK`, `onAction`, states. Do **not** read internal mapper/source paths.
76
+ Understand `useSurveySDK`, `onAction`, `state.answers`. Do **not** read internal mapper/source paths.
62
77
 
63
78
  Optional: read `02-reference/routing-table.md` for type overview.
64
79
 
65
80
  ### PHASE 4: PLAN + INGEST (CRITICAL)
66
81
 
67
- 1. Read **every** file in `01-components/` (all 14 blueprints)
68
- 2. Read `00-integration/component-checklist.md` and `03-ui-specs/README.md`
69
- 3. **Inventory** every `(type, subType)` in `survey.pages` after instanceId fetch
70
- 4. For **each type in the survey**, read matching `03-ui-specs/` (use `MANIFEST.json` `uiSpecs`) and `02-reference/question-types/` **blocking** for `matrix`, `slider_matrix`, `file_upload`
71
- 5. Copy `docs/templates/implementation_plan.md` → `./implementation_plan.md` and fill every section
72
- 6. Copy `docs/templates/Question.tsx` as the dispatcher starting point
73
- 7. **Auto-proceed to Phase 5** do not wait for approval
82
+ 1. Fetch survey with `instanceId`; extract unique `question.type` values from `survey.pages`
83
+ 2. Write inventory to `implementation_plan.md` §3b (validate against `templates/survey-inventory.schema.json`)
84
+ 3. Read **always**: `component-checklist.md`, `00-integration/question-type-sdk-matrix.md`, `03-ui-specs/00-question-shell.md`, `03-ui-specs/12-survey-chrome.md`, `01-components/01-survey-page.md`, `01-components/02-question.md`, `01-components/14-intro-page.md`, `01-components/15-end-page.md`, `01-components/16-paused-page.md`
85
+ 4. For **each type in the survey**, read MANIFEST `questionTypes.{type}` blueprint + reference + uiSpec; cross-check the matching row in `question-type-sdk-matrix.md`
86
+ 5. When config flags match MANIFEST `sharedUiSpecTriggers` (e.g. `gridLayout:dropdown`, `displayStyle:graphics`) read matching `03-ui-specs/shared/` specs
87
+ 6. Copy `docs/templates/implementation_plan.md` `./implementation_plan.md` and fill every section
88
+ 7. Copy `docs/templates/Question.tsx` as the dispatcher starting point
89
+ 8. **Auto-proceed to Phase 5** — do not wait for approval
90
+
91
+ **Fallback:** If survey fetch fails, read all `01-components/*.md` blueprints before coding.
74
92
 
75
93
  ### PHASE 5: BUILD
76
94
 
77
- Read: `00-integration/constraints.md`, `00-integration/component-checklist.md`
95
+ Read: `00-integration/constraints.md`, `00-integration/component-checklist.md`, `00-integration/question-type-sdk-matrix.md`
78
96
 
79
97
  Create all required scale component files **before** wiring `Question.tsx`. For each question in the survey:
80
98
 
81
- 1. Read `02-reference/question-types/{type}.md` for data shape
82
- 2. Read `03-ui-specs/{NN}-{type}.md` (use `MANIFEST.json` `uiSpecs`; split matrix by `subType`)
83
- 3. If `matrixFormat`, `buttonType`, or `sliderType` is present → read `03-ui-specs/shared/` specs
99
+ 1. Read the matching row in `question-type-sdk-matrix.md` (answer shape, visibility helper, performance)
100
+ 2. Read `02-reference/question-types/` doc for data shape (MANIFEST `reference`)
101
+ 2. Read matching `03-ui-specs/` (MANIFEST `uiSpec`)
102
+ 3. If `gridLayout`, `displayStyle`, or `statementLayout` triggers shared specs → read `03-ui-specs/shared/`
84
103
  4. Cross-check `02-reference/config-field-index.md` for every non-default field
85
- 5. Implement per ui-spec — monorepo devs may consult `apps/client/src/components/`; npm installs use ui-specs only
104
+ 5. Implement per ui-spec — trust SDK Integration Checklist in blueprint/reference + `templates/Question.tsx` + exported SDK types + `02-reference/value-derivation.md`
86
105
 
87
106
  Implement components per blueprints + UI specs. Use exported types:
88
107
 
89
108
  ```typescript
90
- import { useSurveySDK, ACTIONS, type SurveyAction } from '@explorer02/cfm-survey-sdk';
109
+ import { useSurveySDK, ACTIONS, QUESTION_TYPE, type SurveyAction } from '@explorer02/cfm-survey-sdk';
91
110
  ```
92
111
 
112
+ **Dispatch on flat `question.type` only** — **11** `QUESTION_TYPE` literals, no `subType`. See `MANIFEST.json` `typeLiterals`.
113
+
114
+ **AWS deploy prep (when client prompt mentions AWS / CloudFront / S3 / EC2 / Sprinklr):** apply Next.js static export config **during this phase**, not at deploy time. Read `00-integration/setup.md#nextjs-aws-static-export` and `00-integration/aws-deploy.md` § Build Config. Fill `implementation_plan.md` §8.
115
+
93
116
  ### PHASE 6: VERIFY
94
117
 
95
118
  Read: `00-integration/constraints.md` (troubleshooting)
96
119
 
120
+ Run: `bash templates/verify-agent-build.sh` (from project root, after copying script if needed)
121
+
97
122
  Checklist:
98
123
 
99
- - [ ] `Question.tsx` copied from `docs/templates/Question.tsx` — covers all 12 `question.type` values
100
- - [ ] Matrix routes by `subType` (`CFM_MATRIX` vs `CSAT_MATRIX` / `RATING_MATRIX`)
124
+ - [ ] `Question.tsx` copied from `docs/templates/Question.tsx` — covers all **11** `question.type` values
125
+ - [ ] Each matrix variant routes by its own type literal (`CFM_MATRIX`, `CSAT_MATRIX`, `RATING_MATRIX`) — **no `subType`**
101
126
  - [ ] All scale components from `component-checklist.md` exist for types in survey
102
127
  - [ ] **Zero stub text:** `grep -r "not yet implemented" src/ components/ app/` returns no matches
103
128
  - [ ] Every question wrapper has `id={question.id}`
104
- - [ ] No imports from `@explorer02/cfm-survey-sdk/src/...`
105
- - [ ] `SurveyPage` handles loading / error / empty / submitted / active
129
+ - [ ] No imports from `@explorer02/cfm-survey-sdk/src/...` or `@repo/sdk/src/...`
130
+ - [ ] `SurveyPage` handles loading / error / empty / all `state.phase` values / active content
131
+ - [ ] Hook uses `state.answers`, `state.canGoBack`, `state.primaryButtonType`, and `payload.answerValue`
132
+ - [ ] Stable `customFieldValues` ref when survey uses custom-field logic; passed to hook and `Question` visibility helpers
133
+ - [ ] `SURVEY_PLACEHOLDERS` stable ref; `surveyOptions` in `useMemo` (see `placeholders-and-tokens.md`)
134
+ - [ ] `SCROLL` + `RECORD_QUESTION_VIEW` lifecycle effects wired (see `survey-lifecycle-analytics.md`)
135
+ - [ ] Monorepo: `packages/sdk` built before client dev (`npm run build`)
106
136
  - [ ] `npm run build` exits 0
137
+ - [ ] **AWS intent:** `out/index.html` exists; file count ≤ 500; Next.js static export configured (`output: export`, `assetPrefix: "./"`)
107
138
 
108
139
  ### PHASE 7: DEPLOY (INPUT 3)
109
140
 
110
141
  Ask: *"Your survey is built and verified. Ready to deploy?"*
111
142
 
112
- If yes: `npx cfm-sdk deploy` (preferred) or `npx vercel --prod --yes`
143
+ **Route by client prompt** (see MANIFEST `deployTargets` and `phases[7].selectTargetFromPrompt`):
144
+
145
+ | Prompt mentions | Target | Action |
146
+ |-----------------|--------|--------|
147
+ | AWS, CloudFront, S3, EC2, Sprinklr | **AWS** | Read `00-integration/aws-deploy.md`; set env vars; run 3-channel deploy or `bash templates/deploy-to-aws.sh` |
148
+ | Vercel or unspecified (default) | **Vercel** | `npx cfm-sdk deploy` (requires `VERCEL_TOKEN` or Vercel CLI) |
149
+
150
+ **AWS env vars (dev machine):**
151
+
152
+ ```bash
153
+ export CFM_INSTANCE_ID="<same JWT as SurveyPage>"
154
+ export CFM_DEPLOY_API_BASE="http://43.204.26.213:3000"
155
+ export OUT_DIR="<absolute path to out/ or dist/>"
156
+ ```
157
+
158
+ **AWS success:** print live CloudFront HTTPS URL from `/deploy/complete`; ask client to open in browser.
159
+
160
+ **Vercel success:** CLI prints production URL.
113
161
 
114
162
  ---
115
163
 
@@ -117,8 +165,11 @@ If yes: `npx cfm-sdk deploy` (preferred) or `npx vercel --prod --yes`
117
165
 
118
166
  | When | Read |
119
167
  |------|------|
120
- | Always first | `index.md`, `00-integration/setup.md`, `00-integration/useSurveySDK.md`, `00-integration/constraints.md` |
121
- | Before any React code | All `01-components/*.md`, `00-integration/component-checklist.md`, `templates/Question.tsx` |
122
- | After instanceId fetch | `03-ui-specs/` per type in survey (blocking for matrix / slider_matrix / file_upload) |
168
+ | Always first | `index.md`, `00-integration/client-integration-guide.md`, `00-integration/question-type-sdk-matrix.md`, `00-integration/setup.md`, `00-integration/useSurveySDK.md`, `00-integration/constraints.md` |
169
+ | Lifecycle & placeholders | `00-integration/survey-lifecycle-analytics.md`, `00-integration/placeholders-and-tokens.md`, `00-integration/partial-save-and-recovery.md` |
170
+ | Skip logic & progress | `00-integration/skip-logic-and-navigation.md`, `00-integration/display-logic-and-navigation.md`, `00-integration/question-display-variants.md`, `00-integration/question-numbering.md`, `00-integration/end-page-logic.md`, `00-integration/answer-logic-and-navigation.md`, `00-integration/custom-field-logic-and-navigation.md`, `00-integration/progress.md` |
171
+ | Before any React code | `component-checklist.md`, `templates/Question.tsx`, MANIFEST `readAlways` docs |
172
+ | After instanceId fetch | Per-type docs from MANIFEST `questionTypes` (blocking: `CFM_MATRIX`, `CSAT_MATRIX`, `RATING_MATRIX`, `SLIDER_MATRIX`, `FILE_UPLOAD`) |
123
173
  | Config audit | `02-reference/config-field-index.md` |
124
- | Tooling | `MANIFEST.json`, `templates/implementation_plan.md` |
174
+ | AWS deploy | `00-integration/aws-deploy.md`, `setup.md#nextjs-aws-static-export`, `templates/deploy-to-aws.sh` |
175
+ | Tooling | `MANIFEST.json`, `templates/implementation_plan.md`, `templates/survey-inventory.schema.json` |
@@ -2,52 +2,118 @@
2
2
  * CANONICAL QUESTION DISPATCHER TEMPLATE
3
3
  *
4
4
  * Agents: copy this file to your project's components folder (e.g. src/components/Question.tsx).
5
- * Then create EVERY imported scale component listed below see:
6
- * docs/00-integration/component-checklist.md
7
- * docs/03-ui-specs/00-question-shell.md
8
- *
9
- * RULES:
10
- * - Do NOT add a default/fallback branch that renders "not yet implemented" or similar stub text.
11
- * - matrix MUST route by question.subType — type alone is NOT enough.
12
- * - If a question type appears in the fetched survey, its branch MUST render a real interactive component.
5
+ * Covers all 11 QUESTION_TYPE literals see question-type-sdk-matrix.md for per-type wiring.
6
+ * Route on `question.type` / `QUESTION_TYPE` only — no subType.
13
7
  *
14
8
  * Replace @explorer02/cfm-survey-sdk with your package.json import name if different.
15
9
  */
16
10
  'use client';
17
11
 
18
- import React from 'react';
19
- import type { SurveyQuestion, AnswerValue } from '@explorer02/cfm-survey-sdk';
20
- import { CsatScale } from './CsatScale';
12
+ import React, { useMemo } from 'react';
13
+ import type { SurveyQuestion, AnswerValue, SurveyAnswers, CustomFieldValueMap, HeatmapAnswer, HeatmapQuestion } from '@explorer02/cfm-survey-sdk';
14
+ import { QUESTION_TYPE, resolveSurveyQuestionPlaceholders, getVisibleMatrixItemsForAnswers, getVisibleMcqOptionsForAnswers, getVisibleRankOrderOptionsForAnswers } from '@explorer02/cfm-survey-sdk';
21
15
  import { CsatMatrixScale } from './CsatMatrixScale';
22
16
  import { SliderMatrixScale } from './SliderMatrixScale';
23
17
  import { LikertMatrixScale } from './LikertMatrixScale';
24
18
  import { FileUploadScale } from './FileUploadScale';
19
+ import { HeatmapScale } from './HeatmapScale';
20
+ import { RankOrderScale } from './RankOrderScale';
25
21
  import RatingScale from './RatingScale';
26
- // rating_scale / slider gaps — create these if survey contains those types:
27
- // import { CustomSliderTrack } from './CustomSliderTrack';
28
22
 
29
23
  type QuestionProps = {
30
24
  question: SurveyQuestion;
31
25
  selectedValue?: AnswerValue;
32
26
  validationError?: string;
33
- onSelect: (value: AnswerValue) => void;
27
+ allAnswers: SurveyAnswers;
28
+ allQuestions: SurveyQuestion[];
29
+ customFieldValues?: CustomFieldValueMap;
30
+ onSelect: (answerValue: AnswerValue) => void;
34
31
  };
35
32
 
36
- export default function Question({ question, selectedValue, validationError, onSelect }: QuestionProps) {
33
+ export default function Question({
34
+ question,
35
+ selectedValue,
36
+ validationError,
37
+ allAnswers,
38
+ allQuestions,
39
+ customFieldValues = {},
40
+ onSelect,
41
+ }: QuestionProps) {
42
+ const questionsById = useMemo(
43
+ () => new Map(allQuestions.map(q => [q.id, q])),
44
+ [allQuestions]
45
+ );
46
+
47
+ const displayQuestionText = useMemo(
48
+ () => resolveSurveyQuestionPlaceholders(question.questionText, allAnswers, questionsById),
49
+ [question.questionText, allAnswers, questionsById]
50
+ );
51
+
52
+ const displayQuestionDescription = useMemo(
53
+ () =>
54
+ question.questionDescription
55
+ ? resolveSurveyQuestionPlaceholders(question.questionDescription, allAnswers, questionsById)
56
+ : undefined,
57
+ [question.questionDescription, allAnswers, questionsById]
58
+ );
59
+
60
+ const matrixQuestion = useMemo(() => {
61
+ const isMatrix =
62
+ question.type === QUESTION_TYPE.CFM_MATRIX ||
63
+ question.type === QUESTION_TYPE.CSAT_MATRIX ||
64
+ question.type === QUESTION_TYPE.RATING_MATRIX ||
65
+ question.type === QUESTION_TYPE.SLIDER_MATRIX;
66
+
67
+ if (!isMatrix) return question;
68
+
69
+ const { statementRows, scaleColumns } = getVisibleMatrixItemsForAnswers(
70
+ question,
71
+ allAnswers,
72
+ allQuestions,
73
+ customFieldValues
74
+ );
75
+
76
+ return {
77
+ ...question,
78
+ statementRows,
79
+ ...(question.type === QUESTION_TYPE.CSAT_MATRIX || question.type === QUESTION_TYPE.CFM_MATRIX
80
+ ? { scaleColumns }
81
+ : {}),
82
+ };
83
+ }, [question, allAnswers, allQuestions, customFieldValues]);
84
+
85
+ const visibleMcqOptions = useMemo(() => {
86
+ if (question.type !== QUESTION_TYPE.MCQ) return [];
87
+ return getVisibleMcqOptionsForAnswers(question, allAnswers, allQuestions, customFieldValues);
88
+ }, [question, allAnswers, allQuestions, customFieldValues]);
89
+
90
+ const rankOrderQuestion = useMemo(() => {
91
+ if (question.type !== QUESTION_TYPE.RANK_ORDER) return null;
92
+ return {
93
+ ...question,
94
+ options: getVisibleRankOrderOptionsForAnswers(
95
+ question,
96
+ allAnswers,
97
+ allQuestions,
98
+ customFieldValues
99
+ ),
100
+ };
101
+ }, [question, allAnswers, allQuestions, customFieldValues]);
102
+
37
103
  return (
38
104
  <section id={question.id} className="space-y-4 rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
39
105
  <h2 className="text-lg font-medium leading-relaxed text-gray-900">
40
- <span dangerouslySetInnerHTML={{ __html: question.text }} />
106
+ <span dangerouslySetInnerHTML={{ __html: displayQuestionText }} />
41
107
  {question.required && <span className="text-red-500 ml-1">*</span>}
42
108
  </h2>
43
- {question.description && (
109
+ {displayQuestionDescription && (
44
110
  <div
45
111
  className="text-sm text-gray-600 mt-2 leading-relaxed"
46
- dangerouslySetInnerHTML={{ __html: question.description }}
112
+ dangerouslySetInnerHTML={{ __html: displayQuestionDescription }}
47
113
  />
48
114
  )}
49
115
 
50
- {question.type === 'rating' && (
116
+ {question.type === QUESTION_TYPE.NPS_SCALE && (
51
117
  <div className="space-y-3">
52
118
  {(question.minLabel || question.midLabel || question.maxLabel) && (
53
119
  <div className="relative w-full h-6 text-xs font-semibold text-gray-500">
@@ -60,9 +126,8 @@ export default function Question({ question, selectedValue, validationError, onS
60
126
  className="absolute top-0 -translate-x-1/2 text-center max-w-[40%] leading-tight"
61
127
  style={{
62
128
  left: `${question.midLabelIndex !== undefined && question.options.length > 1
63
- ? (question.midLabelIndex / (question.options.length - 1)) * 100
64
- : 50
65
- }%`,
129
+ ? (question.midLabelIndex / (question.options.length - 1)) * 100
130
+ : 50}%`,
66
131
  }}
67
132
  dangerouslySetInnerHTML={{ __html: question.midLabel }}
68
133
  />
@@ -78,164 +143,166 @@ export default function Question({ question, selectedValue, validationError, onS
78
143
  options={question.options}
79
144
  selectedValue={selectedValue}
80
145
  onSelect={onSelect}
146
+ variant="nps"
147
+ buttonStyle={question.buttonStyle}
81
148
  />
82
149
  </div>
83
150
  )}
84
151
 
85
- {question.type === 'radio' && (
86
- <div className="space-y-3">
87
- {question.options.map(option => {
88
- const isSelected = selectedValue === option.value;
89
- return (
90
- <label
91
- key={String(option.value)}
92
- className={`flex items-center gap-4 rounded-lg border py-4 px-5 cursor-pointer transition-all ${isSelected ? 'border-[#e20074] bg-[#fdf2f8]' : 'border-[#e5e5e5] bg-white hover:bg-gray-50/50'
93
- }`}
94
- >
95
- <input
96
- type="radio"
97
- name={question.id}
98
- value={option.value === null ? '' : option.value}
99
- checked={isSelected}
100
- onChange={() => onSelect(option.value)}
101
- className="sr-only"
102
- />
103
- <div
104
- className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full border transition-all ${isSelected ? 'border-2 border-[#e20074] bg-white' : 'border-gray-400 bg-white'
105
- }`}
152
+ {question.type === QUESTION_TYPE.MCQ && (() => {
153
+ const effectiveValue =
154
+ selectedValue ??
155
+ (question.selectionMode === 'single'
156
+ ? question.defaultOptionIds?.[0]
157
+ : question.defaultOptionIds);
158
+
159
+ if (question.selectionMode === 'multiple') {
160
+ const selectedIds: string[] = Array.isArray(effectiveValue)
161
+ ? effectiveValue.filter((id): id is string => typeof id === 'string')
162
+ : typeof effectiveValue === 'string'
163
+ ? [effectiveValue]
164
+ : [];
165
+
166
+ const handleToggle = (optionId: string) => {
167
+ const next = selectedIds.includes(optionId)
168
+ ? selectedIds.filter(id => id !== optionId)
169
+ : question.maxSelections && selectedIds.length >= question.maxSelections
170
+ ? selectedIds
171
+ : [...selectedIds, optionId];
172
+ onSelect(next);
173
+ };
174
+
175
+ return (
176
+ <div className="space-y-3">
177
+ {visibleMcqOptions.map(option => {
178
+ const isSelected = selectedIds.includes(option.id);
179
+ return (
180
+ <label
181
+ key={option.id}
182
+ className={`flex items-center gap-4 rounded-lg border py-4 px-5 cursor-pointer transition-all ${isSelected ? 'border-[#e20074] bg-[#fdf2f8]' : 'border-[#e5e5e5] bg-white hover:bg-gray-50/50'}`}
183
+ >
184
+ <input
185
+ type="checkbox"
186
+ name={question.id}
187
+ checked={isSelected}
188
+ onChange={() => handleToggle(option.id)}
189
+ className="sr-only"
190
+ />
191
+ <span
192
+ className="text-[15px] font-medium text-gray-900 leading-tight"
193
+ dangerouslySetInnerHTML={{ __html: option.optionLabel }}
194
+ />
195
+ </label>
196
+ );
197
+ })}
198
+ </div>
199
+ );
200
+ }
201
+
202
+ const singleSelected = typeof effectiveValue === 'string' ? effectiveValue : undefined;
203
+
204
+ return (
205
+ <div className="space-y-3">
206
+ {visibleMcqOptions.map(option => {
207
+ const isSelected = singleSelected === option.id;
208
+ return (
209
+ <label
210
+ key={option.id}
211
+ className={`flex items-center gap-4 rounded-lg border py-4 px-5 cursor-pointer transition-all ${isSelected ? 'border-[#e20074] bg-[#fdf2f8]' : 'border-[#e5e5e5] bg-white hover:bg-gray-50/50'}`}
106
212
  >
107
- {isSelected && <div className="h-2.5 w-2.5 rounded-full bg-[#e20074]" />}
108
- </div>
109
- <span
110
- className="text-[15px] font-medium text-gray-900 leading-tight"
111
- dangerouslySetInnerHTML={{ __html: option.label }}
112
- />
113
- </label>
114
- );
115
- })}
116
- </div>
213
+ <input
214
+ type="radio"
215
+ name={question.id}
216
+ checked={isSelected}
217
+ onChange={() => onSelect(option.id)}
218
+ className="sr-only"
219
+ />
220
+ <span
221
+ className="text-[15px] font-medium text-gray-900 leading-tight"
222
+ dangerouslySetInnerHTML={{ __html: option.optionLabel }}
223
+ />
224
+ </label>
225
+ );
226
+ })}
227
+ </div>
228
+ );
229
+ })()}
230
+
231
+ {question.type === QUESTION_TYPE.RANK_ORDER && rankOrderQuestion && (
232
+ <RankOrderScale
233
+ question={rankOrderQuestion}
234
+ selectedValue={
235
+ typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue)
236
+ ? selectedValue
237
+ : undefined
238
+ }
239
+ onSelect={onSelect}
240
+ />
117
241
  )}
118
242
 
119
- {question.type === 'text' && (
120
- <div className="space-y-1.5">
121
- <textarea
122
- rows={4}
123
- value={(selectedValue as string | number | null) ?? ''}
124
- onChange={e => onSelect(e.target.value)}
125
- placeholder={question.placeholder || 'Type your response here...'}
126
- className="w-full rounded-[4px] border border-gray-300 p-4 text-sm outline-none transition-colors focus:border-[#e20074] focus:ring-1 focus:ring-[#e20074]"
127
- />
128
- <div className="text-right text-xs text-gray-500 pr-1">
129
- {Math.max(
130
- 0,
131
- (question.maxCharacterCount ?? 1000) - (typeof selectedValue === 'string' ? selectedValue.length : 0)
132
- )}{' '}
133
- Zeichen verbleiben.
134
- </div>
135
- </div>
243
+ {question.type === QUESTION_TYPE.TEXTFIELD && (
244
+ <textarea
245
+ rows={question.inputVariant === 'long' ? 4 : 1}
246
+ value={(selectedValue as string) ?? ''}
247
+ onChange={e => onSelect(e.target.value)}
248
+ placeholder={question.placeholder || 'Type your response here...'}
249
+ className="w-full rounded border border-gray-300 p-4 text-sm outline-none focus:border-[#e20074]"
250
+ />
136
251
  )}
137
252
 
138
- {question.type === 'csat' && (
139
- <div className="space-y-3">
140
- {(question.minLabel || question.maxLabel) && (
141
- <div className="flex w-full justify-between text-xs font-semibold text-gray-500">
142
- {question.minLabel && <span dangerouslySetInnerHTML={{ __html: question.minLabel }} />}
143
- {question.maxLabel && <span dangerouslySetInnerHTML={{ __html: question.maxLabel }} />}
144
- </div>
145
- )}
146
- <CsatScale
147
- question={question}
148
- selectedValue={selectedValue as string | number | null}
149
- onSelect={onSelect}
150
- />
151
- </div>
253
+ {question.type === QUESTION_TYPE.CFM_MATRIX && (
254
+ <LikertMatrixScale
255
+ question={matrixQuestion as typeof question & { type: typeof QUESTION_TYPE.CFM_MATRIX }}
256
+ selectedValue={typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue) ? selectedValue : {}}
257
+ onSelect={onSelect}
258
+ />
152
259
  )}
153
260
 
154
- {/* matrix: subType is REQUIRED — never route on type alone */}
155
- {question.type === 'matrix' && (question.subType === 'CSAT_MATRIX' || question.subType === 'RATING_MATRIX') && (
156
- <div className="space-y-3">
157
- <CsatMatrixScale
158
- question={question}
159
- selectedValue={typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue) ? selectedValue : {}}
160
- onSelect={onSelect}
161
- />
162
- </div>
261
+ {(question.type === QUESTION_TYPE.CSAT_MATRIX ||
262
+ question.type === QUESTION_TYPE.RATING_MATRIX) && (
263
+ <CsatMatrixScale
264
+ question={matrixQuestion as Extract<SurveyQuestion, { type: typeof QUESTION_TYPE.CSAT_MATRIX | typeof QUESTION_TYPE.RATING_MATRIX }>}
265
+ selectedValue={typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue) ? selectedValue : {}}
266
+ onSelect={onSelect}
267
+ />
163
268
  )}
164
269
 
165
- {question.type === 'matrix' && question.subType === 'CFM_MATRIX' && (
166
- <div className="space-y-3">
167
- <LikertMatrixScale
168
- question={question}
169
- selectedValue={typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue) ? selectedValue : {}}
170
- onSelect={onSelect}
171
- />
172
- </div>
270
+ {question.type === QUESTION_TYPE.SLIDER_MATRIX && (
271
+ <SliderMatrixScale
272
+ question={matrixQuestion as typeof question & { type: typeof QUESTION_TYPE.SLIDER_MATRIX }}
273
+ selectedValue={typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue) ? selectedValue : {}}
274
+ onSelect={onSelect}
275
+ />
173
276
  )}
174
277
 
175
- {question.type === 'slider_matrix' && (
176
- <div className="space-y-3">
177
- <SliderMatrixScale
178
- question={question}
179
- selectedValue={typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue) ? selectedValue : {}}
180
- onSelect={onSelect}
181
- />
182
- </div>
278
+ {question.type === QUESTION_TYPE.FILE_UPLOAD && (
279
+ <FileUploadScale question={question} selectedValue={selectedValue} onSelect={onSelect} />
183
280
  )}
184
281
 
185
- {question.type === 'file_upload' && (
186
- <div className="space-y-3">
187
- <FileUploadScale
188
- question={question}
189
- selectedValue={selectedValue}
190
- onSelect={onSelect}
191
- />
192
- </div>
282
+ {question.type === QUESTION_TYPE.HEATMAP && (
283
+ <HeatmapScale
284
+ question={question as HeatmapQuestion}
285
+ selectedValue={Array.isArray(selectedValue) ? (selectedValue as HeatmapAnswer) : undefined}
286
+ onSelect={onSelect}
287
+ />
193
288
  )}
194
289
 
195
- {question.type === 'text_and_media' && question.mediaUrl && (
196
- <div className={`mt-4 flex w-full ${
197
- question.mediaAlignment === 'TOP_CENTER' || question.mediaAlignment === 'BOTTOM_CENTER'
198
- ? 'justify-center'
199
- : question.mediaAlignment === 'TOP_RIGHT' || question.mediaAlignment === 'BOTTOM_RIGHT'
200
- ? 'justify-end'
201
- : 'justify-start'
202
- }`}>
203
- <div
204
- style={{ width: question.mediaSize ? `${question.mediaSize}%` : '100%' }}
205
- className="flex flex-col gap-2"
206
- >
207
- {question.mediaMimeType?.startsWith('video/') || question.mediaMimeType === 'VIDEO' ? (
208
- <video controls src={question.mediaUrl} className="max-w-full rounded-md shadow-sm" />
290
+ {question.type === QUESTION_TYPE.TEXT_AND_MEDIA && question.media?.url && (
291
+ <div className={`mt-4 flex w-full ${question.media.alignment?.includes('CENTER') ? 'justify-center' : question.media.alignment?.includes('RIGHT') ? 'justify-end' : 'justify-start'}`}>
292
+ <div style={{ width: question.media.size ? `${question.media.size}%` : '100%' }}>
293
+ {question.media.mimeType?.startsWith('video/') || question.media.mimeType === 'VIDEO' ? (
294
+ <video controls src={question.media.url} className="max-w-full rounded-md" />
209
295
  ) : (
210
- <img src={question.mediaUrl} alt={question.mediaTitle || ''} className="max-w-full rounded-md shadow-sm" />
211
- )}
212
- {question.mediaTitle && (
213
- <p className="text-sm text-gray-500 text-center">{question.mediaTitle}</p>
296
+ <img src={question.media.url} alt={question.media.title || ''} className="max-w-full rounded-md" />
214
297
  )}
298
+ {question.media.title && <p className="text-sm text-gray-500 text-center mt-2">{question.media.title}</p>}
215
299
  </div>
216
300
  </div>
217
301
  )}
218
302
 
219
- {/* rating_scale / slider: implement per 03-ui-specs/05-rating-scale.md and 06-slider.md if present in survey */}
220
- {question.type === 'rating_scale' && (
221
- <div className="space-y-3">
222
- <CsatScale
223
- question={{ ...question, type: 'csat', buttonType: question.scaleStyle === 'star' ? 'star' : 'emoji' }}
224
- selectedValue={selectedValue as number | null}
225
- onSelect={onSelect}
226
- />
227
- </div>
228
- )}
229
-
230
- {question.type === 'slider' && (
231
- <div className="space-y-3">
232
- {/* Wire CustomSliderTrack — min/max/step from question; see 03-ui-specs/06-slider.md */}
233
- </div>
234
- )}
235
-
236
303
  {validationError && (
237
- <div className="mt-4 rounded-[4px] border border-[#333333] bg-[#d9d9d9] py-3.5 px-4 text-left">
238
- <p className="text-[13px] sm:text-sm font-bold text-gray-900 leading-tight">{validationError}</p>
304
+ <div className="mt-4 rounded border border-gray-800 bg-gray-200 py-3 px-4">
305
+ <p className="text-sm font-bold text-gray-900">{validationError}</p>
239
306
  </div>
240
307
  )}
241
308
  </section>