@explorer02/cfm-survey-sdk 0.2.8 → 0.2.9

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 (40) hide show
  1. package/dist/cli/index.js +38 -41
  2. package/dist/cli/index.mjs +36 -39
  3. package/package.json +1 -1
  4. package/templates/docs/00-integration/agent-execution-flow.md +6 -6
  5. package/templates/docs/00-integration/agent-operating-contract.md +2 -1
  6. package/templates/docs/00-integration/component-checklist.md +1 -0
  7. package/templates/docs/00-integration/ui-customization-wizard.md +29 -7
  8. package/templates/docs/00-integration/wizard-chrome-contract.md +11 -0
  9. package/templates/docs/00-integration/wizard-config-handoff.md +11 -9
  10. package/templates/docs/00-integration/wizard-post-customize-build.md +6 -0
  11. package/templates/docs/00-integration/wizard-question-type-styling.md +4 -3
  12. package/templates/docs/00-integration/wizard-troubleshooting.md +20 -4
  13. package/templates/docs/index.md +1 -1
  14. package/templates/docs/templates/CsatMatrixScale.tsx +56 -53
  15. package/templates/docs/templates/CustomSliderTrack.tsx +17 -8
  16. package/templates/docs/templates/FileUploadScale.tsx +11 -3
  17. package/templates/docs/templates/Header.tsx +32 -48
  18. package/templates/docs/templates/HeatmapScale.tsx +7 -4
  19. package/templates/docs/templates/LikertMatrixScale.tsx +58 -43
  20. package/templates/docs/templates/MatrixDropdown.tsx +12 -11
  21. package/templates/docs/templates/Question.tsx +4 -23
  22. package/templates/docs/templates/RankOrderScale.tsx +46 -6
  23. package/templates/docs/templates/RatingScale.tsx +38 -0
  24. package/templates/docs/templates/SliderMatrixScale.tsx +4 -3
  25. package/templates/docs/templates/verify-agent-build.sh +17 -0
  26. package/templates/preview-harness/preview-bridge.inline.js +10 -0
  27. package/templates/preview-harness/previewPages.ts +5 -7
  28. package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +11 -6
  29. package/templates/preview-harness/vite-app/src/fixtures/questions.ts +2 -10
  30. package/templates/preview-harness/vite-app/src/preview-live-overrides.css +11 -6
  31. package/templates/previewBridge.ts +13 -0
  32. package/templates/survey-theme.css +17 -17
  33. package/templates/wizard-dist/assets/{PreviewMock-tTMvwuEy.js → PreviewMock-CysRws9G.js} +1 -1
  34. package/templates/wizard-dist/assets/TypePanel-DWX1LL3q.js +1 -0
  35. package/templates/wizard-dist/assets/index-DIuc7AU2.js +34 -0
  36. package/templates/wizard-dist/assets/index-pCM8n0FT.css +1 -0
  37. package/templates/wizard-dist/index.html +2 -2
  38. package/templates/wizard-dist/assets/TypePanel-B9Ls7BSq.js +0 -1
  39. package/templates/wizard-dist/assets/index-CjkIlOUI.css +0 -1
  40. package/templates/wizard-dist/assets/index-CoCrnnxt.js +0 -34
@@ -30,16 +30,18 @@ npx cfm-sdk customize
30
30
  The CLI will:
31
31
 
32
32
  1. Export all 11 component preview pages (`export-previews` if needed)
33
- 2. Start wizard at `http://localhost:4200`
34
- 3. Open browser on the **agent machine**
35
- 4. **Block** until client clicks **Review & Build**
36
- 5. Write `survey-ui-config.json` and exit 0
33
+ 2. Probe first free port from 4200 and start wizard CLI prints **`WIZARD_LINK=http://localhost:{port}`**
34
+ 3. **Block** until client clicks **Review & Build** or quits
35
+ 4. On save: write `survey-ui-config.json` and exit 0
36
+ 5. On quit: GC `out/previews/` and exit 0 — **do not re-run wizard**; ask deploy (Phase 7)
37
37
 
38
38
  **Agent must:**
39
39
 
40
- - [ ] Client is on the **same machine** (can see `localhost:4200`)
41
- - [ ] Keep terminal open until `✅ UI configuration saved`
42
- - [ ] Apply per Phase 6c from `survey-ui-config.json`
40
+ - [ ] Parse `WIZARD_LINK` from CLI and share **only** that URL with client (never hardcode `:4200`)
41
+ - [ ] Client is on the **same machine** (can open the `WIZARD_LINK` host)
42
+ - [ ] Keep terminal open until CLI exits
43
+ - [ ] On save: apply per Phase 6c from `survey-ui-config.json`
44
+ - [ ] On quit: skip Phase 6c; ask deploy confirmation with existing Phase 6 build
43
45
 
44
46
  **Artifact:** `survey-ui-config.json` — apply source for local flow.
45
47
 
@@ -54,7 +56,7 @@ When the client uploads a logo in the local wizard:
54
56
 
55
57
  The logo is **not** fetched from wizard memory on CloudFront — only from the deploy bundle. See [`aws-deploy.md`](aws-deploy.md) § Logos & local assets.
56
58
 
57
- **Local quit (preview GC):** If the client exits without saving, wizard calls `POST /api/wizard-quit` → CLI deletes `out/previews/`.
59
+ **Local quit (preview GC):** If the client exits without saving, wizard calls `POST /api/wizard-quit` → CLI deletes `out/previews/` → exits 0. Agent **must not** re-run the wizard or share a new link — proceed to Phase 7 deploy ask.
58
60
 
