@explorer02/cfm-survey-sdk 0.2.2 → 0.2.4

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 (113) hide show
  1. package/dist/cli/index.js +140 -33
  2. package/dist/cli/index.mjs +140 -33
  3. package/dist/index.js +1 -1
  4. package/dist/index.mjs +1 -1
  5. package/package.json +8 -1
  6. package/postinstall.js +8 -5
  7. package/templates/AGENT.md +62 -17
  8. package/templates/deploy-wizard-to-s3.sh +52 -0
  9. package/templates/docs/00-integration/agent-execution-flow.md +281 -0
  10. package/templates/docs/00-integration/agent-operating-contract.md +260 -0
  11. package/templates/docs/00-integration/analytics-events-catalog.md +54 -0
  12. package/templates/docs/00-integration/apply-ui-config.md +365 -0
  13. package/templates/docs/00-integration/canonical-survey-fixtures.md +94 -0
  14. package/templates/docs/00-integration/client-integration-guide.md +10 -8
  15. package/templates/docs/00-integration/client-lib-folder.md +115 -0
  16. package/templates/docs/00-integration/component-checklist.md +90 -54
  17. package/templates/docs/00-integration/constraints.md +13 -5
  18. package/templates/docs/00-integration/custom-field-logic-and-navigation.md +56 -0
  19. package/templates/docs/00-integration/display-logic-and-navigation.md +16 -1
  20. package/templates/docs/00-integration/file-upload-aws.md +116 -0
  21. package/templates/docs/00-integration/logic-fields-catalog.md +105 -0
  22. package/templates/docs/00-integration/partial-save-and-recovery.md +24 -0
  23. package/templates/docs/00-integration/progress.md +4 -2
  24. package/templates/docs/00-integration/question-type-sdk-matrix.md +11 -9
  25. package/templates/docs/00-integration/setup.md +15 -3
  26. package/templates/docs/00-integration/skip-logic-and-navigation.md +1 -1
  27. package/templates/docs/00-integration/survey-lifecycle-analytics.md +1 -1
  28. package/templates/docs/00-integration/ui-customization-wizard.md +144 -0
  29. package/templates/docs/00-integration/useSurveySDK.md +35 -1
  30. package/templates/docs/00-integration/wizard-api.md +134 -0
  31. package/templates/docs/00-integration/wizard-config-handoff.md +246 -0
  32. package/templates/docs/00-integration/wizard-preview-build-guide.md +400 -0
  33. package/templates/docs/00-integration/wizard-question-type-styling.md +195 -0
  34. package/templates/docs/01-components/01-survey-page.md +18 -8
  35. package/templates/docs/01-components/02-question.md +2 -2
  36. package/templates/docs/01-components/03-rating-scale.md +12 -18
  37. package/templates/docs/01-components/05-csat-matrix-scale.md +18 -26
  38. package/templates/docs/01-components/06-likert-matrix-scale.md +19 -38
  39. package/templates/docs/01-components/07-slider-matrix-scale.md +48 -33
  40. package/templates/docs/01-components/08-file-upload-scale.md +15 -52
  41. package/templates/docs/01-components/09-custom-slider-track.md +15 -25
  42. package/templates/docs/01-components/10-header-footer.md +23 -4
  43. package/templates/docs/01-components/11-progress-bar.md +14 -6
  44. package/templates/docs/01-components/13-matrix-dropdown.md +28 -33
  45. package/templates/docs/01-components/17-heatmap-scale.md +4 -4
  46. package/templates/docs/01-components/18-rank-order-scale.md +24 -31
  47. package/templates/docs/01-components/19-survey-sticky-chrome.md +79 -0
  48. package/templates/docs/01-components/README.md +2 -2
  49. package/templates/docs/02-reference/question-types/10-slider-matrix.md +38 -26
  50. package/templates/docs/02-reference/question-types/11-file-upload.md +36 -21
  51. package/templates/docs/02-reference/question-types/README.md +11 -1
  52. package/templates/docs/02-reference/routing-table.md +1 -0
  53. package/templates/docs/02-reference/value-derivation.md +1 -1
  54. package/templates/docs/03-ui-specs/00-question-shell.md +10 -0
  55. package/templates/docs/03-ui-specs/01-rating.md +28 -2
  56. package/templates/docs/03-ui-specs/02-radio.md +94 -24
  57. package/templates/docs/03-ui-specs/03-text.md +12 -1
  58. package/templates/docs/03-ui-specs/04-csat.md +90 -26
  59. package/templates/docs/03-ui-specs/06-slider.md +3 -1
  60. package/templates/docs/03-ui-specs/07-matrix-cfm.md +74 -35
  61. package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +50 -16
  62. package/templates/docs/03-ui-specs/09-slider-matrix.md +70 -26
  63. package/templates/docs/03-ui-specs/10-file-upload.md +52 -20
  64. package/templates/docs/03-ui-specs/11-text-and-media.md +4 -0
  65. package/templates/docs/03-ui-specs/12-survey-chrome.md +92 -5
  66. package/templates/docs/03-ui-specs/13-heatmap.md +21 -3
  67. package/templates/docs/03-ui-specs/14-rank-order.md +108 -28
  68. package/templates/docs/03-ui-specs/README.md +11 -7
  69. package/templates/docs/03-ui-specs/shared/custom-slider-track.md +28 -35
  70. package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +1 -1
  71. package/templates/docs/MANIFEST.json +187 -6
  72. package/templates/docs/index.md +116 -116
  73. package/templates/docs/templates/CsatMatrixScale.tsx +637 -0
  74. package/templates/docs/templates/CustomSliderTrack.tsx +144 -0
  75. package/templates/docs/templates/FileUploadScale.tsx +262 -0
  76. package/templates/docs/templates/HeatmapScale.tsx +114 -0
  77. package/templates/docs/templates/LikertMatrixScale.tsx +414 -0
  78. package/templates/docs/templates/MatrixDropdown.tsx +216 -0
  79. package/templates/docs/templates/Question.tsx +3 -0
  80. package/templates/docs/templates/RankOrderScale.tsx +353 -0
  81. package/templates/docs/templates/RatingScale.tsx +129 -0
  82. package/templates/docs/templates/SliderMatrixScale.tsx +248 -0
  83. package/templates/docs/templates/SurveyStickyChrome.tsx +24 -0
  84. package/templates/docs/templates/customFieldValues.ts +36 -0
  85. package/templates/docs/templates/implementation_plan.md +75 -8
  86. package/templates/docs/templates/labelStyles.ts +33 -0
  87. package/templates/docs/templates/selectionStyles.ts +100 -0
  88. package/templates/docs/templates/survey-inventory.schema.json +28 -23
  89. package/templates/docs/templates/surveyUiIcons.tsx +63 -0
  90. package/templates/docs/templates/surveyUiScaleUtils.ts +51 -0
  91. package/templates/docs/templates/verify-agent-build.sh +151 -0
  92. package/templates/preview-harness/preview-bridge.inline.js +201 -0
  93. package/templates/preview-harness/previewPages.js +108 -0
  94. package/templates/preview-harness/previewPages.ts +298 -0
  95. package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +67 -0
  96. package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +122 -0
  97. package/templates/preview-harness/vite-app/src/SurveyPagePreview.tsx +75 -0
  98. package/templates/preview-harness/vite-app/src/fixtures/questions.ts +311 -0
  99. package/templates/preview-harness/vite-app/src/globals.css +16 -0
  100. package/templates/preview-harness/vite-app/src/mount.tsx +9 -0
  101. package/templates/preview-harness/vite-app/src/preview-live-overrides.css +101 -0
  102. package/templates/preview-harness/vite-app/stubs/next-dynamic.tsx +25 -0
  103. package/templates/preview-harness/vite-app/stubs/next-image.tsx +29 -0
  104. package/templates/previewBridge.ts +153 -0
  105. package/templates/survey-theme.css +295 -0
  106. package/templates/survey-ui-config.schema.json +213 -0
  107. package/templates/wizard-dist/assets/PreviewMock-DgHfrVeb.js +1 -0
  108. package/templates/wizard-dist/assets/TypePanel-CFVC3Ptn.js +1 -0
  109. package/templates/wizard-dist/assets/index-BhWM50Yu.css +1 -0
  110. package/templates/wizard-dist/assets/index-DYK3X1e5.js +34 -0
  111. package/templates/wizard-dist/assets/vendor-BwkXDkd3.js +17 -0
  112. package/templates/wizard-dist/index.html +20 -0
  113. package/templates/wizard.html +13 -1129
