@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
@@ -0,0 +1,248 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Portable SliderMatrixScale — copy to src/components/SliderMatrixScale.tsx
5
+ * Requires: CustomSliderTrack.tsx, surveyUiScaleUtils.ts
6
+ */
7
+
8
+ import React from 'react';
9
+ import type { SliderMatrixQuestion, MatrixRowAnswers } from '@explorer02/cfm-survey-sdk';
10
+ import { tickColorFromIndex } from './surveyUiScaleUtils';
11
+ import { CustomSliderTrack } from './CustomSliderTrack';
12
+
13
+ type SliderMatrixScaleProps = {
14
+ question: SliderMatrixQuestion;
15
+ selectedValue?: MatrixRowAnswers;
16
+ onSelect: (value: MatrixRowAnswers) => void;
17
+ };
18
+
19
+ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: SliderMatrixScaleProps) {
20
+ const {
21
+ statementRows,
22
+ scaleAnchorLabels: labels,
23
+ ticks,
24
+ enableInputBox,
25
+ displayValues,
26
+ hasNotApplicableOption: enableNotApplicable,
27
+ sliderStyle,
28
+ } = question;
29
+
30
+ const tickCount = ticks.count;
31
+ const tickValues = ticks.marks.map((mark, index) => ({
32
+ value: mark.tickPosition,
33
+ label: mark.tickLabel,
34
+ color: tickColorFromIndex(index, tickCount),
35
+ }));
36
+ const sliderType = sliderStyle === 'graphics' ? 'graphics' : undefined;
37
+
38
+ const handleSliderChange = (rowId: string, val: number | 'N/A') => {
39
+ onSelect({ ...selectedValue, [rowId]: val });
40
+ };
41
+
42
+ const baseRow = statementRows[0];
43
+ const min = baseRow?.min ?? 0;
44
+ const max = baseRow?.max ?? 10;
45
+ const step = baseRow?.step ?? 1;
46
+
47
+ const marks: number[] = [];
48
+ if (tickCount && tickCount > 1) {
49
+ const markStep = (max - min) / (tickCount - 1);
50
+ for (let i = 0; i < tickCount; i++) {
51
+ marks.push(Number((min + i * markStep).toFixed(2)));
52
+ }
53
+ } else {
54
+ for (let i = min; i <= max; i += step) {
55
+ marks.push(Number(i.toFixed(2)));
56
+ }
57
+ }
58
+
59
+ const htmlStep = tickCount && tickCount > 1 ? (max - min) / (tickCount - 1) : step;
60
+
61
+ const renderHeaderRow = () => {
62
+ if ((!labels || labels.length === 0) && tickValues.length === 0) {
63
+ return null;
64
+ }
65
+ return (
66
+ <div style={{
67
+ display: 'flex', width: '100%', marginBottom: '4px', alignItems: 'flex-end',
68
+ padding: '0 16px', boxSizing: 'border-box',
69
+ }}>
70
+ <div style={{ flex: '0 0 25%', paddingRight: '16px' }} />
71
+
72
+ <div style={{ flex: '1', position: 'relative' }}>
73
+ {labels && labels.length > 0 && (
74
+ <div style={{
75
+ position: 'relative', display: 'flex', justifyContent: 'space-between',
76
+ margin: '0 16px', marginBottom: '16px', color: '#111827', fontSize: '14px', fontWeight: 500,
77
+ }}>
78
+ {labels.map((lbl, idx) => (
79
+ <div key={idx} style={{ width: 0, display: 'flex', justifyContent: 'center' }}>
80
+ <span style={{ whiteSpace: 'nowrap' }}>{lbl}</span>
81
+ </div>
82
+ ))}
83
+ </div>
84
+ )}
85
+
86
+ <div style={{
87
+ position: 'relative', margin: '0 16px', marginBottom: '8px',
88
+ color: '#4b5563', fontSize: '14px', fontWeight: 400, height: '28px',
89
+ }}>
90
+ {tickValues.length > 0 ? (
91
+ tickValues.map((tv, idx) => {
92
+ const percentage = max !== min ? ((tv.value - min) / (max - min)) * 100 : 0;
93
+ const sliceWidth = tickValues.length > 1 ? 100 / (tickValues.length - 1) : 100;
94
+ return (
95
+ <div key={idx} style={{
96
+ position: 'absolute', left: `${percentage}%`, transform: 'translateX(-50%)',
97
+ width: `${sliceWidth}%`, display: 'flex', justifyContent: 'center',
98
+ }}>
99
+ {sliderType === 'graphics' ? (
100
+ <div style={{ wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2', fontWeight: 500, color: '#4b5563', fontSize: '13px' }}>
101
+ {tv.label}
102
+ </div>
103
+ ) : (
104
+ <div style={{
105
+ backgroundColor: tv.color, color: 'white', padding: '4px 8px', borderRadius: '4px',
106
+ fontWeight: 600, wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2',
107
+ boxShadow: '0 1px 2px rgba(0,0,0,0.1)',
108
+ }}>
109
+ {tv.label}
110
+ </div>
111
+ )}
112
+ </div>
113
+ );
114
+ })
115
+ ) : (
116
+ marks.map((m, idx) => {
117
+ const percentage = max !== min ? ((m - min) / (max - min)) * 100 : 0;
118
+ const sliceWidth = marks.length > 1 ? 100 / (marks.length - 1) : 100;
119
+ return (
120
+ <div key={idx} style={{
121
+ position: 'absolute', left: `${percentage}%`, transform: 'translateX(-50%)',
122
+ width: `${sliceWidth}%`, display: 'flex', justifyContent: 'center',
123
+ }}>
124
+ <span style={{ wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2' }}>{m}</span>
125
+ </div>
126
+ );
127
+ })
128
+ )}
129
+ </div>
130
+ </div>
131
+
132
+ {enableInputBox && <div style={{ width: '80px', marginLeft: '16px' }} />}
133
+ {enableNotApplicable && (
134
+ <div style={{ width: '40px', marginLeft: '16px', textAlign: 'center', fontSize: '13px', fontWeight: 600, color: '#4b5563', paddingBottom: '8px' }}>
135
+ N/A
136
+ </div>
137
+ )}
138
+ </div>
139
+ );
140
+ };
141
+
142
+ return (
143
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
144
+ {renderHeaderRow()}
145
+
146
+ {statementRows.map(row => {
147
+ const rowVal = selectedValue[row.id];
148
+ const isNa = rowVal === 'N/A';
149
+ const val = typeof rowVal === 'number' ? rowVal : isNa ? row.min : (row.defaultValue ?? row.min);
150
+
151
+ return (
152
+ <div key={row.id} style={{
153
+ display: 'flex', width: '100%', alignItems: 'center',
154
+ backgroundColor: '#f3f4f6', borderRadius: '12px', padding: '16px', boxSizing: 'border-box',
155
+ }}>
156
+ <div style={{ flex: '0 0 25%', paddingRight: '16px', fontSize: '15px', color: '#111827', wordWrap: 'break-word', display: 'flex', alignItems: 'center' }}>
157
+ {row.statementText !== 'Question' && row.statementText !== '' ? (
158
+ <span dangerouslySetInnerHTML={{ __html: row.statementText }} />
159
+ ) : null}
160
+ </div>
161
+
162
+ <div style={{ flex: '1' }}>
163
+ <CustomSliderTrack
164
+ min={row.min}
165
+ max={row.max}
166
+ htmlStep={htmlStep}
167
+ value={val}
168
+ disabled={isNa}
169
+ displayValues={displayValues}
170
+ hasSelectedValue={typeof rowVal === 'number'}
171
+ sliderType={sliderType}
172
+ ticks={tickCount}
173
+ onChange={v => handleSliderChange(row.id, v)}
174
+ />
175
+ </div>
176
+
177
+ {enableInputBox && (
178
+ <div style={{
179
+ marginLeft: '16px', width: '80px', display: 'flex', alignItems: 'stretch',
180
+ border: '1px solid #d1d5db', borderRadius: '8px', overflow: 'hidden',
181
+ backgroundColor: isNa ? '#e5e7eb' : '#fff', boxSizing: 'border-box',
182
+ }}>
183
+ <input
184
+ type="text"
185
+ value={isNa ? '' : Math.round(Number(val))}
186
+ disabled={isNa}
187
+ onChange={e => {
188
+ if (e.target.value === '') return;
189
+ let num = Number(e.target.value);
190
+ if (!isNaN(num)) {
191
+ if (num < row.min) num = row.min;
192
+ if (num > row.max) num = row.max;
193
+ handleSliderChange(row.id, num);
194
+ }
195
+ }}
196
+ style={{
197
+ width: '50px', padding: '8px', border: 'none',
198
+ fontSize: '14px', outline: 'none', backgroundColor: 'transparent', textAlign: 'center',
199
+ }}
200
+ />
201
+ <div style={{ display: 'flex', flexDirection: 'column', borderLeft: '1px solid #d1d5db', backgroundColor: '#f9fafb', width: '30px' }}>
202
+ <button
203
+ disabled={isNa}
204
+ onClick={() => {
205
+ const num = Number((Number(val) + htmlStep).toFixed(2));
206
+ if (num <= row.max) handleSliderChange(row.id, num);
207
+ }}
208
+ style={{ flex: 1, border: 'none', background: 'none', cursor: isNa ? 'not-allowed' : 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#4b5563' }}
209
+ >
210
+ <svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 5L5 1L9 5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /></svg>
211
+ </button>
212
+ <div style={{ height: '1px', backgroundColor: '#d1d5db', width: '100%' }} />
213
+ <button
214
+ disabled={isNa}
215
+ onClick={() => {
216
+ const num = Number((Number(val) - htmlStep).toFixed(2));
217
+ if (num >= row.min) handleSliderChange(row.id, num);
218
+ }}
219
+ style={{ flex: 1, border: 'none', background: 'none', cursor: isNa ? 'not-allowed' : 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#4b5563' }}
220
+ >
221
+ <svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 1L5 5L9 1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /></svg>
222
+ </button>
223
+ </div>
224
+ </div>
225
+ )}
226
+
227
+ {enableNotApplicable && (
228
+ <div style={{ marginLeft: '16px', width: '40px', display: 'flex', justifyContent: 'center' }}>
229
+ <input
230
+ type="checkbox"
231
+ checked={isNa}
232
+ onChange={e => {
233
+ if (e.target.checked) {
234
+ handleSliderChange(row.id, 'N/A');
235
+ } else {
236
+ handleSliderChange(row.id, row.defaultValue ?? row.min);
237
+ }
238
+ }}
239
+ style={{ width: '20px', height: '20px', cursor: 'pointer' }}
240
+ />
241
+ </div>
242
+ )}
243
+ </div>
244
+ );
245
+ })}
246
+ </div>
247
+ );
248
+ }
@@ -0,0 +1,24 @@
1
+ import Header from '@/components/Header';
2
+ import ProgressBar from '@/components/ProgressBar';
3
+
4
+ type SurveyStickyChromeProps = {
5
+ progressPercentage: number;
6
+ };
7
+
8
+ /**
9
+ * Sticky top chrome: brand header + progress bar.
10
+ * Keeps progress visible while scrolling long pages (e.g. last question below the fold).
11
+ */
12
+ export default function SurveyStickyChrome({ progressPercentage }: SurveyStickyChromeProps) {
13
+ return (
14
+ <div
15
+ className="sticky top-0 z-30 shadow-[0_2px_8px_rgba(0,0,0,0.06)]"
16
+ style={{ background: 'var(--cfm-header-bg, #fff)' }}
17
+ >
18
+ <Header embedded />
19
+ <div className="mx-auto w-full max-w-4xl px-4 pb-5 pt-4 sm:px-6 lg:px-8">
20
+ <ProgressBar progressPercentage={progressPercentage} />
21
+ </div>
22
+ </div>
23
+ );
24
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Portable custom field values template — copy to src/lib/customFieldValues.ts
3
+ *
4
+ * Populate keys from survey logic rules where conditionEntity.entityType === 'CUSTOM_FIELD'.
5
+ * Values come from CRM/transaction context — NOT from survey respondent UI.
6
+ *
7
+ * See: docs/00-integration/custom-field-logic-and-navigation.md
8
+ * docs/00-integration/client-lib-folder.md
9
+ */
10
+
11
+ import type { CustomFieldValueMap } from '@explorer02/cfm-survey-sdk';
12
+
13
+ /**
14
+ * fieldType → client value shape:
15
+ *
16
+ * | fieldType | Value shape | Example |
17
+ * |------------------------|--------------------------------------|---------|
18
+ * | TEXT / TEXT_MULTI | string | 'abc' |
19
+ * | TEXTAREA | string | 'long text' |
20
+ * | NUMBER | number or numeric string | 2 or '0' |
21
+ * | PICKLIST | string (option id) | 'uuid-option-id' |
22
+ * | PICKLIST_MULTISELECT | string[] (option ids) | ['id-a', 'id-b'] |
23
+ * | DATETIME | epoch ms (number or numeric string) | 1781029800000 |
24
+ *
25
+ * Missing keys are treated as empty for logic evaluation.
26
+ */
27
+ export const CUSTOM_FIELD_VALUES: CustomFieldValueMap = {
28
+ // Example — skip SLIDER page when CASE_SENSITIVE_TEXT equals 'abc' (integration-test survey):
29
+ // '_c_67199096d294be011a2b4773': 'abc',
30
+
31
+ // '_c_6a436c1244764c9d4a6ce2fc': 'var2', // TEXT
32
+ // '_c_6a436c3644764c9d4a6ce3f5': 2, // NUMBER
33
+ // '_c_6a436c9e44764c9d4a6ce717': 'option-id-uuid', // PICKLIST
34
+ // '_c_6a436cbb44764c9d4a6ce7e5': ['option-id'], // PICKLIST_MULTISELECT
35
+ // '_c_6a4386dc44208eb8b08a9056': 1781029800000, // DATETIME epoch ms
36
+ };
@@ -1,13 +1,15 @@
1
1
  # Survey UI Implementation Plan
2
2
 
3
3
  > Copy this file to `./implementation_plan.md` in the project root. Fill every section before writing React code.
4
+ >
5
+ > Pipeline: [`agent-operating-contract.md`](../00-integration/agent-operating-contract.md) → [`agent-execution-flow.md`](../00-integration/agent-execution-flow.md)
4
6
 
5
7
  ## 0. Project Context
6
8
 
7
9
  - **Framework:** [ ] Next.js App Router [ ] Vite [ ] CRA [ ] Existing app
8
10
  - **Package import name:** (from `package.json` — `@explorer02/cfm-survey-sdk` or `@repo/sdk`)
9
11
  - **instanceId:** (JWT — wired in SurveyPage)
10
- - **Branding source:** (client prompt / mockup description no wizard)
12
+ - **Branding source:** (client prompt / mockup description; optional wizard override via `survey-ui-config.json`)
11
13
 
12
14
  ## 1. Data Fetching & State
13
15
 
@@ -26,6 +28,7 @@
26
28
  - [ ] `onAction` START / NEXT / PREVIOUS / SUBMIT wiring
27
29
  - [ ] `onAction(SCROLL)` once on first scroll (mark as started)
28
30
  - [ ] `onAction(RECORD_QUESTION_VIEW)` via IntersectionObserver on question wrappers
31
+ - [ ] `SurveyStickyChrome` (sticky Header + ProgressBar) on content phase — see `19-survey-sticky-chrome.md`
29
32
  - [ ] `ProgressBar` bound to `state.progressPercentage` (path-scoped — see `00-integration/progress.md`)
30
33
  - [ ] Back via `state.canGoBack`; primary label via `state.primaryButtonType`
31
34
  - [ ] Read `00-integration/skip-logic-and-navigation.md` for skip/Back behavior
@@ -91,7 +94,34 @@ Fill from `00-integration/component-checklist.md`:
91
94
  | `RankOrderScale.tsx` | `RANK_ORDER` | |
92
95
  | `lib/heatmapCoords.ts` | `HEATMAP` | copy from `templates/heatmapCoords.ts` |
93
96
 
94
- ## 3c. Logic inventory (from fetched survey)
97
+ | `lib/surveyUiScaleUtils.ts` | NPS/matrix colors | copy from `templates/surveyUiScaleUtils.ts` |
98
+ | `lib/customFieldValues.ts` | CRM logic input | copy from `templates/customFieldValues.ts` when survey has `_c_…` ids |
99
+ | `lib/surveyUiScaleUtils.ts` | NPS/tick/matrix helpers | copy from `templates/surveyUiScaleUtils.ts` |
100
+ | `lib/surveyUiIcons.tsx` | CSAT emoji/star | copy from `templates/surveyUiIcons.tsx` |
101
+
102
+ ## 3c. Matrix variant inventory (from fetched survey)
103
+
104
+ Read `00-integration/canonical-survey-fixtures.md`. For each matrix question note:
105
+
106
+ | questionId | type | displayStyle | statementLayout | transposeTable | selectionMode | Labels visible? |
107
+ |------------|------|--------------|-----------------|----------------|---------------|---------------|
108
+ | | | | | | | row + column/anchor |
109
+
110
+ ## 3d. Custom field inventory (mandatory when `_c_…` ids found)
111
+
112
+ After fetch, scan `skipLogics`, `displayLogic`, `answerLogic`, and `endPages` for `entityType: 'CUSTOM_FIELD'`.
113
+
114
+ | `_c_` id | fieldType | used in (skip/display/answer/end) | logic field (e.g. CASE_SENSITIVE_TEXT) | CRM value | populated in `lib/customFieldValues.ts`? |
115
+ |----------|-----------|-----------------------------------|----------------------------------------|-----------|---------------------------------------------|
116
+ | | | | | | |
117
+
118
+ **Blocking gate:** if any row above is filled → copy `templates/customFieldValues.ts` → `src/lib/customFieldValues.ts` before Phase 5 BUILD.
119
+
120
+ Wire map in **both** places: `useSurveySDK({ options: { customFieldValues } })` and `<Question customFieldValues={...} />`.
121
+
122
+ See `00-integration/custom-field-logic-and-navigation.md` and `00-integration/client-lib-folder.md`.
123
+
124
+ ## 3e. Logic inventory (from fetched survey)
95
125
 
96
126
  Scan `survey.pages` for logic flags. Fill after fetch:
97
127
 
@@ -99,9 +129,7 @@ Scan `survey.pages` for logic flags. Fill after fetch:
99
129
  |------------|------------|--------------|----------------------------------|---------------------------|
100
130
  | | | | | |
101
131
 
102
- Custom field ids found populate `CUSTOM_FIELD_VALUES` map. See `00-integration/custom-field-logic-and-navigation.md`.
103
-
104
- ## 3d. Per-Type SDK Integration (from fetched survey)
132
+ ## 3f. Per-Type SDK Integration (from fetched survey)
105
133
 
106
134
  Fill from `00-integration/question-type-sdk-matrix.md`:
107
135
 
@@ -119,18 +147,57 @@ Fill from `00-integration/question-type-sdk-matrix.md`:
119
147
  | LikertMatrixScale | | `06-likert-matrix-scale.md` |
120
148
  | SliderMatrixScale | | `07-slider-matrix-scale.md` |
121
149
  | FileUploadScale | | `08-file-upload-scale.md` |
122
- | Header / Footer / ProgressBar / LanguageSelector | | `10`–`12` |
150
+ | Header / Footer / SurveyStickyChrome / LanguageSelector | | `10`–`12`, `19` |
151
+
152
+ ## 3e. Interaction dependencies (from survey inventory)
153
+
154
+ | Package | Install when |
155
+ |---------|--------------|
156
+ | `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities` | Survey contains `RANK_ORDER` |
157
+ | `react-icons` | Always (shared setup) + CSAT emoji/star matrix |
158
+
159
+ Copy portable templates from `docs/templates/` — see `component-checklist.md`.
123
160
 
124
161
  ## 4b. Per-Question Config & UI Coverage
125
162
 
163
+ Inventory required config fields per type:
164
+
165
+ | type | must inventory |
166
+ |------|----------------|
167
+ | MCQ | `selectionMode`, `minSelections`, `maxSelections`, `defaultOptionIds`, answer-logic on options |
168
+ | RANK_ORDER | `interactionMode`, `optionDisplay`, `requireRankAll`, `shuffleOptions` |
169
+ | CFM/CSAT/RATING | `gridLayout`, `displayStyle`, `selectionMode`, `hasNotApplicableOption`, `statementLayout` |
170
+ | SLIDER_MATRIX | `enableInputBox`, `hasNotApplicableOption`, `sliderStyle`, `statementRows`, `ticks` |
171
+ | FILE_UPLOAD | `maxFileCount`, `fileSizeLimit`, `fileSizeLimitType`, upload API configured? |
172
+ | HEATMAP | `maxClicksAllowed`, image URL |
173
+
126
174
  | questionId | type | configs detected | uiSpec read | component files | branches implemented |
127
175
  |------------|------|------------------|-------------|-----------------|---------------------|
128
176
  | | | | | | |
129
177
 
130
178
  ## 5. Theme, Brand & Logo
131
179
 
132
- - [ ] CSS-only logo (no `<img>`, no `public/` uploads)
180
+ - [ ] Logo: text/CSS (`data-cfm-logo-text`) **or** local `public/` image (`data-cfm-logo`) — see `wizard-preview-build-guide.md`; no external URLs
133
181
  - [ ] Primary / hover colors from client prompt
182
+ - [ ] `src/styles/survey-theme.css` with `--cfm-*` CSS variables (full template set)
183
+ - [ ] **Wizard preview contract:** `var(--cfm-*)` in chrome/scales + `data-cfm-*` on editable text/logo — see `00-integration/wizard-preview-build-guide.md`
184
+ - [ ] **Wizard per-type styling:** `selectionStyles.ts`, `labelStyles.ts`, per-type tokens — see `00-integration/wizard-question-type-styling.md`
185
+ - [ ] **Draft** `./survey-ui-config.json` in sync with theme tokens (seeds optional wizard in Phase 6b)
186
+ - [ ] `previewBridge.ts` wired for wizard live preview (see `ui-customization-wizard.md`)
187
+ - [ ] If `survey-ui-config.json` exists: apply per `apply-ui-config.md` (Phase 5 draft or Phase 6c final)
188
+
189
+ ## 5b. Wizard handoff (Phase 6b–6c — when client uses wizard)
190
+
191
+ See `wizard-config-handoff.md`.
192
+
193
+ - [ ] `customize-ui` uploaded component previews to S3
194
+ - [ ] Deployed wizard link sent to client: `http://43.204.26.213:3000/wizard-app/index.html?session=…&secret=…` (from CLI output)
195
+ - [ ] CLI `customize-ui` exited 0 — waited for client submit
196
+ - [ ] Read `survey-ui-config.diff.md` — paths changed: ___
197
+ - [ ] Applied from `survey-ui-config.final.json` (AWS) or `survey-ui-config.json` (local)
198
+ - [ ] Every diff path applied (count matches)
199
+ - [ ] `confirm-ui-config` run (AWS)
200
+ - [ ] Verify + build re-run after apply
134
201
 
135
202
  ## 6. Submit & Thank You
136
203
 
@@ -145,7 +212,7 @@ Fill from `00-integration/question-type-sdk-matrix.md`:
145
212
  - [ ] `bash templates/verify-agent-build.sh` passes
146
213
  - [ ] `grep -r "not yet implemented"` → zero matches
147
214
 
148
- ## 8. AWS Deploy Readiness (fill if client prompt mentions AWS / CloudFront / S3 / EC2 / Sprinklr)
215
+ ## 8. AWS Deploy Readiness (fill if client prompt mentions AWS / CloudFront / S3 / EC2 / Sprinklr, or wizard deploy.target is aws)
149
216
 
150
217
  - [ ] Next.js `output: "export"` + `assetPrefix: "./"` configured (see `setup.md#nextjs-aws-static-export`)
151
218
  - [ ] `images.unoptimized: true` in `next.config.js`
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Label item styling for matrix / slider tick labels (wizard Question Types step).
3
+ * Copy to `src/lib/surveyUi/labelStyles.ts`.
4
+ */
5
+ import type { CSSProperties } from 'react';
6
+
7
+ export function hintLabelStyle(): CSSProperties {
8
+ return {
9
+ color: 'var(--cfm-hint-label-color, #6b7280)',
10
+ backgroundColor: 'var(--cfm-hint-label-bg, transparent)',
11
+ };
12
+ }
13
+
14
+ export function matrixColumnLabelStyle(): CSSProperties {
15
+ return {
16
+ color: 'var(--cfm-matrix-column-label-color, #4b5563)',
17
+ backgroundColor: 'var(--cfm-matrix-column-label-bg, transparent)',
18
+ };
19
+ }
20
+
21
+ export function csatColumnLabelStyle(): CSSProperties {
22
+ return {
23
+ color: 'var(--cfm-csat-column-label-color, #4b5563)',
24
+ backgroundColor: 'var(--cfm-csat-column-label-bg, transparent)',
25
+ };
26
+ }
27
+
28
+ export function sliderTickLabelStyle(): CSSProperties {
29
+ return {
30
+ color: 'var(--cfm-slider-tick-label-color, #4b5563)',
31
+ backgroundColor: 'var(--cfm-slider-tick-label-bg, transparent)',
32
+ };
33
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Portable selection styling helpers for wizard live preview + apply-ui-config.
3
+ * Copy to `src/lib/surveyUi/selectionStyles.ts` when building agent projects.
4
+ *
5
+ * Question card theme tokens (wizard → Theme & Colors → Question card):
6
+ * --cfm-matrix-selected = cell selected color (annulus fill between ring and dot)
7
+ * --cfm-input-focus-ring = focus ring (outer border + inner dot)
8
+ */
9
+ import type { CSSProperties } from 'react';
10
+
11
+ export const cellSelectedVar = 'var(--cfm-matrix-selected, var(--cfm-primary))';
12
+ export const focusRingVar = 'var(--cfm-input-focus-ring, var(--cfm-primary))';
13
+
14
+ const unselectedRing = '#d1d5db';
15
+ const unselectedDot = '#d1d5db';
16
+
17
+ /** NPS / large scale radio — outer ring border + annulus fill. */
18
+ export function scaleRadioRingStyle(
19
+ isSelected: boolean,
20
+ opts: { pill?: boolean; size?: string } = {},
21
+ ): CSSProperties {
22
+ const { pill = false, size = 'var(--cfm-nps-cell-size, 32px)' } = opts;
23
+ return {
24
+ display: 'flex',
25
+ alignItems: 'center',
26
+ justifyContent: 'center',
27
+ width: size,
28
+ height: size,
29
+ borderRadius: pill ? '9999px' : '50%',
30
+ border: `2px solid ${isSelected ? focusRingVar : unselectedRing}`,
31
+ backgroundColor: isSelected ? cellSelectedVar : '#fff',
32
+ transition: 'all 0.15s',
33
+ boxSizing: 'border-box',
34
+ };
35
+ }
36
+
37
+ /** Inner dot — focus ring color when selected. */
38
+ export function scaleRadioDotStyle(
39
+ isSelected: boolean,
40
+ opts: { pill?: boolean } = {},
41
+ ): CSSProperties {
42
+ const { pill = false } = opts;
43
+ return {
44
+ borderRadius: '50%',
45
+ width: pill ? '40%' : '33%',
46
+ height: pill ? '40%' : '33%',
47
+ backgroundColor: isSelected ? focusRingVar : unselectedDot,
48
+ };
49
+ }
50
+
51
+ /** Compact matrix radio (CSAT / Likert / CFM grid). */
52
+ export function matrixRadioRingStyle(isSelected: boolean, size = '18px'): CSSProperties {
53
+ return {
54
+ display: 'flex',
55
+ width: size,
56
+ height: size,
57
+ alignItems: 'center',
58
+ justifyContent: 'center',
59
+ borderRadius: '50%',
60
+ margin: '0 auto',
61
+ border: `2px solid ${isSelected ? focusRingVar : unselectedRing}`,
62
+ backgroundColor: isSelected ? cellSelectedVar : '#fff',
63
+ transition: 'all 0.15s',
64
+ boxSizing: 'border-box',
65
+ };
66
+ }
67
+
68
+ export function matrixRadioDotStyle(isSelected: boolean, size = '8px'): CSSProperties {
69
+ return {
70
+ width: size,
71
+ height: size,
72
+ borderRadius: '50%',
73
+ backgroundColor: isSelected ? focusRingVar : 'transparent',
74
+ };
75
+ }
76
+
77
+ /** Numbered / solid scale cell when selected. */
78
+ export function scaleCellSelectedStyle(isSelected: boolean): CSSProperties {
79
+ return isSelected
80
+ ? {
81
+ border: `2px solid ${focusRingVar}`,
82
+ backgroundColor: cellSelectedVar,
83
+ color: '#fff',
84
+ }
85
+ : {};
86
+ }
87
+
88
+ export function unselectedOpacityStyle(): CSSProperties {
89
+ return { opacity: 'var(--cfm-csat-unselected-opacity, 0.5)' };
90
+ }
91
+
92
+ export function emojiSizeStyle(): CSSProperties {
93
+ return {
94
+ fontSize: 'var(--cfm-csat-emoji-size, 28px)',
95
+ lineHeight: 1,
96
+ display: 'inline-flex',
97
+ alignItems: 'center',
98
+ justifyContent: 'center',
99
+ };
100
+ }
@@ -7,22 +7,27 @@
7
7
  "required": ["questions", "uniqueTypes"],
8
8
  "properties": {
9
9
  "instanceId": { "type": "string", "description": "JWT used to fetch survey" },
10
+ "interactionDependencies": {
11
+ "type": "array",
12
+ "items": { "type": "string" },
13
+ "description": "npm packages e.g. @dnd-kit/core, react-icons"
14
+ },
10
15
  "uniqueTypes": {
11
16
  "type": "array",
12
17
  "items": {
13
18
  "type": "string",
14
19
  "enum": [
15
- "nps",
16
- "mcq",
17
- "text_input",
18
- "csat",
19
- "star_rating",
20
- "slider",
21
- "cfm_matrix",
22
- "rating_matrix",
23
- "slider_matrix",
24
- "file_upload",
25
- "text_and_media"
20
+ "MCQ",
21
+ "TEXTFIELD",
22
+ "NPS_SCALE",
23
+ "CFM_MATRIX",
24
+ "CSAT_MATRIX",
25
+ "RATING_MATRIX",
26
+ "SLIDER_MATRIX",
27
+ "FILE_UPLOAD",
28
+ "TEXT_AND_MEDIA",
29
+ "HEATMAP",
30
+ "RANK_ORDER"
26
31
  ]
27
32
  },
28
33
  "uniqueItems": true
@@ -37,23 +42,23 @@
37
42
  "type": {
38
43
  "type": "string",
39
44
  "enum": [
40
- "nps",
41
- "mcq",
42
- "text_input",
43
- "csat",
44
- "star_rating",
45
- "slider",
46
- "cfm_matrix",
47
- "rating_matrix",
48
- "slider_matrix",
49
- "file_upload",
50
- "text_and_media"
45
+ "MCQ",
46
+ "TEXTFIELD",
47
+ "NPS_SCALE",
48
+ "CFM_MATRIX",
49
+ "CSAT_MATRIX",
50
+ "RATING_MATRIX",
51
+ "SLIDER_MATRIX",
52
+ "FILE_UPLOAD",
53
+ "TEXT_AND_MEDIA",
54
+ "HEATMAP",
55
+ "RANK_ORDER"
51
56
  ]
52
57
  },
53
58
  "configFlags": {
54
59
  "type": "array",
55
60
  "items": { "type": "string" },
56
- "description": "Non-default config, e.g. gridLayout:carousel"
61
+ "description": "e.g. interactionMode:dragAndDrop, gridLayout:carousel, selectionMode:multiple, defaultOptionIds"
57
62
  },
58
63
  "uiSpecRead": { "type": "string" },
59
64
  "componentFiles": {