@explorer02/cfm-survey-sdk 0.2.3 → 0.2.5

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 (100) hide show
  1. package/dist/cli/index.js +141 -33
  2. package/dist/cli/index.mjs +141 -33
  3. package/dist/index.js +1 -1
  4. package/dist/index.mjs +1 -1
  5. package/package.json +3 -1
  6. package/postinstall.js +9 -5
  7. package/templates/AGENT.md +60 -19
  8. package/templates/deploy-wizard-to-s3.sh +52 -0
  9. package/templates/docs/00-integration/agent-execution-flow.md +293 -0
  10. package/templates/docs/00-integration/agent-operating-contract.md +268 -0
  11. package/templates/docs/00-integration/apply-ui-config.md +390 -0
  12. package/templates/docs/00-integration/aws-deploy.md +117 -3
  13. package/templates/docs/00-integration/canonical-survey-fixtures.md +94 -0
  14. package/templates/docs/00-integration/client-integration-guide.md +9 -7
  15. package/templates/docs/00-integration/client-lib-folder.md +115 -0
  16. package/templates/docs/00-integration/component-checklist.md +90 -62
  17. package/templates/docs/00-integration/constraints.md +14 -5
  18. package/templates/docs/00-integration/custom-field-logic-and-navigation.md +56 -0
  19. package/templates/docs/00-integration/mockup-ui-extraction.md +113 -0
  20. package/templates/docs/00-integration/placeholders-and-custom-fields-wiring.md +192 -0
  21. package/templates/docs/00-integration/placeholders-and-tokens.md +2 -1
  22. package/templates/docs/00-integration/progress.md +4 -2
  23. package/templates/docs/00-integration/setup.md +12 -0
  24. package/templates/docs/00-integration/ui-customization-wizard.md +123 -0
  25. package/templates/docs/00-integration/useSurveySDK.md +35 -1
  26. package/templates/docs/00-integration/wizard-api.md +126 -0
  27. package/templates/docs/00-integration/wizard-config-handoff.md +292 -0
  28. package/templates/docs/00-integration/wizard-preview-build-guide.md +405 -0
  29. package/templates/docs/00-integration/wizard-question-type-styling.md +197 -0
  30. package/templates/docs/00-integration/wizard-troubleshooting.md +80 -0
  31. package/templates/docs/01-components/01-survey-page.md +18 -8
  32. package/templates/docs/01-components/02-question.md +2 -2
  33. package/templates/docs/01-components/03-rating-scale.md +12 -18
  34. package/templates/docs/01-components/05-csat-matrix-scale.md +18 -26
  35. package/templates/docs/01-components/06-likert-matrix-scale.md +19 -39
  36. package/templates/docs/01-components/07-slider-matrix-scale.md +48 -33
  37. package/templates/docs/01-components/09-custom-slider-track.md +15 -25
  38. package/templates/docs/01-components/10-header-footer.md +23 -4
  39. package/templates/docs/01-components/11-progress-bar.md +14 -6
  40. package/templates/docs/01-components/19-survey-sticky-chrome.md +79 -0
  41. package/templates/docs/01-components/README.md +2 -2
  42. package/templates/docs/02-reference/question-types/10-slider-matrix.md +38 -26
  43. package/templates/docs/02-reference/routing-table.md +1 -0
  44. package/templates/docs/02-reference/value-derivation.md +1 -1
  45. package/templates/docs/03-ui-specs/00-question-shell.md +10 -0
  46. package/templates/docs/03-ui-specs/01-rating.md +18 -0
  47. package/templates/docs/03-ui-specs/02-radio.md +16 -1
  48. package/templates/docs/03-ui-specs/03-text.md +12 -1
  49. package/templates/docs/03-ui-specs/04-csat.md +84 -50
  50. package/templates/docs/03-ui-specs/06-slider.md +3 -1
  51. package/templates/docs/03-ui-specs/07-matrix-cfm.md +71 -48
  52. package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +48 -18
  53. package/templates/docs/03-ui-specs/09-slider-matrix.md +59 -32
  54. package/templates/docs/03-ui-specs/10-file-upload.md +11 -0
  55. package/templates/docs/03-ui-specs/11-text-and-media.md +4 -0
  56. package/templates/docs/03-ui-specs/12-survey-chrome.md +54 -5
  57. package/templates/docs/03-ui-specs/13-heatmap.md +12 -1
  58. package/templates/docs/03-ui-specs/14-rank-order.md +11 -0
  59. package/templates/docs/03-ui-specs/README.md +4 -2
  60. package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +1 -1
  61. package/templates/docs/MANIFEST.json +180 -7
  62. package/templates/docs/index.md +114 -117
  63. package/templates/docs/templates/CsatMatrixScale.tsx +637 -0
  64. package/templates/docs/templates/FileUploadScale.tsx +262 -0
  65. package/templates/docs/templates/HeatmapScale.tsx +114 -0
  66. package/templates/docs/templates/LikertMatrixScale.tsx +414 -0
  67. package/templates/docs/templates/Question.tsx +3 -0
  68. package/templates/docs/templates/RatingScale.tsx +129 -0
  69. package/templates/docs/templates/SliderMatrixScale.tsx +248 -0
  70. package/templates/docs/templates/SurveyStickyChrome.tsx +24 -0
  71. package/templates/docs/templates/customFieldValues.ts +36 -0
  72. package/templates/docs/templates/deploy-to-aws.sh +74 -0
  73. package/templates/docs/templates/implementation_plan.md +64 -10
  74. package/templates/docs/templates/labelStyles.ts +33 -0
  75. package/templates/docs/templates/selectionStyles.ts +100 -0
  76. package/templates/docs/templates/surveyUiIcons.tsx +11 -0
  77. package/templates/docs/templates/surveyUiScaleUtils.ts +51 -0
  78. package/templates/docs/templates/verify-agent-build.sh +119 -0
  79. package/templates/preview-harness/preview-bridge.inline.js +201 -0
  80. package/templates/preview-harness/previewPages.js +108 -0
  81. package/templates/preview-harness/previewPages.ts +298 -0
  82. package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +67 -0
  83. package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +122 -0
  84. package/templates/preview-harness/vite-app/src/SurveyPagePreview.tsx +75 -0
  85. package/templates/preview-harness/vite-app/src/fixtures/questions.ts +311 -0
  86. package/templates/preview-harness/vite-app/src/globals.css +16 -0
  87. package/templates/preview-harness/vite-app/src/mount.tsx +9 -0
  88. package/templates/preview-harness/vite-app/src/preview-live-overrides.css +101 -0
  89. package/templates/preview-harness/vite-app/stubs/next-dynamic.tsx +25 -0
  90. package/templates/preview-harness/vite-app/stubs/next-image.tsx +29 -0
  91. package/templates/previewBridge.ts +153 -0
  92. package/templates/survey-theme.css +295 -0
  93. package/templates/survey-ui-config.schema.json +213 -0
  94. package/templates/wizard-dist/assets/PreviewMock-AoZdAEPZ.js +1 -0
  95. package/templates/wizard-dist/assets/TypePanel-39FnbtvG.js +1 -0
  96. package/templates/wizard-dist/assets/index-BhWM50Yu.css +1 -0
  97. package/templates/wizard-dist/assets/index-CWKdsJOp.js +34 -0
  98. package/templates/wizard-dist/assets/vendor-BwkXDkd3.js +17 -0
  99. package/templates/wizard-dist/index.html +20 -0
  100. 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
