@explorer02/cfm-survey-sdk 0.2.7 → 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 (44) 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 +7 -9
  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 -1
  9. package/templates/docs/00-integration/wizard-config-handoff.md +11 -9
  10. package/templates/docs/00-integration/wizard-post-customize-build.md +8 -2
  11. package/templates/docs/00-integration/wizard-preview-build-guide.md +0 -2
  12. package/templates/docs/00-integration/wizard-question-type-styling.md +7 -6
  13. package/templates/docs/00-integration/wizard-troubleshooting.md +20 -4
  14. package/templates/docs/03-ui-specs/README.md +1 -1
  15. package/templates/docs/MANIFEST.json +0 -3
  16. package/templates/docs/index.md +4 -6
  17. package/templates/docs/templates/CsatMatrixScale.tsx +56 -53
  18. package/templates/docs/templates/CustomSliderTrack.tsx +17 -8
  19. package/templates/docs/templates/FileUploadScale.tsx +11 -3
  20. package/templates/docs/templates/Header.tsx +32 -48
  21. package/templates/docs/templates/HeatmapScale.tsx +7 -4
  22. package/templates/docs/templates/LikertMatrixScale.tsx +58 -43
  23. package/templates/docs/templates/MatrixDropdown.tsx +12 -11
  24. package/templates/docs/templates/Question.tsx +4 -23
  25. package/templates/docs/templates/RankOrderScale.tsx +46 -6
  26. package/templates/docs/templates/RatingScale.tsx +38 -0
  27. package/templates/docs/templates/SliderMatrixScale.tsx +4 -3
  28. package/templates/docs/templates/verify-agent-build.sh +17 -0
  29. package/templates/preview-harness/preview-bridge.inline.js +10 -0
  30. package/templates/preview-harness/previewPages.ts +5 -7
  31. package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +11 -6
  32. package/templates/preview-harness/vite-app/src/fixtures/questions.ts +2 -10
  33. package/templates/preview-harness/vite-app/src/preview-live-overrides.css +11 -6
  34. package/templates/previewBridge.ts +13 -0
  35. package/templates/survey-theme.css +17 -17
  36. package/templates/wizard-dist/assets/{PreviewMock-tTMvwuEy.js → PreviewMock-CysRws9G.js} +1 -1
  37. package/templates/wizard-dist/assets/TypePanel-DWX1LL3q.js +1 -0
  38. package/templates/wizard-dist/assets/index-DIuc7AU2.js +34 -0
  39. package/templates/wizard-dist/assets/index-pCM8n0FT.css +1 -0
  40. package/templates/wizard-dist/index.html +2 -2
  41. package/templates/docs/00-integration/wizard-customization-catalog.md +0 -391
  42. package/templates/wizard-dist/assets/TypePanel-B9Ls7BSq.js +0 -1
  43. package/templates/wizard-dist/assets/index-CjkIlOUI.css +0 -1
  44. package/templates/wizard-dist/assets/index-CoCrnnxt.js +0 -34
@@ -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>
@@ -26,11 +26,11 @@ function HeatmapSpot({ spot }: { spot: { id: string; x: number; y: number } }) {
26
26
  style={{
27
27
  left: `${spot.x * 100}%`,
28
28
  top: `${spot.y * 100}%`,
29
- width: 16,
30
- height: 16,
29
+ width: 'var(--cfm-heatmap-pin-size, 16px)',
30
+ height: 'var(--cfm-heatmap-pin-size, 16px)',
31
31
  transform: 'translate(-50%, -50%)',
32
- backgroundColor: '#F97316',
33
- border: '2px solid white',
32
+ backgroundColor: 'var(--cfm-heatmap-pin)',
33
+ border: '2px solid var(--cfm-heatmap-pin-border, white)',
34
34
  boxShadow: '0 1px 3px rgba(0,0,0,0.25)',
35
35
  }}
36
36
  />
@@ -63,6 +63,9 @@ export function HeatmapScale({ question, selectedValue = [], onSelect }: Heatmap
63
63
  }
64
64
 
65
65
  onSelect([...spots, createNewSpot(x, y)]);
66
+ if (typeof window !== 'undefined' && window.parent !== window) {
67
+ window.parent.postMessage({ type: 'CFM_HEATMAP_PIN', pin: { x, y } }, '*');
68
+ }
66
69
  }