@@ -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)
@@ -0,0 +1,79 @@
1
+ # Survey Sticky Chrome
2
+
3
+ > **UI spec (authoritative):** `03-ui-specs/12-survey-chrome.md`
4
+ > **Reference implementation:** `apps/client/src/components/SurveyStickyChrome.tsx`
5
+ > **Template:** [`templates/SurveyStickyChrome.tsx`](../templates/SurveyStickyChrome.tsx)
6
+ > **Component**: `SurveyStickyChrome`
7
+
8
+ ## Purpose
9
+
10
+ Wrap **Header + ProgressBar** in a single `sticky top-0` container so the progress bar stays visible when the respondent scrolls to answer questions lower on the page (especially the last question on a multi-question page).
11
+
12
+ ## Props
13
+
14
+ ```typescript
15
+ type SurveyStickyChromeProps = {
16
+ progressPercentage: number; // from state.progressPercentage
17
+ };
18
+ ```
19
+
20
+ ## Implementation
21
+
22
+ ```tsx
23
+ export default function SurveyStickyChrome({ progressPercentage }: SurveyStickyChromeProps) {
24
+ return (
25
+ <div className="sticky top-0 z-30 bg-white shadow-[0_2px_8px_rgba(0,0,0,0.06)]">
26
+ <Header embedded />
27
+ <div className="mx-auto w-full max-w-4xl px-4 pb-5 pt-4 sm:px-6 lg:px-8">
28
+ <ProgressBar progressPercentage={progressPercentage} />
29
+ </div>
30
+ </div>
31
+ );
32
+ }
33
+ ```
34
+
35
+ ## Spacing (logo → progress bar)
36
+
37
+ Keep a visible gap between the logo block and the progress track — do **not** flush them together.
38
+
39
+ | Token | Tailwind | Purpose |
40
+ |-------|----------|---------|
41
+ | Top inset | `pt-4` | 16px gap below logo / header row |
42
+ | Bottom inset | `pb-5` | 20px gap above scrollable question content |
43
+ | Horizontal | `px-4 sm:px-6 lg:px-8` | Align track with `main` content column (`max-w-4xl`) |
44
+
45
+ ## Header `embedded` prop
46
+
47
+ When `Header` is inside sticky chrome, pass `embedded` to avoid a double shadow:
48
+
49
+ ```tsx
50
+ <header className={`relative h-[120px] w-full bg-white ${embedded ? '' : 'z-10 shadow-[0_2px_8px_rgba(0,0,0,0.06)]'}`}>
51
+ ```
52
+
53
+ Use the standalone `Header` (no `embedded`) on intro, end, loading, and terminal pages.
54
+
55
+ ## SurveyPage wiring (content phase only)
56
+
57
+ ```tsx
58
+ <div className="flex min-h-screen flex-col bg-white">
59
+ <SurveyStickyChrome progressPercentage={state.progressPercentage} />
60
+
61
+ <main className="mx-auto w-full max-w-4xl flex-1 px-4 py-10 sm:px-6 lg:px-8">
62
+ <LanguageSelector ... />
63
+ {/* questions + nav */}
64
+ </main>
65
+
66
+ <Footer />
67
+ </div>
68
+ ```
69
+
70
+ Do **not** render a separate `ProgressBar` inside `<main>` — it belongs in sticky chrome only.
71
+
72
+ ## Agent checklist
73
+
74
+ - [ ] `SurveyStickyChrome` (or equivalent sticky wrapper) on `phase.kind === 'content'`
75
+ - [ ] `ProgressBar` bound to `state.progressPercentage`
76
+ - [ ] `sticky top-0 z-30 bg-white` on wrapper; single shadow on wrapper, not Header
77
+ - [ ] `pt-4` gap between logo and progress track; `pb-5` below track
78
+ - [ ] Progress track aligned to same `max-w-4xl` as question content
79
+ - [ ] Standalone `Header` on intro / end / loading / terminal branches
@@ -15,8 +15,7 @@ SurveyPage (phase router)
15
15
  ├── IntroPage / PausedPage / EndPage (special phases)