+ };
@@ -24,6 +24,55 @@ echo "API: $CFM_DEPLOY_API_BASE"
24
24
  echo "OUT: $OUT_DIR ($FILE_COUNT files)"
25
25
  echo
26
26
 
27
+ # Verify wizard logos and other public assets made it into the static build (not just browser memory).
28
+ python3 <<'PY' || exit 1
29
+ import json, os, re, sys
30
+ from pathlib import Path
31
+
32
+ out = Path(os.environ["OUT_DIR"])
33
+ root = Path(os.environ.get("PROJECT_ROOT", os.getcwd()))
34
+ config_path = root / "survey-ui-config.json"
35
+ missing = []
36
+
37
+ if config_path.is_file():
38
+ cfg = json.load(open(config_path))
39
+ logo = (cfg.get("global") or {}).get("logo") or {}
40
+ fn = logo.get("fileName")
41
+ url = logo.get("url") or ""
42
+ if fn:
43
+ if url.startswith("data:"):
44
+ print(f"FAIL: global.logo.url is a data: URL — apply Phase 6c: Header src=./{fn}, file in public/")
45
+ sys.exit(1)
46
+ for base in (root / "public", out):
47
+ if not (base / fn).is_file():
48
+ missing.append(str(base / fn))
49
+ if missing:
50
+ print("FAIL: logo file missing from build output:")
51
+ for p in missing:
52
+ print(f" - {p}")
53
+ print("Fix: run confirm-ui-config (EC2 wizard) or re-upload in customize; update Header data-cfm-logo src")
54
+ sys.exit(1)
55
+ print(f"Logo OK: {fn} in out/")
56
+ else:
57
+ print("No global.logo.fileName in survey-ui-config.json — skipping logo check")
58
+
59
+ for header in sorted((root / "src").rglob("Header.tsx")):
60
+ text = header.read_text()
61
+ for m in re.finditer(r'data-cfm-logo[^>]*\ssrc=["\']([^"\']+)["\']', text):
62
+ src = m.group(1)
63
+ if src.startswith(("http://", "https://", "data:")):
64
+ print(f"FAIL: {header} uses external/data logo src: {src[:60]}")
65
+ sys.exit(1)
66
+ rel = src.lstrip("./").lstrip("/")
67
+ if rel and not (out / rel).is_file():
68
+ missing.append(str(out / rel))
69
+ print(f"FAIL: Header references {src} but {out / rel} not in build")
70
+ sys.exit(1)
71
+
72
+ if not missing:
73
+ print("Static asset preflight OK")
74
+ PY
75
+
27
76
  HEALTH=$(curl -s "$CFM_DEPLOY_API_BASE/health")
