@explorer02/cfm-survey-sdk 0.2.2 → 0.2.3

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 (41) hide show
  1. package/package.json +1 -1
  2. package/templates/AGENT.md +8 -4
  3. package/templates/docs/00-integration/analytics-events-catalog.md +54 -0
  4. package/templates/docs/00-integration/client-integration-guide.md +1 -1
  5. package/templates/docs/00-integration/component-checklist.md +10 -0
  6. package/templates/docs/00-integration/display-logic-and-navigation.md +16 -1
  7. package/templates/docs/00-integration/file-upload-aws.md +116 -0
  8. package/templates/docs/00-integration/logic-fields-catalog.md +105 -0
  9. package/templates/docs/00-integration/partial-save-and-recovery.md +24 -0
  10. package/templates/docs/00-integration/question-type-sdk-matrix.md +11 -9
  11. package/templates/docs/00-integration/setup.md +3 -3
  12. package/templates/docs/00-integration/skip-logic-and-navigation.md +1 -1
  13. package/templates/docs/00-integration/survey-lifecycle-analytics.md +1 -1
  14. package/templates/docs/01-components/06-likert-matrix-scale.md +2 -1
  15. package/templates/docs/01-components/08-file-upload-scale.md +15 -52
  16. package/templates/docs/01-components/13-matrix-dropdown.md +28 -33
  17. package/templates/docs/01-components/17-heatmap-scale.md +4 -4
  18. package/templates/docs/01-components/18-rank-order-scale.md +24 -31
  19. package/templates/docs/02-reference/question-types/11-file-upload.md +36 -21
  20. package/templates/docs/02-reference/question-types/README.md +11 -1
  21. package/templates/docs/03-ui-specs/01-rating.md +10 -2
  22. package/templates/docs/03-ui-specs/02-radio.md +80 -25
  23. package/templates/docs/03-ui-specs/04-csat.md +35 -5
  24. package/templates/docs/03-ui-specs/07-matrix-cfm.md +20 -4
  25. package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +5 -1
  26. package/templates/docs/03-ui-specs/09-slider-matrix.md +17 -0
  27. package/templates/docs/03-ui-specs/10-file-upload.md +41 -20
  28. package/templates/docs/03-ui-specs/12-survey-chrome.md +38 -0
  29. package/templates/docs/03-ui-specs/13-heatmap.md +9 -2
  30. package/templates/docs/03-ui-specs/14-rank-order.md +98 -29
  31. package/templates/docs/03-ui-specs/README.md +7 -5
  32. package/templates/docs/03-ui-specs/shared/custom-slider-track.md +28 -35
  33. package/templates/docs/MANIFEST.json +28 -3
  34. package/templates/docs/index.md +5 -4
  35. package/templates/docs/templates/CustomSliderTrack.tsx +144 -0
  36. package/templates/docs/templates/MatrixDropdown.tsx +216 -0
  37. package/templates/docs/templates/RankOrderScale.tsx +353 -0
  38. package/templates/docs/templates/implementation_plan.md +20 -0
  39. package/templates/docs/templates/survey-inventory.schema.json +28 -23
  40. package/templates/docs/templates/surveyUiIcons.tsx +52 -0
  41. package/templates/docs/templates/verify-agent-build.sh +32 -0
@@ -1,46 +1,41 @@
1
1
  # Matrix Dropdown
2
2
 
3
- > **UI spec (authoritative):** `03-ui-specs/shared/matrix-dropdown.md`
4
- > **Reference implementation:** `apps/client/src/components/MatrixDropdown.tsx`
5
- > **Component**: `MatrixDropdown`
6
- > **Used by**: `CsatMatrixScale`, `LikertMatrixScale`
3
+ > **UI spec (authoritative):** [`03-ui-specs/shared/matrix-dropdown.md`](../03-ui-specs/shared/matrix-dropdown.md) — implement the **custom panel** (not a native `<select>`).
4
+ > **Portable template:** copy [`templates/MatrixDropdown.tsx`](../templates/MatrixDropdown.tsx) when available.
5
+ > **Component:** `MatrixDropdown`
6
+ > **Used by:** `CsatMatrixScale`, `LikertMatrixScale`
7
7
 
8
8
  ## Role
9
- Renders a dropdown selector for a single row in a matrix question. Used when `gridLayout === 'dropdown'` or `displayStyle === 'dropdown'`.
10
9
 