59
61
  **After save:** `POST /api/save-config` also deletes `out/previews/` before CLI exits (preview garbage collection).
60
62
 
@@ -276,7 +278,7 @@ npx cfm-sdk fetch-ui-config
276
278
  | Mistake | Fix |
277
279
  |---------|-----|
278
280
  | Killed CLI during poll | Re-run `customize-ui`; client re-submits |
279
- | Client quit wizard | CLI exits with abandon message; S3 GC done re-run `customize-ui` for new session |
281
+ | Client quit wizard | CLI exits 0; GC done (`out/previews/` removed). **Do not** re-run wizard skip Phase 6c, ask deploy (INPUT 4) |
280
282
  | Applied from `survey-ui-config.json` while still seed | Use `.final.json` or run `confirm-ui-config` after apply |
281
283
  | Only changed primary color, ignored diff | Read **entire** `diff.md` — clients often change 20+ paths |
282
284
  | Skipped `confirm-ui-config` | Active JSON stays seed; next wizard re-open is wrong |
@@ -121,6 +121,12 @@ Only after H passes → ask deploy (INPUT 4). See [`aws-deploy.md`](aws-deploy.m
121
121
 
122
122
  **Do not re-run** `npx cfm-sdk export-previews` after wizard save — previews were deleted on purpose (GC). Export-previews is for **component structure changes** during Phase 5, not for applying saved config.
123
123
 
124
+ ### If client quit without saving
125
+
126
+ CLI exits 0 with GC message. **Do not** re-run wizard or share a new link. Skip this runbook (no Phase 6c). Ask deploy (INPUT 4) with the Phase 6 build.
127
+
128
+ **Wizard link:** parse `WIZARD_LINK=…` from CLI — never hardcode `localhost:4200`.
129
+
124
130
  ---
125
131
 
126
132
  ## Wizard steps → live preview keys
@@ -102,11 +102,12 @@ Single-select radio: outer border = focus ring, annulus = cell selected, inner d
102
102
  | Wizard section | Config path | CSS vars | Apply in |
103
103
  |----------------|-------------|----------|----------|
104
104
  | Button style | `questionTypes.NPS_SCALE.buttonStyle` | `data-cfm-nps-button-style` | `RatingScale.tsx` |
105
- | Track | `selectedFill`, `trackBar`, `trackHighlight` | `--cfm-nps-selected-fill`, `--cfm-nps-track-bar`, `--cfm-nps-track-highlight` | Track bar background |
106
- | Hint labels | `hintMinText`, `hintMaxText` + hint colors | `--cfm-hint-label-*` | Min/max anchor row |
107
- | Number labels | `numberLabelMode`, per-index colors | `--cfm-number-label-mode`, `--cfm-number-mono-color`, `--cfm-number-color-{n}` | Badge row above radios |
105
+ | Hint labels | `hintMinText`, `hintMaxText` + hint colors | `--cfm-hint-label-*` | `hintLabelStyle()` on min/max anchor row |
106
+ | Number labels | `numberLabelMode`, per-index colors | `--cfm-number-label-mode`, `--cfm-number-mono-color`, `--cfm-number-color-{n}` | Badge row; `resolveNumberLabelColor()` for unselected |
108
107
  | Selection | Theme → Question card | `--cfm-matrix-selected`, `--cfm-input-focus-ring` | `scaleRadioRingStyle` + `scaleRadioDotStyle` |
109
108
 
109
+ **Removed from wizard UI:** NPS selected fill, track bar, track highlight — use Theme → Question card tokens only.
110
+
110
111
  ### CFM_MATRIX (`CFM_matrix`, `CFM_dropdown`, `CFM_transpose`, `CFM_likert`, `CFM_bipolar`)
111
112
 
112
113
  | Wizard section | CSS vars | Apply in |
@@ -9,7 +9,7 @@
9
9
  | | `customize` **(default)** | `customize-ui` **(fallback)** |
10
10
  |--|---------------------------|--------------------------------|
11
11
  | **When** | Client on same machine as agent | Client on different device |
12
- | **URL** | `localhost:4200` | EC2 link with `session` + `secret` |
12
+ | **URL** | `WIZARD_LINK` from CLI (dynamic port) | `WIZARD_LINK` from CLI (EC2) |
13
13
  | **Previews** | `out/previews/` on agent disk | EC2 disk |
14
14
  | **Save** | `survey-ui-config.json` immediately | Poll → `.final.json` + `.diff.md` |
15
15
  | **GC** | CLI stops → no orphans | Session cleanup on EC2 |
@@ -34,7 +34,7 @@ Footer: **"Mock preview — run customize or customize-ui…"**
34
34
 
35
35
  ### Symptom
36
36
 
37
- Client cannot open `http://localhost:4200`
37
+ Client cannot open the `WIZARD_LINK` URL (wrong port or wrong machine)
38
38
 
39
39
  ### Fix
40
40
 
@@ -46,7 +46,7 @@ export CFM_DEPLOY_API_BASE="http://43.204.26.213:3000"
46
46
  npx cfm-sdk customize-ui
47
47
  ```
48
48
 
49
- Send **only the EC2 link** from CLI output.
49
+ Send **only** the `WIZARD_LINK=…` line from CLI output.
50
50
 
51
51
  ---
52
52
 
@@ -72,9 +72,25 @@ Wire `data-cfm-*` hooks and `--cfm-*` CSS vars per [`wizard-preview-build-guide.
72
72
 
73
73
  ---
74
74
 
75
+ ## Client quit without saving
76
+
77
+ ### Symptom
78
+
79
+ CLI prints quit message and exits 0; `out/previews/` removed.
80
+
81
+ ### Agent action (mandatory)
82
+
83
+ - **Do not** re-run `customize` or `customize-ui`
84
+ - **Do not** share a new wizard link
85
+ - Skip Phase 6c (no `survey-ui-config.json` update)
86
+ - Ask deploy confirmation (Phase 7 / INPUT 4) with the existing Phase 6 build
87
+
88
+ ---
89
+
75
90
  ## Checklist
76
91
 
77
92
  - [ ] Used **`customize`** as default when client said yes
78
- - [ ] Waited for `✅ UI configuration saved`
93
+ - [ ] Parsed **`WIZARD_LINK`** from CLI and shared only that URL
94
+ - [ ] Waited for CLI exit 0 (save or quit)
79
95
  - [ ] Phase 5 preview bridge + `data-cfm-*` wired
80
96
  - [ ] EC2 only if client on remote device
@@ -131,7 +131,7 @@ Run: `bash templates/verify-agent-build.sh`
131
131
  Only after Phase 6 passes and client says **yes**:
132
132
 
133
133
  ```bash
134
- npx cfm-sdk customize # DEFAULT — opens localhost:4200, WAITS until client saves
134
+ npx cfm-sdk customize # DEFAULT — prints WIZARD_LINK, WAITS until client saves or quits
135
135
  ```
136
136
 
137
137
  **Phase 6b:** Client on same machine → `customize` writes `survey-ui-config.json` directly. Remote client → `customize-ui` + EC2 link (see `wizard-troubleshooting.md`). Apply per `apply-ui-config.md`.
@@ -1,5 +1,15 @@
1
1
  import React, { useState } from 'react';
2
2
  import type { CsatQuestion, RatingMatrixQuestion, MatrixRowAnswers, ScaleColumn } from '@explorer02/cfm-survey-sdk';
3
+ import { csatColumnLabelStyle } from '@/lib/surveyUi/labelStyles';
4
+ import {
5
+ cellSelectedVar,
6
+ focusRingVar,
7
+ matrixRadioDotStyle,
8
+ matrixRadioRingStyle,
9
+ scaleCellSelectedStyle,
10
+ emojiSizeStyle,
11
+ unselectedOpacityStyle,
12
+ } from '@/lib/surveyUi/selectionStyles';
3
13
  import { getEmojiForIndex, CsatStarIcons } from './surveyUiIcons';
4
14
  import { columnSubmitValue, nonNaScaleColumns } from './surveyUiScaleUtils';
5
15
  import { CustomSliderTrack } from './CustomSliderTrack';
@@ -26,10 +36,10 @@ function GraphicsColumnLabelsRow({ scaleColumns }: { scaleColumns: ScaleColumn[]
26
36
  textAlign: 'center',
27
37
  fontSize: '12px',
28
38
  fontWeight: 500,
29
- color: '#4b5563',
30
39
  lineHeight: 1.2,
31
40
  wordBreak: 'break-word',
32
41
  width: '64px',
42
+ ...csatColumnLabelStyle(),
33
43
  }}
34
44
  >
35
45
  <span dangerouslySetInnerHTML={{ __html: col.label }} />
@@ -94,10 +104,10 @@ function CsatMatrixVerticalList({
94
104
  onClick={() => onCellSelect(row.id, submitValue)}
95
105
  style={{
96
106
  width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'flex-start',
97
- borderRadius: '8px', border: `1px solid ${isSelected ? '#e20074' : '#d1d5db'}`,
107
+ borderRadius: '8px', border: `1px solid ${isSelected ? focusRingVar : '#d1d5db'}`,
98
108
  padding: '14px 18px', cursor: 'pointer', transition: 'all 0.15s',
99
- backgroundColor: isSelected ? '#fdf2f8' : '#fff',
100
- color: isSelected ? '#e20074' : '#374151',
109
+ backgroundColor: isSelected ? 'var(--cfm-mcq-selected-bg)' : '#fff',
110
+ color: isSelected ? focusRingVar : '#374151',
101
111
  fontWeight: isSelected ? 600 : 400, fontSize: '15px', textAlign: 'left',
102
112
  }}
103
113
  >
@@ -116,9 +126,9 @@ function CsatMatrixVerticalList({
116
126
  key={col.id}
117
127
  style={{
118
128
  display: 'flex', alignItems: 'center', gap: '16px',
119
- borderRadius: '8px', border: `1px solid ${isSelected ? '#e20074' : '#e5e5e5'}`,
129
+ borderRadius: '8px', border: `1px solid ${isSelected ? focusRingVar : '#e5e5e5'}`,
120
130
  padding: '16px 20px', cursor: 'pointer', transition: 'all 0.15s',
121
- backgroundColor: isSelected ? '#fdf2f8' : '#fff',
131
+ backgroundColor: isSelected ? 'var(--cfm-mcq-selected-bg)' : '#fff',
122
132
  }}
123
133
  >
124
134
  <input type="radio" name={`${question.id}-${row.id}`}
@@ -126,13 +136,8 @@ function CsatMatrixVerticalList({
126
136
  onChange={() => onCellSelect(row.id, submitValue)}
127
137
  style={{ position: 'absolute', width: 0, height: 0, opacity: 0 }}
128
138
  />
129
- <div style={{
130
- display: 'flex', width: '20px', height: '20px', flexShrink: 0,
131
- alignItems: 'center', justifyContent: 'center', borderRadius: '50%',
132
- border: isSelected ? '2px solid #e20074' : '1.5px solid #9ca3af',
133
- backgroundColor: '#fff',
134
- }}>
135
- {isSelected && <div style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: '#e20074' }} />}
139
+ <div style={matrixRadioRingStyle(isSelected, '20px')}>
140
+ {isSelected && <div style={matrixRadioDotStyle(isSelected, '10px')} />}
136
141
  </div>
137
142
  <span style={{ fontSize: '15px', fontWeight: 500, color: '#111827' }}
138
143
  dangerouslySetInnerHTML={{ __html: col.label }} />
@@ -146,7 +151,7 @@ function CsatMatrixVerticalList({
146
151
  <label style={{ display: 'flex', alignItems: 'center', gap: '12px', cursor: 'pointer', userSelect: 'none' }}>
147
152
  <input type="checkbox" checked={selectedValue[row.id] === null}
148
153
  onChange={() => onCellSelect(row.id, selectedValue[row.id] === null ? undefined : null)}
149
- style={{ width: '16px', height: '16px', accentColor: '#e20074', cursor: 'pointer' }} />
154
+ style={{ width: '16px', height: '16px', accentColor: focusRingVar, cursor: 'pointer' }} />
150
155
  <span style={{ fontSize: '14px', fontWeight: 500, color: '#4b5563' }}>Not Applicable</span>
151
156
  </label>
152
157
  )}
@@ -190,7 +195,7 @@ function CsatMatrixGrid({
190
195
  {/* Labels row */}
191
196
  {showLabels && m > 0 && (
192
197
  <div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: '12px' }}>
193
- <div style={{ width: '180px', flexShrink: 0 }} />
198
+ <div style={{ width: 'var(--cfm-matrix-row-width, 180px)', flexShrink: 0 }} />
194
199
  <div style={{ flex: 1, display: 'flex' }}>
195
200
  <div style={{ flex: 1, position: 'relative', height: '24px', ...(isGraphics ? { margin: '0 40px' } : {}) }}>
196
201
  {labels!.map((label, i) => {
@@ -204,7 +209,8 @@ function CsatMatrixGrid({
204
209
  left: `${percent}%`,
205
210
  transform: 'translateX(-50%)',
206
211
  textAlign: 'center', fontSize: '13px', fontWeight: 600,
207
- color: '#4b5563', lineHeight: 1.3, whiteSpace: 'nowrap'
212
+ lineHeight: 1.3, whiteSpace: 'nowrap',
213
+ ...csatColumnLabelStyle(),
208
214
  }}>
209
215
  {label}
210
216
  </div>
@@ -219,7 +225,7 @@ function CsatMatrixGrid({
219
225
  {/* Column tick labels for graphics sliders (shown with or without anchor labels) */}
220
226
  {isGraphics && m > 0 && (
221
227
  <div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: showLabels ? '8px' : '16px', minHeight: '24px' }}>
222
- <div style={{ width: '180px', flexShrink: 0 }} />
228
+ <div style={{ width: 'var(--cfm-matrix-row-width, 180px)', flexShrink: 0 }} />
223
229
  <div style={{ flex: 1, display: 'flex' }}>
224
230
  <GraphicsColumnLabelsRow scaleColumns={scaleColumns} />
225
231
  {hasNotApplicable && (
@@ -232,13 +238,13 @@ function CsatMatrixGrid({
232
238
  {/* Column Headers for non-graphics */}
233
239
  {!isGraphics && !showLabels && m > 0 && (
234
240
  <div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: '16px', minHeight: '24px' }}>
235
- <div style={{ width: '180px', flexShrink: 0 }} />
241
+ <div style={{ width: 'var(--cfm-matrix-row-width, 180px)', flexShrink: 0 }} />
236
242
  <div style={{ flex: 1, display: 'flex' }}>
237
243
  <div style={{ flex: 1, display: 'grid', gridTemplateColumns: `repeat(${scaleColumns.length}, 1fr)` }}>
238
244
  {scaleColumns.map(col => (
239
245
  <div key={col.id} style={{ display: 'flex', justifyContent: 'center', padding: '0 4px' }}>
240
246
  <span
241
- style={{ fontSize: '13px', fontWeight: 500, color: '#4b5563', textAlign: 'center', lineHeight: 1.2 }}
247
+ style={{ fontSize: '13px', fontWeight: 500, textAlign: 'center', lineHeight: 1.2, ...csatColumnLabelStyle() }}
242
248
  dangerouslySetInnerHTML={{ __html: col.label }}
243
249
  />
244
250
  </div>
@@ -258,9 +264,9 @@ function CsatMatrixGrid({
258
264
  {statementRows.map((row, rowIdx) => (
259
265
  <div key={row.id} style={{
260
266
  display: 'flex', alignItems: 'center', borderRadius: '8px',
261
- padding: '12px 0', backgroundColor: rowIdx % 2 === 0 ? 'rgba(249, 250, 251, 0.8)' : '#fff',
267
+ padding: '12px 0', backgroundColor: rowIdx % 2 === 0 ? 'var(--cfm-zebra-row)' : '#fff',
262
268
  }}>
263
- <div style={{ width: '180px', flexShrink: 0, padding: '0 16px' }}>
269
+ <div style={{ width: 'var(--cfm-matrix-row-width, 180px)', flexShrink: 0, padding: '0 16px' }}>
264
270
  <span style={{ fontSize: '14px', fontWeight: 500, color: '#111827', lineHeight: 1.4 }}
265
271
  dangerouslySetInnerHTML={{ __html: row.statementText }} />
266
272
  </div>
@@ -298,13 +304,8 @@ function CsatMatrixGrid({
298
304
  const StarNode = isStar ? (isSelected ? (CsatStarIcons.filled as any) : (CsatStarIcons.empty as any)) : null;
299
305
 
300
306
  const RadioNode = (
301
- <div style={{
302
- display: 'flex', width: '18px', height: '18px',
303
- alignItems: 'center', justifyContent: 'center', borderRadius: '50%',
304
- margin: '0 auto', border: isSelected ? '2px solid #e20074' : '2px solid #d1d5db',
305
- backgroundColor: '#fff',
306
- }}>
307
- {isSelected && <div style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: '#e20074' }} />}
307
+ <div style={matrixRadioRingStyle(isSelected)}>
308
+ {isSelected && <div style={matrixRadioDotStyle(isSelected)} />}
308
309
  </div>
309
310
  );
310
311
 
@@ -324,23 +325,26 @@ function CsatMatrixGrid({
324
325
  cursor: 'pointer', borderRadius: '6px',
325
326
  ...(isEmoji ? {
326
327
  padding: '6px',
328
+ ...emojiSizeStyle(),
327
329
  transform: isSelected ? 'scale(1.1)' : isHovered ? 'scale(1.25)' : 'scale(1)',
328
330
  ...(isSelected ? {
329
- backgroundColor: 'rgba(252, 231, 243, 0.6)',
330
- boxShadow: '0 0 0 2px rgba(226, 0, 116, 0.3)',
331
+ backgroundColor: 'var(--cfm-mcq-selected-bg)',
332
+ boxShadow: `0 0 0 2px ${focusRingVar}`,
331
333
  borderRadius: '50%',
332
334
  } : {}),
333
335
  } : isStar ? {
334
336
  padding: '4px',
335
337
  transform: isHovered ? 'scale(1.1)' : 'scale(1)',
336
- opacity: isSelected ? 1 : isHovered ? 1 : 0.5,
338
+ ...(isSelected ? {} : unselectedOpacityStyle()),
339
+ opacity: isSelected ? 1 : isHovered ? 1 : undefined,
337
340
  } : isNumbered ? {
338
- width: '40px', height: '40px', borderRadius: '8px',
341
+ width: 'var(--cfm-csat-cell-size, 40px)', height: 'var(--cfm-csat-cell-size, 40px)', borderRadius: '8px',
339
342
  fontSize: '14px', fontWeight: 500,
340
- border: `1px solid ${isSelected ? '#e20074' : '#d1d5db'}`,
341
- backgroundColor: isSelected ? '#e20074' : '#fff',
343
+ border: `1px solid ${isSelected ? focusRingVar : '#d1d5db'}`,
344
+ backgroundColor: isSelected ? cellSelectedVar : '#fff',
342
345
  color: isSelected ? '#fff' : '#111827',
343
346
  transition: 'all 0.15s',
347
+ ...(isSelected ? scaleCellSelectedStyle(true) : {}),
344
348
  } : { padding: '8px' }),
345
349
  }}
346
350
  >
@@ -378,7 +382,7 @@ function CsatMatrixGrid({
378
382
  <label style={{ cursor: 'pointer' }}>
379
383
  <input type="checkbox" checked={selectedValue[row.id] === null}
380
384
  onChange={() => onCellSelect(row.id, selectedValue[row.id] === null ? undefined : null)}
381
- style={{ width: '16px', height: '16px', accentColor: '#e20074', cursor: 'pointer' }} />
385
+ style={{ width: '16px', height: '16px', accentColor: focusRingVar, cursor: 'pointer' }} />
382
386
  </label>
383
387
  </div>
384
388
  )}
@@ -437,7 +441,8 @@ function CsatMatrixCarousel({
437
441
  <div key={i} style={{
438
442
  position: 'absolute', left: `${percent}%`, transform: 'translateX(-50%)',
439
443
  textAlign: 'center', fontSize: '13px', fontWeight: 600,
440
- color: '#4b5563', lineHeight: 1.3, whiteSpace: 'nowrap'
444
+ lineHeight: 1.3, whiteSpace: 'nowrap',
445
+ ...csatColumnLabelStyle(),
441
446
  }}>
442
447
  {label}
443
448
  </div>
@@ -481,13 +486,8 @@ function CsatMatrixCarousel({
481
486
  const EmojiNode = isEmoji ? getEmojiForIndex(displayIdx, scaleColumns.length) : null;
482
487
  const StarNode = isStar ? (isSelected ? (CsatStarIcons.filled as any) : (CsatStarIcons.empty as any)) : null;
483
488
  const RadioNode = (
484
- <div style={{
485
- display: 'flex', width: '18px', height: '18px',
486
- alignItems: 'center', justifyContent: 'center', borderRadius: '50%',
487
- margin: '0 auto', border: isSelected ? '2px solid #e20074' : '2px solid #d1d5db',
488
- backgroundColor: '#fff',
489
- }}>
490
- {isSelected && <div style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: '#e20074' }} />}
489
+ <div style={matrixRadioRingStyle(isSelected)}>
490
+ {isSelected && <div style={matrixRadioDotStyle(isSelected)} />}
491
491
  </div>
492
492
  );
493
493
 
@@ -503,19 +503,23 @@ function CsatMatrixCarousel({
503
503
  transition: 'all 0.15s', border: 'none', backgroundColor: 'transparent',
504
504
  cursor: 'pointer', borderRadius: '6px', pointerEvents: 'none',
505
505
  ...(isEmoji ? {
506
- padding: '6px', transform: isSelected ? 'scale(1.1)' : isHovered ? 'scale(1.25)' : 'scale(1)',
507
- ...(isSelected ? { backgroundColor: 'rgba(252, 231, 243, 0.6)', boxShadow: '0 0 0 2px rgba(226, 0, 116, 0.3)', borderRadius: '50%' } : {}),
506
+ padding: '6px', ...emojiSizeStyle(),
507
+ transform: isSelected ? 'scale(1.1)' : isHovered ? 'scale(1.25)' : 'scale(1)',
508
+ ...(isSelected ? { backgroundColor: 'var(--cfm-mcq-selected-bg)', boxShadow: `0 0 0 2px ${focusRingVar}`, borderRadius: '50%' } : {}),
508
509
  } : isStar ? {
509
- padding: '4px', transform: isHovered ? 'scale(1.1)' : 'scale(1)', opacity: isSelected ? 1 : isHovered ? 1 : 0.5,
510
+ padding: '4px', transform: isHovered ? 'scale(1.1)' : 'scale(1)',
511
+ ...(isSelected ? {} : unselectedOpacityStyle()),
512
+ opacity: isSelected ? 1 : isHovered ? 1 : undefined,
510
513
  } : isNumbered ? {
511
- width: '40px', height: '40px', borderRadius: '8px', fontSize: '14px', fontWeight: 500,
512
- border: `1px solid ${isSelected ? '#e20074' : '#d1d5db'}`, backgroundColor: isSelected ? '#e20074' : '#fff', color: isSelected ? '#fff' : '#111827',
514
+ width: 'var(--cfm-csat-cell-size, 40px)', height: 'var(--cfm-csat-cell-size, 40px)', borderRadius: '8px', fontSize: '14px', fontWeight: 500,
515
+ border: `1px solid ${isSelected ? focusRingVar : '#d1d5db'}`, backgroundColor: isSelected ? cellSelectedVar : '#fff', color: isSelected ? '#fff' : '#111827',
516
+ ...(isSelected ? scaleCellSelectedStyle(true) : {}),
513
517
  } : { padding: '8px' }),
514
518
  }}
515
519
  >
516
520
  {(EmojiNode ?? StarNode ?? (isNumbered ? (displayIdx + 1) : RadioNode)) as React.ReactNode}
517
521
  </button>
518
- <span style={{ fontSize: '13px', fontWeight: 500, color: '#4b5563', textAlign: 'center', lineHeight: 1.2 }}
522
+ <span style={{ fontSize: '13px', fontWeight: 500, textAlign: 'center', lineHeight: 1.2, ...csatColumnLabelStyle() }}
519
523
  dangerouslySetInnerHTML={{ __html: col.label }} />
520
524
  </div>
521
525
  );
@@ -528,7 +532,7 @@ function CsatMatrixCarousel({
528
532
  <label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
529
533
  <input type="checkbox" checked={selectedValue[row.id] === null}
530
534
  onChange={() => onCellSelect(row.id, selectedValue[row.id] === null ? undefined : null)}
531
- style={{ width: '16px', height: '16px', accentColor: '#e20074', cursor: 'pointer' }} />
535
+ style={{ width: '16px', height: '16px', accentColor: focusRingVar, cursor: 'pointer' }} />
532
536
  <span style={{ fontSize: '14px', fontWeight: 500, color: '#4b5563' }}>N/A</span>
533
537
  </label>
534
538
  </div>
@@ -550,7 +554,7 @@ function CsatMatrixCarousel({
550
554
  <button key={i} type="button" onClick={() => setActiveCarouselIndex(i)}
551
555
  style={{
552
556
  width: i === activeCarouselIndex ? '20px' : '8px', height: '8px', borderRadius: '4px',
553
- backgroundColor: i === activeCarouselIndex ? '#e20074' : '#d1d5db', border: 'none', padding: 0,
557
+ backgroundColor: i === activeCarouselIndex ? cellSelectedVar : '#d1d5db', border: 'none', padding: 0,
554
558
  cursor: 'pointer', transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
555
559
  }} />
556
560
  ))}
@@ -590,7 +594,6 @@ export function CsatMatrixScale({ question, selectedValue = {}, onSelect }: Csat
590
594
  const isStar = displayStyle === 'star';
591
595
  const isNumbered = displayStyle === 'numbered';
592
596
  const isGraphics = displayStyle === 'graphics';
593
- const isStandardRadio = displayStyle === 'standard';
594
597
 
595
598
  const scaleColumns = nonNaScaleColumns(question.scaleColumns);
596
599
 
@@ -609,7 +612,7 @@ export function CsatMatrixScale({ question, selectedValue = {}, onSelect }: Csat
609
612
  );
610
613
  }
611
614
 
612
- if (isDropdown || isSelectBox || isStandardRadio) {
615
+ if (isDropdown || isSelectBox) {
613
616
  return (
614
617
  <CsatMatrixVerticalList
615
618
  question={question}
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import React from 'react';
10
+ import { focusRingVar, cellSelectedVar } from '@/lib/surveyUi/selectionStyles';
10
11
  import { getEmojiForIndex } from './surveyUiIcons';
11
12
 
12
13
  type CustomSliderTrackProps = {
@@ -50,7 +51,8 @@ export function CustomSliderTrack({
50
51
  ? tooltipLabel.replace(/<[^>]*>/g, '')
51
52
  : String(Math.round(value));
52
53
 
53
- const themeColor = disabled ? '#9ca3af' : '#e20074';
54
+ const trackColor = disabled ? 'var(--cfm-slider-tick, #9ca3af)' : 'var(--cfm-slider-track, var(--cfm-primary))';
55
+ const thumbBorderColor = disabled ? 'var(--cfm-slider-tick, #9ca3af)' : focusRingVar;
54
56
 
55
57
  return (
56
58
  <div
@@ -60,12 +62,12 @@ export function CustomSliderTrack({
60
62
  >
61
63
  <div style={{
62
64
  position: 'absolute', top: '50%', left: 0, right: 0, transform: 'translateY(-50%)',
63
- height: '4px', borderRadius: '2px', backgroundColor: '#e5e7eb',
65
+ height: '4px', borderRadius: '2px', backgroundColor: 'var(--cfm-slider-tick, #e5e7eb)',
64
66
  }} />
65
67
 
66
68
  <div style={{
67
69
  position: 'absolute', top: '50%', left: 0, width: `${percentage}%`, transform: 'translateY(-50%)',
68
- height: '4px', borderRadius: '2px', backgroundColor: themeColor,
70
+ height: '4px', borderRadius: '2px', backgroundColor: trackColor,
69
71
  transition: isDragging ? 'none' : 'width 0.15s ease-in-out',
70
72
  }} />
71
73
 
@@ -76,7 +78,7 @@ export function CustomSliderTrack({
76
78
  key={idx}
77
79
  style={{
78
80
  position: 'absolute', top: '50%', left: `${tickPct}%`, transform: 'translate(-50%, -50%)',
79
- width: '1px', height: '8px', backgroundColor: '#d1d5db', pointerEvents: 'none',
81
+ width: '1px', height: '8px', backgroundColor: 'var(--cfm-slider-tick, #d1d5db)', pointerEvents: 'none',
80
82
  }}
81
83
  />
82
84
  );
@@ -113,15 +115,22 @@ export function CustomSliderTrack({
113
115
  ) : (
114
116
  <div style={{
115
117
  position: 'absolute', top: '50%', left: `${percentage}%`, transform: 'translate(-50%, -50%)',
116
- width: '18px', height: '18px', borderRadius: '50%', backgroundColor: '#ffffff',
117
- border: `2.5px solid ${themeColor}`,
118
+ width: '18px', height: '18px', borderRadius: '50%',
119
+ border: `2.5px solid ${thumbBorderColor}`,
120
+ backgroundColor: cellSelectedVar,
121
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
118
122
  boxShadow: (isHovered || isDragging) && !disabled
119
- ? '0 0 0 6px rgba(226, 0, 116, 0.2), 0 1px 3px rgba(0,0,0,0.2)'
123
+ ? '0 0 0 6px var(--cfm-mcq-selected-bg, rgba(226, 0, 116, 0.2)), 0 1px 3px rgba(0,0,0,0.2)'
120
124
  : '0 1px 3px rgba(0,0,0,0.2)',
121
125
  pointerEvents: 'none',
122
126
  transition: isDragging ? 'none' : 'left 0.15s ease-in-out, box-shadow 0.15s, border-color 0.2s',
123
127
  boxSizing: 'border-box',
124
- }} />
128
+ }}>
129
+ <div style={{
130
+ width: '33%', height: '33%', borderRadius: '50%',
131
+ backgroundColor: 'var(--cfm-slider-thumb, var(--cfm-primary))',
132
+ }} />
133
+ </div>
125
134
  )}
126
135
 
127
136
  {(isHovered || isDragging) && !disabled && (
@@ -8,6 +8,7 @@
8
8
 
9
9
  import React, { useState, useRef } from 'react';
10
10
  import type { FileUploadQuestion } from '@explorer02/cfm-survey-sdk';
11
+ import { focusRingVar } from '@/lib/surveyUi/selectionStyles';
11
12
 
12
13
  type UploadedFileAnswer = {
13
14
  id: string;
@@ -86,8 +87,15 @@ function FileUploadDropzone({
86
87
  onDrag: (e: React.DragEvent) => void;
87
88
  onDrop: (e: React.DragEvent) => void;
88
89
  }) {
90
+ const dropzoneStyleAttr =
91
+ typeof document !== 'undefined'
92
+ ? document.documentElement.getAttribute('data-cfm-dropzone-style') ?? undefined
93
+ : undefined;
94
+
89
95
  return (
90
96
  <div
97
+ className="cfm-dropzone"
98
+ {...(dropzoneStyleAttr ? { 'data-cfm-dropzone-style': dropzoneStyleAttr } : {})}
91
99
  onDragEnter={onDrag}
92
100
  onDragOver={onDrag}
93
101
  onDragLeave={onDrag}
@@ -98,10 +106,10 @@ function FileUploadDropzone({
98
106
  flexDirection: 'column',
99
107
  alignItems: 'center',
100
108
  justifyContent: 'center',
101
- border: `2px dashed ${isDragActive ? '#e20074' : '#d1d5db'}`,
109
+ border: `2px var(--cfm-upload-border-style, dashed) ${isDragActive ? focusRingVar : 'var(--cfm-upload-border-color)'}`,
102
110
  borderRadius: '12px',
103
- backgroundColor: isDragActive ? '#fdf2f8' : '#f9fafb',
104
- padding: '40px 24px',
111
+ backgroundColor: isDragActive ? 'var(--cfm-mcq-selected-bg)' : 'var(--cfm-upload-fill)',
112
+ padding: 'var(--cfm-upload-padding)',
105
113
  cursor: disabled ? 'not-allowed' : 'pointer',
106
114
  opacity: disabled ? 0.6 : 1,
107
115
  textAlign: 'center',
@@ -2,7 +2,7 @@
2
2
  * Wizard-ready Header — copy to src/components/Header.tsx
3
3
  *
4
4
  * Brand row: [logo image] [company name] — company sits after logo when both exist.
5
- * When no logo file: text fallback via data-cfm-logo-text (company name or empty).
5
+ * Layout locked: logo left, name after (flex row only).
6
6
  *
7
7
  * After wizard save, logo src MUST come from survey-ui-config.json global.logo.fileName
8
8
  * via uiConfig.ts — never hardcode telekom-logo.svg or change fileName manually.
@@ -33,42 +33,30 @@ export default function Header({ embedded = false }: HeaderProps) {
33
33
  }}
34
34
  >
35
35
  <div
36
- className="cfm-header-inner grid h-full w-full items-center"
36
+ className="cfm-header-inner flex h-full w-full items-center"
37
37
  style={{
38
- gridTemplateColumns: '1fr 1fr 1fr',
39
38
  padding: `0 var(--cfm-header-padding-x, 40px)`,
40
39
  }}
41
40
  >
42
41
  <div
43
- className="cfm-header-brand flex items-center"
42
+ className="cfm-header-brand flex min-w-0 items-center"
44
43
  style={{
45
- gridColumn: 'var(--cfm-header-logo-col, 1)',
46
- justifySelf: 'var(--cfm-header-logo-justify, start)',
47
44
  gap: 'var(--cfm-header-brand-gap, 16px)',
48
45
  }}
49
46
  >
50
- {hasLogoFile ? (
51
- <img
52
- data-cfm-logo
53
- src={seedLogoSrc!}
54
- alt={seedCompany || 'Logo'}
55
- className="shrink-0 object-contain"
56
- style={{
57
- width: 'var(--cfm-header-logo-width, 120px)',
58
- height: 'var(--cfm-header-logo-height, 120px)',
59
- padding: 'var(--cfm-header-logo-padding, 10px)',
60
- background: 'var(--cfm-primary)',
61
- }}
62
- />
63
- ) : (
64
- <img
65
- data-cfm-logo
66
- src=""
67
- alt="Logo"
68
- className="shrink-0 object-contain"
69
- style={{ display: 'none' }}
70
- />
71
- )}
47
+ <img
48
+ data-cfm-logo
49
+ src={hasLogoFile ? seedLogoSrc! : ''}
50
+ alt={seedCompany || 'Logo'}
51
+ className="shrink-0 object-contain"
52
+ style={{
53
+ display: hasLogoFile ? '' : 'none',
54
+ width: 'var(--cfm-header-logo-width, 120px)',
55
+ height: 'var(--cfm-header-logo-height, 120px)',
56
+ padding: 'var(--cfm-header-logo-padding, 0px)',
57
+ background: 'var(--cfm-header-logo-bg, transparent)',
58
+ }}
59
+ />
72
60
 
73
61
  <span
74
62
  data-cfm-logo-text
@@ -83,26 +71,22 @@ export default function Header({ embedded = false }: HeaderProps) {
83
71
  {seedCompany}
84
72
  </span>
85
73
 
86
- <span
87
- data-cfm-company
88
- className="font-semibold"
89
- style={{
90
- display:
91
- seedShowCompany && (hasLogoFile || seedCompany)
92
- ? hasLogoFile
93
- ? ''
94
- : 'none'
95
- : 'none',
96
- marginTop: 'var(--cfm-header-company-margin-top, 0)',
97
- marginLeft: 'var(--cfm-header-company-margin-left, 0)',
98
- marginRight: 'var(--cfm-header-company-margin-right, 0)',
99
- color: 'var(--cfm-header-company-color, var(--cfm-text))',
100
- fontSize: 'var(--cfm-header-company-size, 18px)',
101
- fontWeight: 'var(--cfm-header-company-weight, 600)',
102
- }}
103
- >
104
- {seedCompany}
105
- </span>
74
+ {seedShowCompany && seedCompany ? (
75
+ <span
76
+ data-cfm-company
77
+ className="truncate font-semibold"
78
+ style={{
79
+ display: hasLogoFile ? '' : 'none',
80
+ color: 'var(--cfm-header-company-color, var(--cfm-text))',
81
+ fontSize: 'var(--cfm-header-company-size, 18px)',
82
+ fontWeight: 'var(--cfm-header-company-weight, 600)',
83
+ }}
84
+ >
85
+ {seedCompany}
86
+ </span>
87
+ ) : (
88
+ <span data-cfm-company className="hidden" aria-hidden />
89
+ )}
106
90
  </div>
107
91
  </div>
108
92
  </header>