28
77
  echo "Health: $HEALTH"
29
78
  echo
@@ -111,4 +160,29 @@ if [ "$HTTP" != "200" ]; then
111
160
  exit 1
112
161
  fi
113
162
 
163
+ # Optional: verify logo asset under same deploy prefix (broken img = missing from out/ or wrong Header src)
164
+ python3 <<'PY' || true
165
+ import json, os, re, subprocess
166
+ from pathlib import Path
167
+ from urllib.parse import urljoin
168
+
169
+ root = Path(os.environ.get("PROJECT_ROOT", os.getcwd()))
170
+ cfg_path = root / "survey-ui-config.json"
171
+ live = json.load(open("/tmp/deploy-complete.json"))["url"]
172
+ if not cfg_path.is_file():
173
+ raise SystemExit(0)
174
+ fn = ((json.load(open(cfg_path)).get("global") or {}).get("logo") or {}).get("fileName")
175
+ if not fn:
176
+ raise SystemExit(0)
177
+ base = live.rsplit("/", 1)[0] + "/"
178
+ logo_url = urljoin(base, fn)
179
+ code = subprocess.check_output(
180
+ ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", logo_url],
181
+ text=True,
182
+ ).strip()
183
+ print(f"Logo asset: {logo_url} → HTTP {code}")
184
+ if code != "200":
185
+ print("WARN: logo missing on CloudFront — rebuild after Phase 6c logo apply (see aws-deploy.md § Static assets)")
186
+ PY
187
+
114
188
  echo "=== Deploy complete ==="
@@ -1,13 +1,16 @@
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)
6
+ > Mockup rules: [`mockup-ui-extraction.md`](../00-integration/mockup-ui-extraction.md)
4
7
 
5
8
  ## 0. Project Context
6
9
 
7
10
  - **Framework:** [ ] Next.js App Router [ ] Vite [ ] CRA [ ] Existing app
8
11
  - **Package import name:** (from `package.json` — `@explorer02/cfm-survey-sdk` or `@repo/sdk`)
9
12
  - **instanceId:** (JWT — wired in SurveyPage)
10
- - **Branding source:** (client prompt / mockup description no wizard)
13
+ - **Branding source:** (client prompt / mockup — **UI only** per [`mockup-ui-extraction.md`](../00-integration/mockup-ui-extraction.md); optional wizard override via `survey-ui-config.json`)
11
14
 
12
15
  ## 1. Data Fetching & State
13
16
 
@@ -26,6 +29,7 @@
26
29
  - [ ] `onAction` START / NEXT / PREVIOUS / SUBMIT wiring
27
30
  - [ ] `onAction(SCROLL)` once on first scroll (mark as started)
28
31
  - [ ] `onAction(RECORD_QUESTION_VIEW)` via IntersectionObserver on question wrappers