16
16
 
17
17
  └── SurveyContent (phase.kind === 'content')
18
- ├── Header
19
- ├── ProgressBar
18
+ ├── SurveyStickyChrome (sticky Header + ProgressBar)
20
19
  ├── LanguageSelector
21
20
  ├── Question × N (dispatcher)
22
21
  ├── Back / Next|Submit nav
@@ -62,6 +61,7 @@ Shared: `CustomSliderTrack`, `MatrixDropdown`
62
61
  | `09-custom-slider-track.md` | `CustomSliderTrack` |
63
62
  | `10-header-footer.md` | `Header` & `Footer` |
64
63
  | `11-progress-bar.md` | `ProgressBar` |
64
+ | `19-survey-sticky-chrome.md` | `SurveyStickyChrome` (sticky Header + ProgressBar) |
65
65
  | `12-language-selector.md` | `LanguageSelector` |
66
66
  | `13-matrix-dropdown.md` | `MatrixDropdown` |
67
67
  | `14-intro-page.md` | `IntroPage` |
@@ -2,50 +2,62 @@
2
2
 
3
3
  > **Type**: `'SLIDER_MATRIX'` | **API Source**: `SLIDER_MATRIX`
4
4
  > **Mapper**: `sliderMatrixMapper.ts` | **Component**: `SliderMatrixScale`