67
70
 
68
71
  function handleImageLoad() {
@@ -1,5 +1,12 @@
1
1
  import React, { useState } from 'react';
2
2
  import type { CfmMatrixQuestion, MatrixRowAnswers } from '@explorer02/cfm-survey-sdk';
3
+ import { matrixColumnLabelStyle } from '@/lib/surveyUi/labelStyles';
4
+ import {
5
+ cellSelectedVar,
6
+ focusRingVar,
7
+ matrixRadioDotStyle,
8
+ matrixRadioRingStyle,
9
+ } from '@/lib/surveyUi/selectionStyles';
3
10
  import { columnSubmitValue } from './surveyUiScaleUtils';
4
11
  import MatrixDropdown from './MatrixDropdown';
5
12
 
@@ -111,8 +118,8 @@ function LikertMatrixCarouselLayout({
111
118
  <div style={{
112
119
  display: 'flex', alignItems: 'center', justifyContent: 'center',
113
120
  width: '18px', height: '18px', borderRadius: '4px',
114
- border: selected ? '2px solid #e20074' : '2px solid #d1d5db',
115
- backgroundColor: selected ? '#e20074' : '#fff',
121
+ border: selected ? `2px solid ${focusRingVar}` : '2px solid #d1d5db',
122
+ backgroundColor: selected ? cellSelectedVar : '#fff',
116
123
  flexShrink: 0, transition: 'all 0.15s'
117
124
  }}>
118
125
  {selected && (
@@ -122,13 +129,8 @@ function LikertMatrixCarouselLayout({
122
129
  )}
123
130
  </div>
124
131
  ) : (
125
- <div style={{
126
- display: 'flex', width: '18px', height: '18px', flexShrink: 0,
127
- alignItems: 'center', justifyContent: 'center', borderRadius: '50%',
128
- border: selected ? '2px solid #e20074' : '1.5px solid #9ca3af',
129
- backgroundColor: '#fff',
130
- }}>
131
- {selected && <div style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: '#e20074' }} />}
132
+ <div style={matrixRadioRingStyle(selected)}>
133
+ {selected && <div style={matrixRadioDotStyle(selected, '10px')} />}
132
134
  </div>
133
135
  )}
134
136
  <span style={{ fontSize: '15px', fontWeight: 400, color: '#374151', userSelect: 'none' }} dangerouslySetInnerHTML={{ __html: col.label }} />
@@ -206,7 +208,7 @@ function LikertMatrixGridLayout({
206
208
  <div style={{ display: 'flex', flexDirection: 'column', marginBottom: '16px' }}>
207
209
  {hasLabels && (
208
210
  <div style={{ display: 'flex', alignItems: 'flex-end', minHeight: '24px', marginBottom: '8px' }}>
209
- <div style={{ flex: '0 0 25%', paddingRight: '16px', boxSizing: 'border-box' }} />
211
+ <div style={{ flex: `0 0 var(--cfm-matrix-bipolar-width, 25%)`, paddingRight: '16px', boxSizing: 'border-box' }} />
210
212
  <div style={{ flex: 1, position: 'relative', height: '20px' }}>
211
213
  {labels.map((lbl, idx) => {
212
214
  let leftPos = '50%';
@@ -224,23 +226,23 @@ function LikertMatrixGridLayout({
224
226
  );
225
227
  })}
226
228
  </div>
227
- <div style={{ flex: '0 0 25%', paddingLeft: '16px', boxSizing: 'border-box' }} />
229
+ <div style={{ flex: `0 0 var(--cfm-matrix-bipolar-width, 25%)`, paddingLeft: '16px', boxSizing: 'border-box' }} />
228
230
  </div>
229
231
  )}
230
232
  {showColumnHeaders && (
231
233
  <div style={{ display: 'flex', alignItems: 'flex-end', minHeight: '24px' }}>
232
- <div style={{ flex: isBipolar ? '0 0 25%' : '0 0 180px', paddingRight: '16px', boxSizing: 'border-box' }} />
234
+ <div style={{ flex: isBipolar ? '0 0 var(--cfm-matrix-bipolar-width, 25%)' : '0 0 var(--cfm-matrix-row-width, 180px)', paddingRight: '16px', boxSizing: 'border-box' }} />
233
235
  <div style={{ flex: 1, display: 'flex' }}>
234
236
  <div style={{ flex: 1, display: 'grid', gridTemplateColumns: `repeat(${gridCols.length}, 1fr)` }}>
235
237
  {gridCols.map((col) => (
236
238
  <div key={col.id} style={{ display: 'flex', justifyContent: 'center', padding: '0 4px' }}>
237
- <span style={{ fontSize: '13px', fontWeight: 500, color: '#4b5563', textAlign: 'center', lineHeight: 1.2 }}
239
+ <span style={{ fontSize: '13px', fontWeight: 500, textAlign: 'center', lineHeight: 1.2, ...matrixColumnLabelStyle() }}
238
240
  dangerouslySetInnerHTML={{ __html: transposeTable ? ('statementText' in col ? col.statementText : col.label) : ('label' in col ? col.label : (col as { statementText: string }).statementText) }} />
239
241
  </div>
240
242
  ))}
241
243
  </div>
242
244
  </div>
243
- {isBipolar && <div style={{ flex: '0 0 25%', paddingLeft: '16px', boxSizing: 'border-box' }} />}
245
+ {isBipolar && <div style={{ flex: `0 0 var(--cfm-matrix-bipolar-width, 25%)`, paddingLeft: '16px', boxSizing: 'border-box' }} />}
244
246
  </div>
245
247
  )}
246
248
  </div>
@@ -258,10 +260,10 @@ function LikertMatrixGridLayout({
258
260
  {repeatColumnHeaders && renderHeader()}
259
261
  <div style={{
260
262
  display: 'flex', alignItems: 'center', borderRadius: '8px',
261
- padding: '12px 0', backgroundColor: !repeatColumnHeaders && rowIdx % 2 === 0 ? 'rgba(249, 250, 251, 0.8)' : '#fff',
263
+ padding: '12px 0', backgroundColor: !repeatColumnHeaders && rowIdx % 2 === 0 ? 'var(--cfm-zebra-row)' : '#fff',
262
264
  boxSizing: 'border-box'
263
265
  }}>
264
- <div style={{ flex: isBipolar ? '0 0 25%' : '0 0 180px', padding: '0 16px', boxSizing: 'border-box' }}>
266
+ <div style={{ flex: isBipolar ? '0 0 var(--cfm-matrix-bipolar-width, 25%)' : '0 0 var(--cfm-matrix-row-width, 180px)', padding: '0 16px', boxSizing: 'border-box' }}>
265
267
  <span style={{ fontSize: '14px', fontWeight: 500, color: '#111827', lineHeight: 1.4, wordWrap: 'break-word', display: 'block' }}
266
268
  dangerouslySetInnerHTML={{ __html: transposeTable ? ('label' in rowItem ? rowItem.label : '') : ('statementText' in rowItem ? rowItem.statementText : '') }} />
267
269
  </div>
@@ -279,32 +281,45 @@ function LikertMatrixGridLayout({
279
281
 
280
282
  return (
281
283
  <div key={colItem.id} style={{ display: 'flex', justifyContent: 'center' }}>
282
- <label style={{
283
- cursor: isDisabled ? 'not-allowed' : 'pointer',
284
- opacity: isDisabled ? 0.4 : 1,
285
- padding: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center'
286
- }}>
287
- <input
288
- type={isMultiple ? "checkbox" : "radio"}
289
- checked={selected}
290
- disabled={isDisabled}
291
- onClick={(e) => {
292
- if (!isMultiple && selected) {
293
- onCellSelect(actualRowId, undefined);
294
- }
295
- }}
296
- onChange={(e) => {
297
- if (isMultiple || !selected) {
298
- onCellSelect(actualRowId, actualColValue);
299
- }
300
- }}
301
- style={{
302
- width: '18px', height: '18px', accentColor: '#e20074',
303
- cursor: isDisabled ? 'not-allowed' : 'pointer',
304
- ...(isMultiple ? { borderRadius: '4px' } : {})
305
- }}
306
- />
307
- </label>
284
+ <button
285
+ type="button"
286
+ disabled={isDisabled}
287
+ onClick={() => {
288
+ if (isDisabled) return;
289
+ if (!isMultiple && selected) {
290
+ onCellSelect(actualRowId, undefined);
291
+ } else {
292
+ onCellSelect(actualRowId, actualColValue);
293
+ }
294
+ }}
295
+ style={{
296
+ cursor: isDisabled ? 'not-allowed' : 'pointer',
297
+ opacity: isDisabled ? 0.4 : 1,
298
+ padding: 'var(--cfm-matrix-cell-padding, 8px)',
299
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
300
+ border: 'none', background: 'transparent',
301
+ }}
302
+ >
303
+ {isMultiple ? (
304
+ <div style={{
305
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
306
+ width: '18px', height: '18px', borderRadius: '4px',
307
+ border: selected ? `2px solid ${focusRingVar}` : '2px solid #d1d5db',
308
+ backgroundColor: selected ? cellSelectedVar : '#fff',
309
+ transition: 'all 0.15s',
310
+ }}>
311
+ {selected && (
312
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
313
+ <polyline points="20 6 9 17 4 12" />
314
+ </svg>
315
+ )}
316
+ </div>
317
+ ) : (
318
+ <div style={matrixRadioRingStyle(selected)}>
319
+ {selected && <div style={matrixRadioDotStyle(selected)} />}
320
+ </div>
321
+ )}
322
+ </button>
308
323
  </div>
309
324
  );
310
325
  })}
@@ -312,7 +327,7 @@ function LikertMatrixGridLayout({
312
327
  </div>
313
328
 
314
329
  {isBipolar && (
315
- <div style={{ flex: '0 0 25%', padding: '0 16px', textAlign: 'right', boxSizing: 'border-box' }}>
330
+ <div style={{ flex: '0 0 var(--cfm-matrix-bipolar-width, 25%)', padding: '0 16px', textAlign: 'right', boxSizing: 'border-box' }}>
316
331
  <span style={{ fontSize: '14px', fontWeight: 500, color: '#111827', lineHeight: 1.4, wordWrap: 'break-word', display: 'block' }}
317
332
  dangerouslySetInnerHTML={{ __html: transposeTable ? '' : ('oppositeStatementText' in rowItem ? rowItem.oppositeStatementText ?? '' : '') }} />
318
333
  </div>
@@ -9,6 +9,7 @@
9
9
  import React, { useState, useRef, useEffect } from 'react';
10
10
  import type { ScaleColumn } from '@explorer02/cfm-survey-sdk';
11
11
  import { matrixColumnStoredValue } from '@explorer02/cfm-survey-sdk';
12
+ import { cellSelectedVar, focusRingVar } from '@/lib/surveyUi/selectionStyles';
12
13
 
13
14
  type MatrixDropdownProps = {
14
15
  options: ScaleColumn[];
@@ -72,9 +73,9 @@ export default function MatrixDropdown({
72
73
  style={{
73
74
  display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '8px',
74
75
  width: '100%', padding: '10px 14px', backgroundColor: '#fff',
75
- border: `1px solid ${isOpen ? '#e20074' : '#d1d5db'}`, borderRadius: '8px',
76
+ border: `1px solid ${isOpen ? focusRingVar : '#d1d5db'}`, borderRadius: '8px',
76
77
  cursor: 'pointer', transition: 'all 0.2s', minHeight: '44px',
77
- boxShadow: isOpen ? '0 0 0 3px rgba(226, 0, 116, 0.1)' : '0 1px 2px rgba(0,0,0,0.05)',
78
+ boxShadow: isOpen ? '0 0 0 3px var(--cfm-mcq-selected-bg)' : '0 1px 2px rgba(0,0,0,0.05)',
78
79
  }}
79
80
  >
80
81
  {multiValues.length === 0 ? (
@@ -124,9 +125,9 @@ export default function MatrixDropdown({
124
125
  style={{
125
126
  display: 'flex', alignItems: 'center', justifyContent: 'space-between',
126
127
  width: '100%', padding: '14px 16px', backgroundColor: '#fff',
127
- border: `1px solid ${isOpen ? '#e20074' : '#d1d5db'}`, borderRadius: '8px',
128
+ border: `1px solid ${isOpen ? focusRingVar : '#d1d5db'}`, borderRadius: '8px',
128
129
  cursor: 'pointer', transition: 'all 0.2s',
129
- boxShadow: isOpen ? '0 0 0 3px rgba(226, 0, 116, 0.1)' : '0 1px 2px rgba(0,0,0,0.05)',
130
+ boxShadow: isOpen ? '0 0 0 3px var(--cfm-mcq-selected-bg)' : '0 1px 2px rgba(0,0,0,0.05)',
130
131
  }}
131
132
  >
132
133
  <span
@@ -165,12 +166,12 @@ export default function MatrixDropdown({
165
166
  key={col.id}
166
167
  style={{
167
168
  padding: '12px 16px', cursor: isDisabled ? 'not-allowed' : 'pointer', transition: 'background-color 0.15s',
168
- backgroundColor: isSelected ? '#fdf2f8' : '#fff',
169
+ backgroundColor: isSelected ? 'var(--cfm-mcq-selected-bg)' : '#fff',
169
170
  opacity: isDisabled ? 0.5 : 1,
170
171
  display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px',
171
172
  }}
172
- onMouseEnter={e => { if (!isDisabled) e.currentTarget.style.backgroundColor = '#fdf2f8'; }}
173
- onMouseLeave={e => { if (!isDisabled) e.currentTarget.style.backgroundColor = isSelected ? '#fdf2f8' : '#fff'; }}
173
+ onMouseEnter={e => { if (!isDisabled) e.currentTarget.style.backgroundColor = 'var(--cfm-mcq-selected-bg)'; }}
174
+ onMouseLeave={e => { if (!isDisabled) e.currentTarget.style.backgroundColor = isSelected ? 'var(--cfm-mcq-selected-bg)' : '#fff'; }}
174
175
  onClick={e => {
175
176
  e.preventDefault();
176
177
  if (isDisabled) return;
@@ -187,8 +188,8 @@ export default function MatrixDropdown({
187
188
  <div style={{
188
189
  display: 'flex', alignItems: 'center', justifyContent: 'center',
189
190
  width: '18px', height: '18px', borderRadius: '4px',
190
- border: isSelected ? '2px solid #e20074' : '2px solid #d1d5db',
191
- backgroundColor: isSelected ? '#e20074' : '#fff',
191
+ border: isSelected ? `2px solid ${focusRingVar}` : '2px solid #d1d5db',
192
+ backgroundColor: isSelected ? cellSelectedVar : '#fff',
192
193
  flexShrink: 0, transition: 'all 0.15s',
193
194
  }}>
194
195
  {isSelected && (
@@ -198,11 +199,11 @@ export default function MatrixDropdown({
198
199
  )}
199
200
  </div>
200
201
  )}
201
- <span style={{ fontSize: '15px', fontWeight: isSelected ? 600 : 400, color: isSelected ? '#e20074' : '#374151', userSelect: 'none' }}
202
+ <span style={{ fontSize: '15px', fontWeight: isSelected ? 600 : 400, color: isSelected ? focusRingVar : '#374151', userSelect: 'none' }}
202
203
  dangerouslySetInnerHTML={{ __html: col.label }} />
203
204
  </div>
204
205
  {!multiple && isSelected && (
205
- <svg style={{ width: '18px', height: '18px', color: '#e20074', flexShrink: 0 }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
206
+ <svg style={{ width: '18px', height: '18px', color: focusRingVar, flexShrink: 0 }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
206
207
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
207
208
  </svg>
208
209
  )}
@@ -140,29 +140,6 @@ export default function Question({
140
140
 
141
141
  {question.type === QUESTION_TYPE.NPS_SCALE && (
142
142
  <div className="space-y-3">
143
- {(question.minLabel || question.midLabel || question.maxLabel) && (
144
- <div className="relative w-full h-6 text-xs font-semibold text-gray-500">
145
- <span
146
- className="absolute left-0 top-0 max-w-[30%] text-left leading-tight"
147
- dangerouslySetInnerHTML={{ __html: question.minLabel ?? '' }}
148
- />
149
- {question.midLabel && (
150
- <span
151
- className="absolute top-0 -translate-x-1/2 text-center max-w-[40%] leading-tight"
152
- style={{
153
- left: `${question.midLabelIndex !== undefined && question.options.length > 1
154
- ? (question.midLabelIndex / (question.options.length - 1)) * 100
155
- : 50}%`,
156
- }}
157
- dangerouslySetInnerHTML={{ __html: question.midLabel }}
158
- />
159
- )}
160
- <span
161
- className="absolute right-0 top-0 max-w-[30%] text-right leading-tight"
162
- dangerouslySetInnerHTML={{ __html: question.maxLabel ?? '' }}
163
- />
164
- </div>
165
- )}
166
143
  <RatingScale
167
144
  questionId={question.id}
168
145
  options={question.options}
@@ -170,6 +147,10 @@ export default function Question({
170
147
  onSelect={onSelect}
171
148
  variant="nps"
172
149
  buttonStyle={question.buttonStyle}
150
+ minLabel={question.minLabel}
151
+ midLabel={question.midLabel}
152
+ maxLabel={question.maxLabel}
153
+ midLabelIndex={question.midLabelIndex}
173
154
  />
174
155
  </div>
175
156
  )}
@@ -25,6 +25,7 @@ import {
25
25
  verticalListSortingStrategy,
26
26
  } from '@dnd-kit/sortable';
27
27
  import { CSS } from '@dnd-kit/utilities';
28
+ import { focusRingVar } from '@/lib/surveyUi/selectionStyles';
28
29
  import type {
29
30
  RankOrderAnswers,
30
31
  RankOrderOption,
@@ -119,11 +120,37 @@ function RankOrderOptionRow({
119
120
  rankBadge,
120
121
  }: RankOrderOptionRowProps) {
121
122
  return (
122
- <div className="flex items-center gap-3 rounded-lg border border-[#e5e5e5] bg-white px-4 py-3 transition-colors hover:bg-gray-50/50">
123
+ <div
124
+ style={{
125
+ display: 'flex',
126
+ alignItems: 'center',
127
+ gap: 'var(--cfm-rank-item-gap, 12px)',
128
+ borderRadius: '8px',
129
+ border: '1px solid #e5e5e5',
130
+ backgroundColor: 'var(--cfm-rank-item-bg, #fff)',
131
+ padding: 'var(--cfm-rank-item-padding, 12px 16px)',
132
+ transition: 'background-color 0.15s',
133
+ }}
134
+ className="hover:bg-gray-50/50"
135
+ >
123
136
  {rankSelect}
124
137
  {dragHandle}
125
138
  {rankBadge !== undefined && (
126
- <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[#fdf2f8] text-sm font-semibold text-[#e20074]">
139
+ <span
140
+ style={{
141
+ display: 'flex',
142
+ height: '28px',
143
+ width: '28px',
144
+ flexShrink: 0,
145
+ alignItems: 'center',
146
+ justifyContent: 'center',
147
+ borderRadius: '9999px',
148
+ backgroundColor: 'var(--cfm-rank-badge)',
149
+ fontSize: '14px',
150
+ fontWeight: 600,
151
+ color: '#fff',
152
+ }}
153
+ >
127
154
  {rankBadge}
128
155
  </span>
129
156
  )}
@@ -147,7 +174,7 @@ function DropdownRankLayout({
147
174
  };
148
175
 
149
176
  return (
150
- <div className="space-y-3">
177
+ <div className="space-y-3" style={{ display: 'flex', flexDirection: 'column', gap: 'var(--cfm-rank-item-gap, 12px)' }}>
151
178
  {question.options.map(option => {
152
179
  const currentRank = rankAnswers[option.id];
153
180
 
@@ -161,7 +188,19 @@ function DropdownRankLayout({
161
188
  aria-label={`Rank for ${option.optionLabel || option.id}`}
162
189
  value={currentRank ?? ''}
163
190
  onChange={event => handleRankChange(option.id, event.target.value)}
164
- className="h-10 w-16 shrink-0 rounded border border-gray-300 bg-white px-2 text-sm outline-none focus:border-[#e20074] focus:ring-1 focus:ring-[#e20074]"
191
+ style={{
192
+ height: '40px',
193
+ width: '64px',
194
+ flexShrink: 0,
195
+ borderRadius: '4px',
196
+ border: '1px solid var(--cfm-rank-select-border, #d1d5db)',
197
+ backgroundColor: '#fff',
198
+ padding: '0 8px',
199
+ fontSize: '14px',
200
+ outline: 'none',
201
+ }}
202
+ onFocus={e => { e.currentTarget.style.borderColor = focusRingVar; }}
203
+ onBlur={e => { e.currentTarget.style.borderColor = 'var(--cfm-rank-select-border, #d1d5db)'; }}
165
204
  >
166
205
  <option value="">-</option>
167
206
  {rankChoices.map(rank => (
@@ -218,7 +257,8 @@ function SortableRankItem({
218
257
  <button
219
258
  type="button"
220
259
  aria-label={`Drag to reorder ${option.optionLabel || option.id}`}
221
- className="flex h-10 w-8 shrink-0 cursor-grab items-center justify-center rounded text-gray-500 hover:bg-gray-100 active:cursor-grabbing"
260
+ className="flex h-10 w-8 shrink-0 cursor-grab items-center justify-center rounded active:cursor-grabbing"
261
+ style={{ color: 'var(--cfm-rank-handle, #6b7280)' }}
222
262
  {...attributes}
223
263
  {...listeners}
224
264
  >
@@ -316,7 +356,7 @@ function DragDropRankLayout({
316
356
  return (
317
357
  <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
318
358
  <SortableContext items={orderedOptionIds} strategy={verticalListSortingStrategy}>
319
- <div className="space-y-3">
359
+ <div className="space-y-3" style={{ display: 'flex', flexDirection: 'column', gap: 'var(--cfm-rank-item-gap, 12px)' }}>
320
360
  {orderedOptionIds.map((optionId, index) => {
321
361
  const option = question.options.find(item => item.id === optionId);
322
362
  if (!option) {