32
+ - [ ] `SurveyStickyChrome` (sticky Header + ProgressBar) on content phase — see `19-survey-sticky-chrome.md`
29
33
  - [ ] `ProgressBar` bound to `state.progressPercentage` (path-scoped — see `00-integration/progress.md`)
30
34
  - [ ] Back via `state.canGoBack`; primary label via `state.primaryButtonType`
31
35
  - [ ] Read `00-integration/skip-logic-and-navigation.md` for skip/Back behavior
@@ -91,17 +95,51 @@ Fill from `00-integration/component-checklist.md`:
91
95
  | `RankOrderScale.tsx` | `RANK_ORDER` | |
92
96
  | `lib/heatmapCoords.ts` | `HEATMAP` | copy from `templates/heatmapCoords.ts` |
93
97
 
94
- ## 3c. Logic inventory (from fetched survey)
98
+ | `lib/surveyUiScaleUtils.ts` | NPS/matrix colors | copy from `templates/surveyUiScaleUtils.ts` |
99
+ | `lib/customFieldValues.ts` | CRM logic input | copy from `templates/customFieldValues.ts` when survey has `_c_…` ids |
100
+ | `lib/surveyUiScaleUtils.ts` | NPS/tick/matrix helpers | copy from `templates/surveyUiScaleUtils.ts` |
101
+ | `lib/surveyUiIcons.tsx` | CSAT emoji/star | copy from `templates/surveyUiIcons.tsx` |
95
102
 
96
- Scan `survey.pages` for logic flags. Fill after fetch:
103
+ ## 3c. Matrix variant inventory (from fetched survey)
104
+
105
+ Read `00-integration/canonical-survey-fixtures.md`. For each matrix question note:
106
+
107
+ | questionId | type | displayStyle | statementLayout | transposeTable | selectionMode | Labels visible? |
108
+ |------------|------|--------------|-----------------|----------------|---------------|---------------|
109
+ | | | | | | | row + column/anchor |
110
+
111
+ ## 3d. Custom field inventory (mandatory when `_c_…` ids found)
112
+
113
+ After fetch, scan `skipLogics`, `displayLogic`, `answerLogic`, and `endPages` for `entityType: 'CUSTOM_FIELD'`.
114
+ **Do not** infer ids from mockup — see [`placeholders-and-custom-fields-wiring.md`](../00-integration/placeholders-and-custom-fields-wiring.md).
115
+
116
+ | `_c_` id | fieldType | used in (skip/display/answer/end) | logic field (e.g. CASE_SENSITIVE_TEXT) | CRM value | populated in `lib/customFieldValues.ts`? |
117
+ |----------|-----------|-----------------------------------|----------------------------------------|-----------|---------------------------------------------|
118
+ | | | | | | |
119
+
120
+ **Blocking gate:** if any row above is filled → copy `templates/customFieldValues.ts` → `src/lib/customFieldValues.ts` before Phase 5 BUILD.
121
+
122
+ Wire map in **both** places: `useSurveySDK({ options: { customFieldValues } })` and `<Question customFieldValues={...} />`.
123
+
124
+ See `00-integration/custom-field-logic-and-navigation.md` and `00-integration/client-lib-folder.md`.
125
+
126
+ ## 3d-b. Placeholder inventory (from fetched question text)
127
+
128
+ Scan fetched survey for `{{TOKEN}}` and `{{SURVEY_QUESTION-:-id}}` — **not** from mockup. See [`placeholders-and-custom-fields-wiring.md`](../00-integration/placeholders-and-custom-fields-wiring.md).
129
+
130
+ | Token in fetch | Key in `SURVEY_PLACEHOLDERS` | Example value | Question ids |
131
+ |----------------|------------------------------|---------------|--------------|
132
+ | | | | |
133
+
134
+ ## 3e. Logic inventory (from fetched survey)
135
+
136
+ Scan `survey.pages` for logic flags. Fill after fetch — **not** from mockup:
97
137
 
98
138
  | questionId | skipLogics | displayLogic | answerLogic (options/rows/cols) | customField refs (`_c_…`) |
99
139
  |------------|------------|--------------|----------------------------------|---------------------------|
100
140
  | | | | | |
101
141
 
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)
142
+ ## 3f. Per-Type SDK Integration (from fetched survey)
105
143
 
106
144
  Fill from `00-integration/question-type-sdk-matrix.md`:
107
145
 