5
+ > **Portable template**: [`templates/SliderMatrixScale.tsx`](../../templates/SliderMatrixScale.tsx)
5
6
 
6
7
  ## TypeScript Type
7
8
 
8
9
  ```typescript
9
- type SliderMatrixQuestion = QuestionBase & {
10
+ type SliderMatrixQuestion = {
10
11
  type: 'SLIDER_MATRIX';
11
- rows: SliderMatrixRow[]; // Each row has independent slider config
12
- labels?: string[]; // Anchor labels above track
13
- ticks?: number; // Number of tick marks (default 10)
14
- tickValues?: TickValue[]; // Pre-calculated tick values with colors
15
- enableInputBox?: boolean;
12
+ id: string;
13
+ questionText: string;
14
+ questionDescription?: string;
15
+ required: boolean;
16
+ questionNumber?: number;
17
+ statementRows: SliderMatrixStatementRow[];
18
+ scaleAnchorLabels?: string[];
19
+ ticks: {
20
+ count: number;
21
+ marks: { tickPosition: number; tickLabel: string }[];
22
+ };
23
+ enableInputBox: boolean;
16
24
  displayValues?: boolean;
17
- enableNotApplicable?: boolean;
18
- sliderType?: 'graphics'; // Emoji thumb slider
25
+ hasNotApplicableOption: boolean;
26
+ sliderStyle?: 'standard' | 'graphics';
19
27
  };
20
28
 
21
- type SliderMatrixRow = MatrixRow & {
22
- min: number; max: number; step: number; defaultValue?: number;
29
+ type SliderMatrixStatementRow = StatementRow & {
30
+ min: number;
31
+ max: number;
32
+ step: number;
33
+ defaultValue?: number;
23
34
  };
24
-
25
- type TickValue = { value: number; label: string; color: string; };
26
35
  ```
27
36
 
28
37
  ## Answer Shape: `MatrixRowAnswers`
38
+
29
39
  ```typescript
30
- { "row1_id": 75, "row2_id": 30, "row3_id": null }
31
- // number = slider position, null = N/A
40
+ { "486d2930-4b96-4ccd-abf7-19fe0fc63f57": 5, "4fba0abf-...": "N/A" }
41
+ // number = slider value; 'N/A' = not applicable (string, not null)
32
42
  ```
33
43
 
44
+ Skip logic field `SLIDER` uses values like `"rowId_tickValue"` (e.g. `"486d2930-..._5"`).
45
+
34
46
  ## Rendering Guidance
35
- - Each row: row label + independent range slider
36
- - If `sliderType === 'graphics'` use `CustomSliderTrack` (emoji thumb)
37
- - If `tickValues` available render tick marks with labels and colors
38
- - If `enableInputBox` show numeric input alongside slider
39
- - If `enableNotApplicable`add N/A checkbox per row
40
- - `onSelect` receives the full `MatrixRowAnswers` with all row values
47
+
48
+ - Copy portable `SliderMatrixScale` always uses `CustomSliderTrack` per row
49
+ - Shared header: colored badges from `ticks.marks` via `tickColorFromIndex`
50
+ - Optional `scaleAnchorLabels` above tick badges
51
+ - `sliderStyle === 'graphics'` → emoji thumb on `CustomSliderTrack`
52
+ - `enableInputBox` numeric input + stepper buttons per row
53
+ - `hasNotApplicableOption` → checkbox; value `'N/A'` disables slider
41
54
 
42
55
  ## SDK Integration Checklist (agent)
43
56
 
57
+ - [ ] Copied templates before customizing
44
58
  - [ ] Type literal: `QUESTION_TYPE.SLIDER_MATRIX`