11
- Accepts `ScaleColumn[]`; emits stored column ids via `matrixColumnStoredValue(col)` see `02-reference/value-derivation.md`.
10
+ Renders a custom dropdown selector for a single matrix row. Used when `gridLayout === 'dropdown'` or `displayStyle` is `'dropdown'` / `'selectbox'`.
11
+
12
+ Emits stored column **`id`** values via `matrixColumnStoredValue(col)` — see [`02-reference/value-derivation.md`](../02-reference/value-derivation.md).
13
+
14
+ ## Props (wiring only)
12
15
 
13
- ## Props Interface
14
16
  ```typescript
15
17
  type MatrixDropdownProps = {
16
- columns: MatrixColumn[];
17
- selectedValue: string | number | null | undefined;
18
+ options: ScaleColumn[]; // visible columns for this row
19
+ value: string | number | null | undefined;
18
20
  onChange: (value: string | number | null) => void;
21
+ multiple?: boolean;
19
22
  placeholder?: string;
20
23
  };
21
24
  ```
22
25
 
23
- ## Structure
24
- Renders a native HTML `<select>` element.
25
-
26
- ```tsx
27
- <select
28
- value={selectedValue === null ? '' : String(selectedValue ?? '')}
29
- onChange={e => {
30
- const val = e.target.value;
31
- if (val === '') onChange(null); // Map empty string back to null for N/A
32
- else {
33
- // Find original option to keep type (number vs string) intact
34
- const opt = columns.find(c => String(c.value) === val);
35
- onChange(opt ? opt.value : val);
36
- }
37
- }}
38
- >
39
- <option value="" disabled>Select an option...</option>
40
- {columns.map(col => (
41
- <option key={String(col.value)} value={String(col.value)}>
42
- {col.label}
43
- </option>
44
- ))}
45
- </select>
46
- ```
26
+ ## Implementation
27
+
28
+ Do **not** use a native `<select>`. Follow the shared ui-spec for:
29
+
30
+ - Click-to-open panel with click-outside dismiss
31
+ - Single-select: one choice; multi-select: tag chips + toggle
32
+ - N/A mutual exclusion when `hasNotApplicableOption` applies
33
+ - Placeholder text: `"Select..."` (dropdown) or full-width cards (selectbox)
34
+
35
+ See [`03-ui-specs/shared/matrix-dropdown.md`](../03-ui-specs/shared/matrix-dropdown.md) for layout, states, and agent checklist.
36
+
37
+ ## SDK Integration Checklist
38
+
39
+ - [ ] Copy `templates/MatrixDropdown.tsx` or implement per shared ui-spec
40
+ - [ ] `onChange` stores column **`id`** via `matrixColumnStoredValue(col)`
41
+ - [ ] Filter columns with `getVisibleMatrixItemsForAnswers` before passing to dropdown
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **UI spec (authoritative):** `03-ui-specs/13-heatmap.md`
4
4
  > **Reference implementation:** `apps/client/src/components/HeatmapScale.tsx`
5
- > **Coordinate helpers:** `apps/client/src/lib/surveyUi/heatmapCoords.ts`
5
+ > **Portable coord helper:** copy [`templates/heatmapCoords.ts`](../templates/heatmapCoords.ts)
6
6
  > **Target Component**: `HeatmapScale`
7
7
  > **Handles**: `type: 'HEATMAP'`
8
8
 
@@ -15,8 +15,8 @@ Render the heatmap image, capture click spots in normalized 0–1 coordinates, t
15
15
  ```typescript
16
16
  type HeatmapScaleProps = {
17
17
  question: HeatmapQuestion;
18
- selectedValue?: HeatmapAnswer;
19
- onSelect: (answer: HeatmapAnswer | undefined) => void;
18
+ selectedValue?: HeatmapClickPoint[];
19
+ onSelect: (answer: HeatmapClickPoint[] | undefined) => void;
20
20
  };
21
21
  ```
22
22
 