@@ -119,7 +157,7 @@ Fill from `00-integration/question-type-sdk-matrix.md`:
119
157
  | LikertMatrixScale | | `06-likert-matrix-scale.md` |
120
158
  | SliderMatrixScale | | `07-slider-matrix-scale.md` |
121
159
  | FileUploadScale | | `08-file-upload-scale.md` |
122
- | Header / Footer / ProgressBar / LanguageSelector | | `10`–`12` |
160
+ | Header / Footer / SurveyStickyChrome / LanguageSelector | | `10`–`12`, `19` |
123
161
 
124
162
  ## 3e. Interaction dependencies (from survey inventory)
125
163
 
@@ -139,7 +177,7 @@ Inventory required config fields per type:
139
177
  | MCQ | `selectionMode`, `minSelections`, `maxSelections`, `defaultOptionIds`, answer-logic on options |
140
178
  | RANK_ORDER | `interactionMode`, `optionDisplay`, `requireRankAll`, `shuffleOptions` |
141
179
  | CFM/CSAT/RATING | `gridLayout`, `displayStyle`, `selectionMode`, `hasNotApplicableOption`, `statementLayout` |
142
- | SLIDER_MATRIX | `enableInputBox`, `enableNotApplicable`, `sliderStyle` |
180
+ | SLIDER_MATRIX | `enableInputBox`, `hasNotApplicableOption`, `sliderStyle`, `statementRows`, `ticks` |
143
181
  | FILE_UPLOAD | `maxFileCount`, `fileSizeLimit`, `fileSizeLimitType`, upload API configured? |
144
182
  | HEATMAP | `maxClicksAllowed`, image URL |
145
183
 
@@ -149,8 +187,24 @@ Inventory required config fields per type:
149
187
 
150
188
  ## 5. Theme, Brand & Logo
151
189
 
152
- - [ ] CSS-only logo (no `<img>`, no `public/` uploads)
190
+ - [ ] Logo: text/CSS (`data-cfm-logo-text`) **or** local `public/` image (`data-cfm-logo`) — see `wizard-preview-build-guide.md`; no external URLs
153
191
  - [ ] Primary / hover colors from client prompt
192
+ - [ ] `src/styles/survey-theme.css` with `--cfm-*` CSS variables (full template set)
193
+ - [ ] **Wizard preview contract:** `var(--cfm-*)` in chrome/scales + `data-cfm-*` on editable text/logo — see `00-integration/wizard-preview-build-guide.md`
194
+ - [ ] **Wizard per-type styling:** `selectionStyles.ts`, `labelStyles.ts`, per-type tokens — see `00-integration/wizard-question-type-styling.md`
195
+ - [ ] **Wizard `surveyTypes`:** all 11 literals in seed (mockup may show NPS only — wizard still customizes all types)
196
+ - [ ] `previewBridge.ts` wired for wizard live preview (see `ui-customization-wizard.md`)
197
+ - [ ] If `survey-ui-config.json` exists: apply per `apply-ui-config.md` (Phase 5 draft or Phase 6c final)
198
+
199
+ ## 5b. Wizard handoff (Phase 6b–6c — when client uses wizard)
200
+
201
+ See `wizard-config-handoff.md`.
202
+
203
+ - [ ] `npx cfm-sdk customize` ran (default) and exited 0 — client saved on same machine
204
+ - [ ] OR `customize-ui` + EC2 link if client on remote device
205
+ - [ ] `survey-ui-config.json` written (or `.final.json` + `.diff.md` for EC2)
206
+ - [ ] Applied per `apply-ui-config.md`
207
+ - [ ] Verify + build re-run after apply
154
208
 
155
209
  ## 6. Submit & Thank You
156
210
 
@@ -165,7 +219,7 @@ Inventory required config fields per type:
165
219
  - [ ] `bash templates/verify-agent-build.sh` passes
166
220
  - [ ] `grep -r "not yet implemented"` → zero matches
167
221
 
168
- ## 8. AWS Deploy Readiness (fill if client prompt mentions AWS / CloudFront / S3 / EC2 / Sprinklr)
222
+ ## 8. AWS Deploy Readiness (fill if client prompt mentions AWS / CloudFront / S3 / EC2 / Sprinklr, or wizard deploy.target is aws)
169
223
 
170
224
  - [ ] Next.js `output: "export"` + `assetPrefix: "./"` configured (see `setup.md#nextjs-aws-static-export`)
171
225
  - [ ] `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
+ }