45
- - [ ] Answer stored as: `MatrixRowAnswers` — numeric slider value or `null` for N/A per row
46
- - [ ] onAction CHANGE payload: `{ questionId, answerValue: MatrixRowAnswers }`
47
- - [ ] Visibility helper: `getVisibleStatementRowsForAnswers(question, answers, allQuestions, customFieldValues?)`
48
- - [ ] Custom fields: 4th arg on statement-row helper when logic references CRM fields
49
- - [ ] Logic features: skip ✓ display ✓ answer ✓ (rows) variants ✓ numbering ✓
50
- - [ ] Performance: debounce drag events if partial save enabled; one slider per visible row
59
+ - [ ] Answer stored as: `MatrixRowAnswers` — number or `'N/A'` per row
60
+ - [ ] onAction CHANGE: `{ questionId, answerValue: MatrixRowAnswers }`
61
+ - [ ] Visibility helper: `getVisibleStatementRowsForAnswers(..., customFieldValues?)`
62
+ - [ ] Statement text in 25% row column via `dangerouslySetInnerHTML`
51
63
  - [ ] Matrix row: [`question-type-sdk-matrix.md#slider_matrix`](../../00-integration/question-type-sdk-matrix.md#slider_matrix)
@@ -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
@@ -48,5 +48,6 @@ import { QUESTION_TYPE } from '@explorer02/cfm-survey-sdk';
48
48
  | `CustomSliderTrack` | `01-components/09-custom-slider-track.md` |
49
49
  | `Header` / `Footer` | `01-components/10-header-footer.md` |
50
50
  | `ProgressBar` | `01-components/11-progress-bar.md` |
51
+ | `SurveyStickyChrome` | `01-components/19-survey-sticky-chrome.md` |
51
52
  | `LanguageSelector` | `01-components/12-language-selector.md` |
52
53
  | `MatrixDropdown` | `01-components/13-matrix-dropdown.md` |
@@ -24,7 +24,7 @@ type ScaleColumn = { id: string; label: string };
24
24
  - Filter N/A from scale pickers: `scaleColumns.filter(col => col.id !== 'na')`
25
25
  - Slider matrix rows still store numeric slider positions (unchanged)
26
26
 
27
- Monorepo client mirror: `apps/client/src/lib/surveyUi/scaleValues.ts` exports the same helper for matrix UI components.
27
+ Portable template: [`templates/surveyUiScaleUtils.ts`](../templates/surveyUiScaleUtils.ts) exports `matrixColumnStoredValue` (same contract). Copy to `src/lib/` — see [`client-lib-folder.md`](../00-integration/client-lib-folder.md).
28
28
 
29
29
  ## NPS / star rating options
30
30
 
@@ -31,3 +31,13 @@ Never use a `default` fallback that shows `"not yet implemented"`.
31
31
 
32
32
  - Answers: `state.answers` (not `state.values`)
33
33
  - Change action: `{ type: 'CHANGE', payload: { questionId, answerValue } }`
34
+
35
+ ## Wizard customization (Phase 5 — required)
36
+
37
+ Question shell inherits global theme tokens — no per-shell hex overrides:
38
+
39
+ - Validation banner: `--cfm-validation-border`, `--cfm-validation-bg`
40
+ - Question title: `--cfm-question-title-color`
41
+ - Required asterisk: `--cfm-required-color`
42
+
43
+ See [`wizard-preview-build-guide.md`](../00-integration/wizard-preview-build-guide.md) for `data-cfm-*` on wrapper-adjacent chrome only.
@@ -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
 
@@ -36,3 +44,21 @@ See [`02-reference/value-derivation.md`](../02-reference/value-derivation.md).
36
44
  - [ ] Two-row grid with `gridTemplateColumns: repeat(options.length, 1fr)`
37
45
  - [ ] Only track row is clickable
38
46
  - [ ] min/mid/max labels in Question wrapper with midLabelIndex formula
47
+
48
+ ## Wizard customization (Phase 5 — required)
49
+
50
+ > Reference hex in this spec are defaults. Wire `var(--cfm-*)` per [`wizard-question-type-styling.md`](../00-integration/wizard-question-type-styling.md). Copy [`selectionStyles.ts`](../templates/selectionStyles.ts) and [`labelStyles.ts`](../templates/labelStyles.ts).
51
+
52
+ | Control | CSS vars | Apply in |
53
+ |---------|----------|----------|
54
+ | Track bar / highlight | `--cfm-nps-track-bar`, `--cfm-nps-track-highlight`, `--cfm-nps-selected-fill` | `RatingScale.tsx` track row |
55
+ | Hint labels (min/max) | `--cfm-hint-label-color`, `--cfm-hint-label-bg` | `hintLabelStyle()` on anchor row |
56
+ | Number labels | `--cfm-number-label-mode`, `--cfm-number-mono-color`, `--cfm-number-color-{n}` | Badge row — unselected only |
57
+ | Selection | `--cfm-matrix-selected`, `--cfm-input-focus-ring` | `scaleRadioRingStyle` / `scaleRadioDotStyle` |
58
+
59
+ ### Wizard checklist
60
+
61
+ - [ ] `selectionStyles.ts` imported in `RatingScale.tsx`
62
+ - [ ] Selected radio: outer border + dot = focus ring; annulus = cell selected
63
+ - [ ] Selected numbered badge: border = focus ring, fill = cell selected, text white
64
+ - [ ] Unselected badges keep traffic-light / per-number `--cfm-number-*` colors
@@ -2,44 +2,114 @@
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
93
+ - [ ] Use `selectionMode`, not `isMultiSelect`
94
+ - [ ] Full-width card options; custom circular radio / checkbox indicators
95
+ - [ ] Selected: use `var(--cfm-input-focus-ring)` border + `var(--cfm-mcq-selected-bg)` / `var(--cfm-matrix-selected)` — not hardcoded `#e20074` / `#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
100
+ - [ ] Matrix row: [`question-type-sdk-matrix.md#mcq`](../00-integration/question-type-sdk-matrix.md#mcq)
40
101
 
41
- ## SDK Integration Checklist (agent)
102
+ ## Wizard customization (Phase 5 — required)
42
103
 
43
- - [ ] Visibility helper: `getVisibleMcqOptionsForAnswers`
44
- - [ ] Store `option.id` — never option label text
45
- - [ ] Matrix row: [`question-type-sdk-matrix.md#mcq`](../00-integration/question-type-sdk-matrix.md#mcq)
104
+ > Wire MCQ tokens per [`wizard-question-type-styling.md`](../00-integration/wizard-question-type-styling.md). Copy [`selectionStyles.ts`](../templates/selectionStyles.ts).
105
+
106
+ | Control | CSS vars | Apply in |
107
+ |---------|----------|----------|
108
+ | Option gap / radius / padding | `--cfm-mcq-option-gap`, `--cfm-mcq-border-radius`, `--cfm-mcq-card-padding` | MCQ option cards |
109
+ | Selection (radio) | `--cfm-input-focus-ring`, `--cfm-matrix-selected`, `--cfm-mcq-selected-bg` | Outer ring border + annulus + inner dot |
110
+ | Selection (checkbox) | `--cfm-input-focus-ring`, `--cfm-mcq-selected-bg` | Checkbox border + fill |
111
+
112
+ ### Wizard checklist
113
+
114
+ - [ ] Single-select radio uses ring + annulus + dot pattern from `selectionStyles.ts`
115
+ - [ ] `data-cfm-mcq-style` on `:root` when `questionTypes.MCQ.optionStyle` set
@@ -1,6 +1,6 @@
1
1
  # UI Spec: Text Field (`type: 'TEXTFIELD'`)
2
2
 
3
- > **Reference:** `apps/client/src/components/Question.tsx` (inline)
3
+ > **Reference:** portable `docs/templates/Question.tsx` (inline TEXTFIELD branch)
4
4
  > **SDK type:** `TextInputQuestion` — `fetchSurvey/types/text.ts`
5
5
 
6
6
  ## Config Inventory
@@ -49,3 +49,14 @@ onSelect(string) // onChange from textarea
49
49
  - [ ] Character counter bottom-right
50
50
  - [ ] Magenta focus ring
51
51
  - [ ] Implement `inputVariant: 'short'` as single-line input when present
52
+
53
+ ## Wizard customization (Phase 5 — required)
54
+
55
+ | Control | CSS vars | Apply in |
56
+ |---------|----------|----------|
57
+ | Input radius / border / placeholder | `--cfm-input-radius`, `--cfm-input-border`, `--cfm-input-placeholder` | `textarea` / `input` |
58
+ | Focus outline | `--cfm-input-focus-ring` | `:focus-visible` ring |
59
+
60
+ ### Wizard checklist
61
+
62
+ - [ ] No hardcoded magenta focus hex — use `var(--cfm-input-focus-ring)`