@@ -34,7 +34,7 @@ type HeatmapScaleProps = {
34
34
 
35
35
  ## Coordinate Helpers
36
36
 
37
- Use `apps/client/src/lib/surveyUi/heatmapCoords.ts`:
37
+ Copy [`templates/heatmapCoords.ts`](../templates/heatmapCoords.ts):
38
38
 
39
39
  - `getNormalizedClickFromImage(event, imageElement)`
40
40
  - `findSpotNearClick(clickX, clickY, spots, imageRect)`
@@ -1,13 +1,13 @@
1
1
  # Rank Order Architectural Blueprint
2
2
 
3
3
  > **UI spec (authoritative):** `03-ui-specs/14-rank-order.md`
4
- > **Reference implementation:** `apps/client/src/components/RankOrderScale.tsx`
4
+ > **Portable template:** copy [`templates/RankOrderScale.tsx`](../templates/RankOrderScale.tsx)
5
5
  > **Target Component:** `RankOrderScale`
6
6
  > **Handles:** `type: 'RANK_ORDER'`
7
7
 
8
8
  ## Core Responsibility
9
9
 
10
- Render rankable options in either **dropdown** or **drag-and-drop** mode, support three option display variants (text only, image only, text and image), and bubble rank assignments via `onSelect`.
10
+ Render rankable options in **dropdown** or **drag-and-drop** mode, three option display variants, bubble ranks via `onSelect`.
11
11
 
12
12
  ## Props Contract
13
13
 
@@ -21,9 +21,9 @@ type RankOrderScaleProps = {
21
21
 
22
22
  ## State Management
23
23
 
24
- - `selectedValue` from SDK is source of truth — do not duplicate answer state locally
25
- - Dropdown mode: use `assignRankWithoutDuplicates` from SDK when a rank changes
26
- - Drag-and-drop mode: use `buildRankOrderFromOrderedOptionIds` after reorder
24
+ - `selectedValue` from SDK is source of truth — no duplicate local answer state
25
+ - Dropdown: `assignRankWithoutDuplicates` on rank change
26
+ - Drag-and-drop: `buildRankOrderFromOrderedOptionIds` after reorder; `getOrderedOptionIdsFromRanks` for display order
27
27
 
28
28
  ## Layout Orchestrator
29
29
 
@@ -36,20 +36,9 @@ export function RankOrderScale({ question, selectedValue, onSelect }: RankOrderS
36
36
  }
37
37
  ```
38
38
 
39
- ## Dropdown Mode
39
+ ## shuffleOptions
40
40
 
41
- - One row per visible option
42
- - Left: rank `<select>` with `-` (unassigned) and `1..N`
43
- - Selecting rank `R` clears `R` from any other option (SDK helper)
44
- - Option content depends on `question.optionDisplay`
45
- - **Image URLs:** prefer `option.imageUrl` over `option.previewImageUrl` — preview `*_p.*` URLs often return 403 on CloudFront
46
-
47
- ## Drag-and-Drop Mode
48
-
49
- - Use `@dnd-kit/core` + `@dnd-kit/sortable` for smooth reorder
50
- - List order maps to ranks: position 0 → rank 1
51
- - Show rank badge on each row
52
- - Provide Up/Down buttons for keyboard accessibility
41
+ When API `SHUFFLE_ORDER` is true, SDK shuffles options at map time. **Do not re-shuffle in the client** logic and option ids must stay aligned with fetched order.
53
42
 
54
43
  ## Wiring in Question.tsx
55
44
 
@@ -57,6 +46,7 @@ export function RankOrderScale({ question, selectedValue, onSelect }: RankOrderS
57
46
  import {
58
47
  QUESTION_TYPE,
59
48
  getVisibleRankOrderOptionsForAnswers,
49
+ normalizeRankOrderAnswers,
60
50
  } from '@explorer02/cfm-survey-sdk';
61
51
 
62
52
  const rankOrderQuestion = useMemo(() => {
@@ -64,10 +54,7 @@ const rankOrderQuestion = useMemo(() => {
64
54
  return {
65
55
  ...question,
66
56
  options: getVisibleRankOrderOptionsForAnswers(
67
- question,
68
- allAnswers,
69
- allQuestions,
70
- customFieldValues
57
+ question, allAnswers, allQuestions, customFieldValues
71
58
  ),
72
59
  };
73
60
  }, [question, allAnswers, allQuestions, customFieldValues]);
@@ -75,7 +62,11 @@ const rankOrderQuestion = useMemo(() => {
75
62
  {question.type === QUESTION_TYPE.RANK_ORDER && rankOrderQuestion && (
76
63
  <RankOrderScale
77
64
  question={rankOrderQuestion}
78
- selectedValue={...}
65
+ selectedValue={normalizeRankOrderAnswers(
66
+ typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue)
67
+ ? selectedValue
68
+ : undefined
69
+ )}
79
70
  onSelect={onSelect}
80
71
  />
81
72
  )}
@@ -83,18 +74,20 @@ const rankOrderQuestion = useMemo(() => {
83
74
 
84
75
  ## Dependencies
85
76
 
86
- Add to client `package.json`:
77
+ ```bash
78
+ npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
79
+ ```
80
+
81
+ ## Next.js SSR
87
82
 
88
- - `@dnd-kit/core`
89
- - `@dnd-kit/sortable`
90
- - `@dnd-kit/utilities`
83
+ Template uses `'use client'`. For drag-and-drop, optional `dynamic(..., { ssr: false })` in Question.tsx if hydration warnings occur.
91
84
 
92
85
  ## SDK Integration Checklist (agent)
93
86
 
87
+ - [ ] Copy [`templates/RankOrderScale.tsx`](../templates/RankOrderScale.tsx)
94
88
  - [ ] Type literal: `QUESTION_TYPE.RANK_ORDER`
95
- - [ ] Answer stored as: `RankOrderAnswers`
96
- - [ ] onAction CHANGE: `{ questionId, answerValue: RankOrderAnswers }`
97
- - [ ] Visibility helper: `getVisibleRankOrderOptionsForAnswers(..., customFieldValues?)`
98
- - [ ] Rank utils: `assignRankWithoutDuplicates`, `buildRankOrderFromOrderedOptionIds`, `normalizeRankOrderAnswers`
89
+ - [ ] Answer: `RankOrderAnswers`
90
+ - [ ] Visibility: `getVisibleRankOrderOptionsForAnswers(..., customFieldValues?)`
91
+ - [ ] Utils: `assignRankWithoutDuplicates`, `buildRankOrderFromOrderedOptionIds`, `normalizeRankOrderAnswers`
99
92
  - [ ] Prefer `option.imageUrl` over `previewImageUrl`
100
93
  - [ ] Matrix row: [`question-type-sdk-matrix.md#rank_order`](../00-integration/question-type-sdk-matrix.md#rank_order)
@@ -2,44 +2,59 @@
2
2
 
3
3
  > **Type**: `'FILE_UPLOAD'` | **API Source**: `FILE_UPLOAD`
4
4
  > **Mapper**: `fileUploadMapper.ts` | **Component**: `FileUploadScale`
5
+ > **Upload flow**: [`00-integration/file-upload-aws.md`](../../00-integration/file-upload-aws.md)
5
6
 
6
7
  ## TypeScript Type
7
8
 
8
9
  ```typescript
9
10
  type FileUploadQuestion = QuestionBase & {
10
11
  type: 'FILE_UPLOAD';
11
- uploadMessage?: string; // Instruction text (HTML)
12
- supportedFileFormats?: string[]; // ['PDF', 'PNG', 'DOCX'] (uppercase, no dots)
13
- maxFileCount?: number; // Max files allowed
14
- fileSizeLimit?: number; // Max size in MB
12
+ uploadMessage?: string;
13
+ supportedFileFormats?: string[];
14
+ maxFileCount?: number;
15
+ fileSizeLimit?: number;
15
16
  fileSizeLimitType?: 'PER_FILE' | 'IN_TOTAL';
16
17
  };
17
18
 
18
- type UploadedFile = {
19
- id: string; name: string; mediaUrl: string;
20
- mimeType?: string; size?: number;
19
+ /** Stored in state.answers — required for submit */
20
+ type UploadedFileAnswer = {
21
+ id: string;
22
+ mediaUrl: string;
23
+ name?: string;
24
+ mimeType?: string;
25
+ size?: number;
21
26
  };
22
27
  ```
23
28
 
24
- ## Answer Shape: `UploadedFile[]`
25
- Array of uploaded file descriptors. Submitted via `quesIdVsAttachmentDetails` (NOT `questionToAnswers`).
29
+ ## Answer Shape: `UploadedFileAnswer[]`
30
+
31
+ Submitted via `quesIdVsAttachmentDetails` (NOT `questionToAnswers`). SDK submit formatter only accepts objects with `id` and `mediaUrl` — **raw browser `File[]` is dropped at submit**.
32
+
33
+ ## Two-phase client flow
34
+
35
+ 1. **Pick + validate locally** — extension, count, size (`PER_FILE` / `IN_TOTAL`)
36
+ 2. **Upload each file** — `POST /get-upload-url` → presigned PUT → collect `{ id, mediaUrl, name, mimeType, size }`
37
+ 3. **`onAction(CHANGE)`** — pass `UploadedFileAnswer[]` to SDK
38
+
39
+ See [`file-upload-aws.md`](../../00-integration/file-upload-aws.md) for API contract.
26
40
 
27
41
  ## Rendering Guidance
28
- - Drag-and-drop zone + click-to-upload button
42
+
43
+ - Drag-and-drop zone + click-to-upload
29
44
  - Show accepted formats from `supportedFileFormats`
30
- - Set `<input type="file" accept=".pdf,.png,.docx">`
31
- - Enforce `maxFileCount` in UI
32
- - Show file list with name, size, and remove button
33
- - Enforce `fileSizeLimit` with appropriate error messages
34
- - `onSelect` receives the full `UploadedFile[]` array
45
+ - Set `<input type="file" accept=".pdf,.png,...">` from formats
46
+ - Enforce `maxFileCount` and `fileSizeLimit` before upload
47
+ - Show file list with name, size, remove button, uploading spinner per file
48
+ - `onSelect(undefined)` when all files removed
49
+
50
+ ## Skip logic
51
+
52
+ Presence-only (`EXISTS` / `MISSING`) — evaluated on page entry; no discrete value-change events required.
35
53
 
36
54
  ## SDK Integration Checklist (agent)
37
55
 
38
56
  - [ ] Type literal: `QUESTION_TYPE.FILE_UPLOAD`
39
- - [ ] Answer stored as: `UploadedFile[]`
40
- - [ ] onAction CHANGE payload: `{ questionId, answerValue: UploadedFile[] }` after upload completes
41
- - [ ] Visibility helper: N/A
42
- - [ ] Custom fields: skip/display rules only — no visibility helper
43
- - [ ] Logic features: skip ✓ display ✓ answer ✗ variants ✓ numbering ✓
44
- - [ ] Performance: upload to presigned URL before CHANGE; show progress UI
57
+ - [ ] Answer stored as: `UploadedFileAnswer[]` after upload completes
58
+ - [ ] onAction CHANGE: `{ questionId, answerValue: UploadedFileAnswer[] }`
59
+ - [ ] Do **not** store raw `File[]` as final answer
45
60
  - [ ] Matrix row: [`question-type-sdk-matrix.md#file_upload`](../../00-integration/question-type-sdk-matrix.md#file_upload)
@@ -49,10 +49,20 @@ Each type is **standalone** — open its file in exported SDK types for the comp
49
49
  - **Answer:** `string` or `string[]` (`option.id` values)
50
50
  - **Options:** `McqOption[]` with `id`, `optionLabel`
51
51
 
52
+ | Field | Type | API source |
53
+ |-------|------|------------|
54
+ | `selectionMode` | `'single' \| 'multiple'` | multi-select config |
55
+ | `defaultOptionIds?` | `string[]` | `DEFAULT_ANSWER` when enabled |
56
+ | `minSelections?` | `number` | `ANSWER_CONDITION` / `MIN_ANSWER` |
57
+ | `maxSelections?` | `number` | `ANSWER_CONDITION` / `MAX_ANSWER` |
58
+ | `answerConditionErrorMessage?` | `string` | `ANSWER_CONDITION` error message |
59
+
60
+ **Default answers:** On survey load, SDK `buildInitialAnswers` / `applyMcqDefaults` pre-populates `state.answers[questionId]` from `defaultOptionIds` (single: first id; multi: full array). Client binds UI checked state to `state.answers` — do not maintain separate default state. SDK re-applies defaults on validation/submit if answer is still empty.
61
+
52
62
  ### SDK Integration Checklist (agent)
53
63
 
54
64
  - [ ] Type literal: `QUESTION_TYPE.MCQ`
55
- - [ ] Answer stored as: `option.id` (string/number) or array when multi-select
65
+ - [ ] Answer stored as: `option.id` (string/number) or array when `selectionMode === 'multiple'`
56
66
  - [ ] onAction CHANGE payload: `{ questionId, answerValue: optionId | optionId[] }`
57
67
  - [ ] Visibility helper: `getVisibleMcqOptionsForAnswers(question, answers, allQuestions, customFieldValues?)`
58
68
  - [ ] Custom fields: 4th arg when logic references CRM fields
@@ -17,8 +17,16 @@
17
17
  | `midLabel` | ✅ | Absolute positioned in Question.tsx |
18
18
  | `maxLabel` | ✅ | Absolute positioned in Question.tsx |
19
19
  | `midLabelIndex` | ✅ | Mid label `left` percentage |
20
- | `reverseScaleOrder` | | Document for agents when SDK sends true |
21
- | `buttonVariant` | ❌ | Client always uses dual-layer radio layout |
20
+ | `reverseScaleOrder` | | Reverse option index before selection/color lookup |
21
+ | `buttonVariant` | ❌ | Client uses dual-layer radio layout |
22
+
23
+ ## Answer logic on scale points
24
+
25
+ When scale points have answer logic, filter with `getVisibleScaleColumnsForAnswers(question, answers, allQuestions, customFieldValues?)` before render.
26
+
27
+ ## Implementation Recipe — reverseScaleOrder
28
+
29
+ When `reverseScaleOrder === true`, map visual index `i` to stored index `options.length - 1 - i` before deriving submit value and badge color.
22
30
 
23
31
  ## Answer Mutation
24
32
 
@@ -2,44 +2,99 @@
2
2
 
3
3
  > **SDK type:** `McqQuestion` — `fetchSurvey/types/mcq.ts`
4
4
  > **Component blueprint:** `01-components/02-question.md`
5
+ > **Dispatcher:** inline in `Question.tsx` (see [`templates/Question.tsx`](../templates/Question.tsx))
5
6
 
6
7
  ## Config Inventory
7
8
 
8
- | Field | Implemented | UI impact |
9
- |-------|-------------|-----------|
10
- | `options[]` | ✅ | One card per option |
11
- | `options[].optionLabel` | ✅ | HTML via `dangerouslySetInnerHTML` |
12
- | `options[].id` | ✅ | **Submit value** — selected answer is `option.id` |
13
- | `isMultiSelect` | ⚠️ | SDK supports multi-select; implement checkbox cards when `true` |
9
+ | Field | UI impact |
10
+ |-------|-----------|
11
+ | `options[]` | One card per visible option |
12
+ | `options[].optionLabel` | HTML via `dangerouslySetInnerHTML` |
13
+ | `options[].id` | **Submit value** — answer is `option.id` |
14
+ | `selectionMode` | `'single'` radio cards; `'multiple'` checkbox cards + `string[]` answer |
15
+ | `defaultOptionIds?` | Pre-selected on load (SDK seeds `state.answers`) |
16
+ | `minSelections?` / `maxSelections?` | Answer-condition count (SDK validates on NEXT) |
17
+ | `answerConditionErrorMessage?` | Custom validation banner text |
18
+ | `requiredErrorMessage?` | Required-field error text |
14
19
 
15
- ## Multi-select note
20
+ ## Selection mode (not `isMultiSelect`)
16
21
 
17
- When `question.isMultiSelect === true`:
22
+ Use `question.selectionMode` from the SDK — **never** `isMultiSelect`.
18
23
 
19
- - Answer shape is `string[]` (or `number[]`) of selected `option.id` values
20
- - Toggle options on click; do not replace the entire array with a single id
21
- - onAction CHANGE: `{ questionId, answerValue: selectedIds[] }`
22
- - Still filter via `getVisibleMcqOptionsForAnswers` before render
24
+ | `selectionMode` | UI | Answer shape |
25
+ |-----------------|-----|--------------|
26
+ | `'single'` (default) | Radio cards; one selected | `string` (option `id`) |
27
+ | `'multiple'` | Checkbox cards; toggle on click | `string[]` of option ids |
23
28
 
24
- When `false` (default), single-select radio behavior applies.
29
+ Multi-select toggle pattern:
30
+
31
+ ```typescript
32
+ const handleToggle = (optionId: string) => {
33
+ const next = selectedIds.includes(optionId)
34
+ ? selectedIds.filter(id => id !== optionId)
35
+ : question.maxSelections && selectedIds.length >= question.maxSelections
36
+ ? selectedIds // block add at max
37
+ : [...selectedIds, optionId];
38
+ onSelect(next);
39
+ };
40
+ ```
41
+
42
+ Filter options before render: `getVisibleMcqOptionsForAnswers(question, allAnswers, allQuestions, customFieldValues?)`.
43
+
44
+ ---
45
+
46
+ ## Implementation Recipe — Default answers
47
+
48
+ | Concern | SDK behavior | Client UI contract |
49
+ |---------|--------------|-------------------|
50
+ | **Config** | API `DEFAULT_ANSWER` → `question.defaultOptionIds?: string[]` | Inventory in implementation plan §4b |
51
+ | **Initial load** | `buildInitialAnswers` / `applyMcqDefaults` writes into `state.answers` | Bind checked state to `selectedValue` (`state.answers[question.id]`) |
52
+ | **Single-select** | Default = `defaultOptionIds[0]` | Pre-selected radio when present in `state.answers` |
53
+ | **Multi-select** | Default = full `defaultOptionIds` array | Pre-checked boxes for each id |
54
+ | **User override** | `applyMcqDefaults` never overwrites user selection | Toggle via `onAction(CHANGE)` |
55
+ | **Validation / submit** | SDK re-applies defaults in `validateCurrentPage` and submit if empty | Client does **not** re-apply defaults at NEXT/SUBMIT |
56
+ | **Answer logic** | Hidden defaults may exist in `state.answers` until SDK sanitizes | Show checked state only for **visible** options |
57
+ | **Template fallback** | `Question.tsx` uses `selectedValue ?? defaultOptionIds` defensively | Prefer `state.answers`; fallback is display-only |
58
+
59
+ ---
60
+
61
+ ## Implementation Recipe — Answer conditions
62
+
63
+ When API `ANSWER_CONDITION` is enabled, SDK maps:
64
+
65
+ - `minSelections` — minimum selected count (multi-select)
66
+ - `maxSelections` — maximum selected count (multi-select)
67
+ - `answerConditionErrorMessage` — shown in `state.validationErrors[questionId]`
68
+
69
+ **Client must:**
70
+
71
+ 1. Render `validationError` banner when SDK sets an error (do not duplicate count validation).
72
+ 2. Block adding options when `selectedIds.length >= maxSelections` in multi-select UI.
73
+ 3. Pass through `onAction(CHANGE)` with updated array — SDK validates on NEXT.
74
+
75
+ Skip logic may use `NUM_OF_SELECTED_OPTIONS` — see [`logic-fields-catalog.md`](../00-integration/logic-fields-catalog.md).
76
+
77
+ ---
25
78
 
26
79
  ## Answer Mutation
27
80
 
28
81
  ```typescript
29
- onSelect(option.id) // string
30
- // → onAction CHANGE { questionId, answerValue: string }
82
+ // Single-select
83
+ onSelect(option.id)
84
+ onAction({ type: 'CHANGE', payload: { questionId, answerValue: string } })
85
+
86
+ // Multi-select
87
+ onSelect(selectedIds) // string[]
88
+ onAction({ type: 'CHANGE', payload: { questionId, answerValue: string[] } })
31
89
  ```
32
90
 
33
91
  ## Agent Checklist
34
92
 
35
- - [ ] Full-width card options with gap-3 vertical stack
36
- - [ ] Custom circular radio indicator (not browser default)
37
- - [ ] Selected: magenta border + pink background
38
- - [ ] HTML labels
39
- - [ ] If `isMultiSelect`: implement checkbox cards with array answer shape
40
-
41
- ## SDK Integration Checklist (agent)
42
-
43
- - [ ] Visibility helper: `getVisibleMcqOptionsForAnswers`
44
- - [ ] Store `option.id` — never option label text
93
+ - [ ] Use `selectionMode`, not `isMultiSelect`
94
+ - [ ] Full-width card options; custom circular radio / checkbox indicators
95
+ - [ ] Selected: magenta border `#e20074` + pink bg `#fdf2f8`
96
+ - [ ] Multi-select: `string[]` answer; block at `maxSelections`
97
+ - [ ] Defaults: bind to `state.answers`; pre-selected visual on load when `defaultOptionIds` set
98
+ - [ ] `getVisibleMcqOptionsForAnswers` with optional `customFieldValues` 4th arg
99
+ - [ ] Store `option.id` — never label text
45
100
  - [ ] Matrix row: [`question-type-sdk-matrix.md#mcq`](../00-integration/question-type-sdk-matrix.md#mcq)
@@ -38,12 +38,42 @@ CSAT_MATRIX
38
38
  - Alternating row backgrounds
39
39
  - Column headers OR `scaleAnchorLabels[]` anchor row with percentage positioning
40
40
 
41
- ## Carousel
41
+ ## Carousel — Implementation Recipe
42
42
 
43
- - Pink active dot expands: `20px × 8px` vs `8px` inactive
44
- - Column labels below cells
45
- - Emoji tooltip via hover state
46
- - Graphics mode: slider maps column index
43
+ Same rules as CFM carousel (prev/next disabled at bounds, dot click jumps row, no auto-advance). CSAT uses **pink** active dot `#e20074` (not Likert blue).
44
+
45
+ ## N/A mutex
46
+
47
+ | Layout | Behavior |
48
+ |--------|----------|
49
+ | Grid N/A column | Selecting N/A clears row; selecting column clears N/A |
50
+ | Vertical checkbox | `hasNotApplicableOption` checkbox below row sets `null` |
51
+
52
+ ## Emoji / star / numbered — react-icons mapping
53
+
54
+ Copy [`templates/surveyUiIcons.tsx`](../templates/surveyUiIcons.tsx) or implement `getEmojiForIndex(index, totalColumns)`.
55
+
56
+ **5-point CSAT emoji index → icon key:**
57
+
58
+ | Column index (0-based) | Sentiment | Maps to 11-point scale key |
59
+ |--------------------------|-----------|----------------------------|
60
+ | 0 | Very negative | 3 |
61
+ | 1 | Negative | 4 |
62
+ | 2 | Neutral | 6 |
63
+ | 3 | Positive | 7 |
64
+ | 4 | Very positive | 9 |
65
+
66
+ **Star mode:** filled `FaStar` `#e20074` when selected; `FaRegStar` `#d1d5db` opacity 0.5 when unselected; hover ring `ring-2 ring-[#e20074] ring-offset-2`.
67
+
68
+ **Numbered mode:** 40×40 badge; selected = magenta fill `#e20074` white text; unselected = gray border.
69
+
70
+ **Graphics mode:** `CustomSliderTrack` per row with `ticks={scaleColumns.length}`, `tickLabels={scaleColumns.map(c => c.label)}`, `sliderType="graphics"`.
71
+
72
+ Apply `reverseScaleOrder` by flipping column index before icon lookup.
73
+
74
+ ## Scale column labels on slider (Day 19)
75
+
76
+ Pass `tickLabels={visibleScaleColumns.map(col => col.label)}` to `CustomSliderTrack` so tooltips and graphics thumb show mapped labels, not raw numeric indices.
47
77
 
48
78
  ## Answer Mutation
49
79
 
@@ -46,12 +46,28 @@ CFM_MATRIX
46
46
  - Zebra rows: even rows `rgba(249,250,251,0.8)`
47
47
  - Native inputs: `accentColor: #e20074`, 18×18px
48
48
 
49
- ## Carousel
49
+ ## Carousel — Implementation Recipe
50
50
 
51
- - Blue active dot: `#2563eb` (Likert only)
52
- - Card: white, border, shadow, centered row text
51
+ | Rule | Behavior |
52
+ |------|----------|
53
+ | Prev button | Disabled when `currentRowIndex === 0` |
54
+ | Next button | Disabled when on last visible row |
55
+ | Dot indicators | Click dot → jump to that row index |
56
+ | On answer | **Do not** auto-advance to next row |
57
+ | Keyboard | Optional Left/Right for prev/next |
58
+ | Active dot (Likert) | `#2563eb`, expanded pill `20×8px` vs `8×8px` inactive |
53
59
 
54
- ## Answer Mutation
60
+ One statement row visible at a time; column headers below cells when `showColumnHeaders`.
61
+
62
+ ## N/A mutex (all layouts)
63
+
64
+ | Action | Result |
65
+ |--------|--------|
66
+ | Select N/A | Clear other column values for row; other inputs opacity 0.4 + disabled |
67
+ | Select column after N/A | Clear N/A first |
68
+ | Multi-select + N/A | N/A sets `[null]` exclusively |
69
+
70
+ ## Implementation Recipe
55
71
 
56
72
  Store **`column.id`** via `matrixColumnStoredValue(col)` — see `02-reference/value-derivation.md`.
57
73
 
@@ -19,7 +19,11 @@
19
19
 
20
20
  ## Config Decision Tree
21
21
 
22
- Same as `CSAT_MATRIX` grid — reuse `CsatMatrixScale` component. See [04-csat.md](04-csat.md) decision tree with `type: 'RATING_MATRIX'`.
22
+ Same as `CSAT_MATRIX` — reuse `CsatMatrixScale`. See [04-csat.md](04-csat.md) for emoji/star/carousel/N/A/graphics recipes.
23
+
24
+ ## Carousel / N/A / scale labels
25
+
26
+ Follow CSAT Implementation Recipes in [04-csat.md](04-csat.md): pink carousel dots, N/A mutex, `tickLabels` from `scaleColumns[].label` on graphics sliders.
23
27
 
24
28
  ## Answer Mutation
25
29
 
@@ -36,6 +36,23 @@ sliceWidth = tickValues.length > 1 ? 100 / (tickValues.length - 1) : 100
36
36
  // graphics: plain text label instead of colored badge
37
37
  ```
38
38
 
39
+ ## CustomSliderTrack per row — scale labels
40
+
41
+ ```typescript
42
+ <CustomSliderTrack
43
+ min={row.min}
44
+ max={row.max}
45
+ htmlStep={row.htmlStep}
46
+ value={rowVal as number}
47
+ tickLabels={row.tickLabels ?? scaleColumns.map(c => c.label)}
48
+ ticks={row.ticks}
49
+ sliderType={question.sliderStyle === 'graphics' ? 'graphics' : undefined}
50
+ onChange={v => updateRow(row.id, v)}
51
+ />
52
+ ```
53
+
54
+ Pass **`scaleColumns[].label`** (or row tick labels) as `tickLabels` — required for correct tooltip text (Day 19 fix).
55
+
39
56
  ## N/A Behavior
40
57
 
41
58
  - Checked → `rowVal === 'N/A'`; slider disabled, input grayed