@explorer02/cfm-survey-sdk 0.2.9 → 0.3.0

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 +41 -38
  2. package/dist/cli/index.mjs +39 -36
  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 +1 -2
  6. package/templates/docs/00-integration/component-checklist.md +0 -1
  7. package/templates/docs/00-integration/ui-customization-wizard.md +38 -29
  8. package/templates/docs/00-integration/wizard-chrome-contract.md +1 -11
  9. package/templates/docs/00-integration/wizard-config-handoff.md +19 -11
  10. package/templates/docs/00-integration/wizard-post-customize-build.md +0 -6
  11. package/templates/docs/00-integration/wizard-question-type-styling.md +3 -3
  12. package/templates/docs/00-integration/wizard-troubleshooting.md +4 -20
  13. package/templates/docs/index.md +1 -1
  14. package/templates/docs/templates/CsatMatrixScale.tsx +59 -53
  15. package/templates/docs/templates/CustomSliderTrack.tsx +8 -17
  16. package/templates/docs/templates/FileUploadScale.tsx +4 -12
  17. package/templates/docs/templates/Header.tsx +48 -32
  18. package/templates/docs/templates/HeatmapScale.tsx +2 -5
  19. package/templates/docs/templates/LikertMatrixScale.tsx +45 -57
  20. package/templates/docs/templates/MatrixDropdown.tsx +11 -12
  21. package/templates/docs/templates/Question.tsx +24 -4
  22. package/templates/docs/templates/RankOrderScale.tsx +9 -42
  23. package/templates/docs/templates/RatingScale.tsx +0 -38
  24. package/templates/docs/templates/SliderMatrixScale.tsx +3 -4
  25. package/templates/docs/templates/verify-agent-build.sh +0 -17
  26. package/templates/preview-harness/preview-bridge.inline.js +17 -14
  27. package/templates/preview-harness/previewPages.js +23 -15
  28. package/templates/preview-harness/previewPages.ts +7 -5
  29. package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +22 -14
  30. package/templates/preview-harness/vite-app/src/fixtures/questions.ts +13 -2
  31. package/templates/preview-harness/vite-app/src/preview-live-overrides.css +6 -11
  32. package/templates/previewBridge.ts +5 -5
  33. package/templates/survey-theme.css +17 -17
  34. package/templates/wizard-dist/assets/{PreviewMock-CysRws9G.js → PreviewMock-Bax7oRAL.js} +1 -1
  35. package/templates/wizard-dist/assets/{TypePanel-DWX1LL3q.js → TypePanel-D2t4FPSd.js} +1 -1
  36. package/templates/wizard-dist/assets/index-BhWM50Yu.css +1 -0
  37. package/templates/wizard-dist/assets/index-c5lka74l.js +34 -0
  38. package/templates/wizard-dist/index.html +2 -2
  39. package/templates/wizard-dist/assets/index-DIuc7AU2.js +0 -34
  40. package/templates/wizard-dist/assets/index-pCM8n0FT.css +0 -1
@@ -7,7 +7,6 @@
7
7
  */
8
8
 
9
9
  import React from 'react';
10
- import { focusRingVar, cellSelectedVar } from '@/lib/surveyUi/selectionStyles';
11
10
  import { getEmojiForIndex } from './surveyUiIcons';
12
11
 
13
12
  type CustomSliderTrackProps = {
@@ -51,8 +50,7 @@ export function CustomSliderTrack({
51
50
  ? tooltipLabel.replace(/<[^>]*>/g, '')
52
51
  : String(Math.round(value));
53
52
 
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;
53
+ const themeColor = disabled ? '#9ca3af' : 'var(--cfm-input-focus-ring, var(--cfm-primary))';
56
54
 
57
55
  return (
58
56
  <div
@@ -62,12 +60,12 @@ export function CustomSliderTrack({
62
60
  >
63
61
  <div style={{
64
62
  position: 'absolute', top: '50%', left: 0, right: 0, transform: 'translateY(-50%)',
65
- height: '4px', borderRadius: '2px', backgroundColor: 'var(--cfm-slider-tick, #e5e7eb)',
63
+ height: '4px', borderRadius: '2px', backgroundColor: 'var(--cfm-slider-track, #e5e7eb)',
66
64
  }} />
67
65
 
68
66
  <div style={{
69
67
  position: 'absolute', top: '50%', left: 0, width: `${percentage}%`, transform: 'translateY(-50%)',
70
- height: '4px', borderRadius: '2px', backgroundColor: trackColor,
68
+ height: '4px', borderRadius: '2px', backgroundColor: themeColor,
71
69
  transition: isDragging ? 'none' : 'width 0.15s ease-in-out',
72
70
  }} />
73
71
 
@@ -78,7 +76,7 @@ export function CustomSliderTrack({
78
76
  key={idx}
79
77
  style={{
80
78
  position: 'absolute', top: '50%', left: `${tickPct}%`, transform: 'translate(-50%, -50%)',
81
- width: '1px', height: '8px', backgroundColor: 'var(--cfm-slider-tick, #d1d5db)', pointerEvents: 'none',
79
+ width: '1px', height: '8px', backgroundColor: '#d1d5db', pointerEvents: 'none',
82
80
  }}
83
81
  />
84
82
  );
@@ -115,22 +113,15 @@ export function CustomSliderTrack({
115
113
  ) : (
116
114
  <div style={{
117
115
  position: 'absolute', top: '50%', left: `${percentage}%`, transform: 'translate(-50%, -50%)',
118
- width: '18px', height: '18px', borderRadius: '50%',
119
- border: `2.5px solid ${thumbBorderColor}`,
120
- backgroundColor: cellSelectedVar,
121
- display: 'flex', alignItems: 'center', justifyContent: 'center',
116
+ width: '18px', height: '18px', borderRadius: '50%', backgroundColor: '#ffffff',
117
+ border: `2.5px solid ${themeColor}`,
122
118
  boxShadow: (isHovered || isDragging) && !disabled
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)'
119
+ ? '0 0 0 6px rgba(226, 0, 116, 0.2), 0 1px 3px rgba(0,0,0,0.2)'
124
120
  : '0 1px 3px rgba(0,0,0,0.2)',
125
121
  pointerEvents: 'none',
126
122
  transition: isDragging ? 'none' : 'left 0.15s ease-in-out, box-shadow 0.15s, border-color 0.2s',
127
123
  boxSizing: 'border-box',
128
- }}>
129
- <div style={{
130
- width: '33%', height: '33%', borderRadius: '50%',
131
- backgroundColor: 'var(--cfm-slider-thumb, var(--cfm-primary))',
132
- }} />
133
- </div>
124
+ }} />
134
125
  )}
135
126
 
136
127
  {(isHovered || isDragging) && !disabled && (
@@ -8,7 +8,6 @@
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';
12
11
 
13
12
  type UploadedFileAnswer = {
14
13
  id: string;
@@ -87,15 +86,8 @@ function FileUploadDropzone({
87
86
  onDrag: (e: React.DragEvent) => void;
88
87
  onDrop: (e: React.DragEvent) => void;
89
88
  }) {
90
- const dropzoneStyleAttr =
91
- typeof document !== 'undefined'
92
- ? document.documentElement.getAttribute('data-cfm-dropzone-style') ?? undefined
93
- : undefined;
94
-
95
89
  return (
96
90
  <div
97
- className="cfm-dropzone"
98
- {...(dropzoneStyleAttr ? { 'data-cfm-dropzone-style': dropzoneStyleAttr } : {})}
99
91
  onDragEnter={onDrag}
100
92
  onDragOver={onDrag}
101
93
  onDragLeave={onDrag}
@@ -106,10 +98,10 @@ function FileUploadDropzone({
106
98
  flexDirection: 'column',
107
99
  alignItems: 'center',
108
100
  justifyContent: 'center',
109
- border: `2px var(--cfm-upload-border-style, dashed) ${isDragActive ? focusRingVar : 'var(--cfm-upload-border-color)'}`,
110
- borderRadius: '12px',
111
- backgroundColor: isDragActive ? 'var(--cfm-mcq-selected-bg)' : 'var(--cfm-upload-fill)',
112
- padding: 'var(--cfm-upload-padding)',
101
+ border: `2px ${isDragActive ? 'solid' : 'var(--cfm-upload-border-style, dashed)'} var(--cfm-upload-border-color, #d1d5db)`,
102
+ borderRadius: 'var(--cfm-border-radius, 12px)',
103
+ backgroundColor: isDragActive ? 'var(--cfm-mcq-selected-bg, #fdf2f8)' : 'var(--cfm-upload-fill, #f9fafb)',
104
+ padding: 'var(--cfm-upload-padding, 40px 24px)',
113
105
  cursor: disabled ? 'not-allowed' : 'pointer',
114
106
  opacity: disabled ? 0.6 : 1,
115
107
  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
- * Layout locked: logo left, name after (flex row only).
5
+ * When no logo file: text fallback via data-cfm-logo-text (company name or empty).
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,30 +33,42 @@ export default function Header({ embedded = false }: HeaderProps) {
33
33
  }}
34
34
  >
35
35
  <div
36
- className="cfm-header-inner flex h-full w-full items-center"
36
+ className="cfm-header-inner grid h-full w-full items-center"
37
37
  style={{
38
+ gridTemplateColumns: '1fr 1fr 1fr',
38
39
  padding: `0 var(--cfm-header-padding-x, 40px)`,
39
40
  }}
40
41
  >
41
42
  <div
42
- className="cfm-header-brand flex min-w-0 items-center"
43
+ className="cfm-header-brand flex items-center"
43
44
  style={{
45
+ gridColumn: 'var(--cfm-header-logo-col, 1)',
46
+ justifySelf: 'var(--cfm-header-logo-justify, start)',
44
47
  gap: 'var(--cfm-header-brand-gap, 16px)',
45
48
  }}
46
49
  >
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
- />
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: 'transparent',
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
+ )}
60
72
 
61
73
  <span
62
74
  data-cfm-logo-text
@@ -71,22 +83,26 @@ export default function Header({ embedded = false }: HeaderProps) {
71
83
  {seedCompany}
72
84
  </span>
73
85
 
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
- )}
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>
90
106
  </div>
91
107
  </div>
92
108
  </header>
@@ -29,8 +29,8 @@ function HeatmapSpot({ spot }: { spot: { id: string; x: number; y: number } }) {
29
29
  width: 'var(--cfm-heatmap-pin-size, 16px)',
30
30
  height: 'var(--cfm-heatmap-pin-size, 16px)',
31
31
  transform: 'translate(-50%, -50%)',
32
- backgroundColor: 'var(--cfm-heatmap-pin)',
33
- border: '2px solid var(--cfm-heatmap-pin-border, white)',
32
+ backgroundColor: 'var(--cfm-heatmap-pin, #F97316)',
33
+ border: '2px solid var(--cfm-heatmap-pin-border, #fff)',
34
34
  boxShadow: '0 1px 3px rgba(0,0,0,0.25)',
35
35
  }}
36
36
  />
@@ -63,9 +63,6 @@ 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
- }
69
66
  }
70
67
 
71
68
  function handleImageLoad() {
@@ -1,14 +1,10 @@
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';
10
3
  import { columnSubmitValue } from './surveyUiScaleUtils';
11
4
  import MatrixDropdown from './MatrixDropdown';
5
+ import { matrixColumnLabelStyle } from '@/lib/surveyUi/labelStyles';
6
+
7
+ const MATRIX_ROW_LABEL_WIDTH = 'var(--cfm-matrix-row-width, 180px)';
12
8
 
13
9
  type LikertMatrixScaleProps = {
14
10
  question: CfmMatrixQuestion;
@@ -118,8 +114,8 @@ function LikertMatrixCarouselLayout({
118
114
  <div style={{
119
115
  display: 'flex', alignItems: 'center', justifyContent: 'center',
120
116
  width: '18px', height: '18px', borderRadius: '4px',
121
- border: selected ? `2px solid ${focusRingVar}` : '2px solid #d1d5db',
122
- backgroundColor: selected ? cellSelectedVar : '#fff',
117
+ border: selected ? '2px solid #e20074' : '2px solid #d1d5db',
118
+ backgroundColor: selected ? '#e20074' : '#fff',
123
119
  flexShrink: 0, transition: 'all 0.15s'
124
120
  }}>
125
121
  {selected && (
@@ -129,8 +125,13 @@ function LikertMatrixCarouselLayout({
129
125
  )}
130
126
  </div>
131
127
  ) : (
132
- <div style={matrixRadioRingStyle(selected)}>
133
- {selected && <div style={matrixRadioDotStyle(selected, '10px')} />}
128
+ <div style={{
129
+ display: 'flex', width: '18px', height: '18px', flexShrink: 0,
130
+ alignItems: 'center', justifyContent: 'center', borderRadius: '50%',
131
+ border: selected ? '2px solid #e20074' : '1.5px solid #9ca3af',
132
+ backgroundColor: '#fff',
133
+ }}>
134
+ {selected && <div style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: '#e20074' }} />}
134
135
  </div>
135
136
  )}
136
137
  <span style={{ fontSize: '15px', fontWeight: 400, color: '#374151', userSelect: 'none' }} dangerouslySetInnerHTML={{ __html: col.label }} />
@@ -208,7 +209,7 @@ function LikertMatrixGridLayout({
208
209
  <div style={{ display: 'flex', flexDirection: 'column', marginBottom: '16px' }}>
209
210
  {hasLabels && (
210
211
  <div style={{ display: 'flex', alignItems: 'flex-end', minHeight: '24px', marginBottom: '8px' }}>
211
- <div style={{ flex: `0 0 var(--cfm-matrix-bipolar-width, 25%)`, paddingRight: '16px', boxSizing: 'border-box' }} />
212
+ <div style={{ flex: '0 0 25%', paddingRight: '16px', boxSizing: 'border-box' }} />
212
213
  <div style={{ flex: 1, position: 'relative', height: '20px' }}>
213
214
  {labels.map((lbl, idx) => {
214
215
  let leftPos = '50%';
@@ -226,12 +227,12 @@ function LikertMatrixGridLayout({
226
227
  );
227
228
  })}
228
229
  </div>
229
- <div style={{ flex: `0 0 var(--cfm-matrix-bipolar-width, 25%)`, paddingLeft: '16px', boxSizing: 'border-box' }} />
230
+ <div style={{ flex: '0 0 25%', paddingLeft: '16px', boxSizing: 'border-box' }} />
230
231
  </div>
231
232
  )}
232
233
  {showColumnHeaders && (
233
234
  <div style={{ display: 'flex', alignItems: 'flex-end', minHeight: '24px' }}>
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' }} />
235
+ <div style={{ flex: isBipolar ? '0 0 25%' : `0 0 ${MATRIX_ROW_LABEL_WIDTH}`, paddingRight: '16px', boxSizing: 'border-box' }} />
235
236
  <div style={{ flex: 1, display: 'flex' }}>
236
237
  <div style={{ flex: 1, display: 'grid', gridTemplateColumns: `repeat(${gridCols.length}, 1fr)` }}>
237
238
  {gridCols.map((col) => (
@@ -242,7 +243,7 @@ function LikertMatrixGridLayout({
242
243
  ))}
243
244
  </div>
244
245
  </div>
245
- {isBipolar && <div style={{ flex: `0 0 var(--cfm-matrix-bipolar-width, 25%)`, paddingLeft: '16px', boxSizing: 'border-box' }} />}
246
+ {isBipolar && <div style={{ flex: '0 0 25%', paddingLeft: '16px', boxSizing: 'border-box' }} />}
246
247
  </div>
247
248
  )}
248
249
  </div>
@@ -260,10 +261,10 @@ function LikertMatrixGridLayout({
260
261
  {repeatColumnHeaders && renderHeader()}
261
262
  <div style={{
262
263
  display: 'flex', alignItems: 'center', borderRadius: '8px',
263
- padding: '12px 0', backgroundColor: !repeatColumnHeaders && rowIdx % 2 === 0 ? 'var(--cfm-zebra-row)' : '#fff',
264
+ padding: '12px 0', backgroundColor: !repeatColumnHeaders && rowIdx % 2 === 0 ? 'rgba(249, 250, 251, 0.8)' : '#fff',
264
265
  boxSizing: 'border-box'
265
266
  }}>
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' }}>
267
+ <div style={{ flex: isBipolar ? '0 0 25%' : `0 0 ${MATRIX_ROW_LABEL_WIDTH}`, padding: '0 16px', boxSizing: 'border-box' }}>
267
268
  <span style={{ fontSize: '14px', fontWeight: 500, color: '#111827', lineHeight: 1.4, wordWrap: 'break-word', display: 'block' }}
268
269
  dangerouslySetInnerHTML={{ __html: transposeTable ? ('label' in rowItem ? rowItem.label : '') : ('statementText' in rowItem ? rowItem.statementText : '') }} />
269
270
  </div>
@@ -281,45 +282,32 @@ function LikertMatrixGridLayout({
281
282
 
282
283
  return (
283
284
  <div key={colItem.id} style={{ display: 'flex', justifyContent: 'center' }}>
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>
285
+ <label style={{
286
+ cursor: isDisabled ? 'not-allowed' : 'pointer',
287
+ opacity: isDisabled ? 0.4 : 1,
288
+ padding: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center'
289
+ }}>
290
+ <input
291
+ type={isMultiple ? "checkbox" : "radio"}
292
+ checked={selected}
293
+ disabled={isDisabled}
294
+ onClick={(e) => {
295
+ if (!isMultiple && selected) {
296
+ onCellSelect(actualRowId, undefined);
297
+ }
298
+ }}
299
+ onChange={(e) => {
300
+ if (isMultiple || !selected) {
301
+ onCellSelect(actualRowId, actualColValue);
302
+ }
303
+ }}
304
+ style={{
305
+ width: '18px', height: '18px', accentColor: 'var(--cfm-input-focus-ring)',
306
+ cursor: isDisabled ? 'not-allowed' : 'pointer',
307
+ ...(isMultiple ? { borderRadius: '4px' } : {})
308
+ }}
309
+ />
310
+ </label>
323
311
  </div>
324
312
  );
325
313
  })}
@@ -327,7 +315,7 @@ function LikertMatrixGridLayout({
327
315
  </div>
328
316
 
329
317
  {isBipolar && (
330
- <div style={{ flex: '0 0 var(--cfm-matrix-bipolar-width, 25%)', padding: '0 16px', textAlign: 'right', boxSizing: 'border-box' }}>
318
+ <div style={{ flex: '0 0 25%', padding: '0 16px', textAlign: 'right', boxSizing: 'border-box' }}>
331
319
  <span style={{ fontSize: '14px', fontWeight: 500, color: '#111827', lineHeight: 1.4, wordWrap: 'break-word', display: 'block' }}
332
320
  dangerouslySetInnerHTML={{ __html: transposeTable ? '' : ('oppositeStatementText' in rowItem ? rowItem.oppositeStatementText ?? '' : '') }} />
333
321
  </div>
@@ -9,7 +9,6 @@
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';
13
12
 
14
13
  type MatrixDropdownProps = {
15
14
  options: ScaleColumn[];
@@ -73,9 +72,9 @@ export default function MatrixDropdown({
73
72
  style={{
74
73
  display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '8px',
75
74
  width: '100%', padding: '10px 14px', backgroundColor: '#fff',
76
- border: `1px solid ${isOpen ? focusRingVar : '#d1d5db'}`, borderRadius: '8px',
75
+ border: `1px solid ${isOpen ? '#e20074' : '#d1d5db'}`, borderRadius: '8px',
77
76
  cursor: 'pointer', transition: 'all 0.2s', minHeight: '44px',
78
- boxShadow: isOpen ? '0 0 0 3px var(--cfm-mcq-selected-bg)' : '0 1px 2px rgba(0,0,0,0.05)',
77
+ boxShadow: isOpen ? '0 0 0 3px rgba(226, 0, 116, 0.1)' : '0 1px 2px rgba(0,0,0,0.05)',
79
78
  }}
80
79
  >
81
80
  {multiValues.length === 0 ? (
@@ -125,9 +124,9 @@ export default function MatrixDropdown({
125
124
  style={{
126
125
  display: 'flex', alignItems: 'center', justifyContent: 'space-between',
127
126
  width: '100%', padding: '14px 16px', backgroundColor: '#fff',
128
- border: `1px solid ${isOpen ? focusRingVar : '#d1d5db'}`, borderRadius: '8px',
127
+ border: `1px solid ${isOpen ? '#e20074' : '#d1d5db'}`, borderRadius: '8px',
129
128
  cursor: 'pointer', transition: 'all 0.2s',
130
- boxShadow: isOpen ? '0 0 0 3px var(--cfm-mcq-selected-bg)' : '0 1px 2px rgba(0,0,0,0.05)',
129
+ boxShadow: isOpen ? '0 0 0 3px rgba(226, 0, 116, 0.1)' : '0 1px 2px rgba(0,0,0,0.05)',
131
130
  }}
132
131
  >
133
132
  <span
@@ -166,12 +165,12 @@ export default function MatrixDropdown({
166
165
  key={col.id}
167
166
  style={{
168
167
  padding: '12px 16px', cursor: isDisabled ? 'not-allowed' : 'pointer', transition: 'background-color 0.15s',
169
- backgroundColor: isSelected ? 'var(--cfm-mcq-selected-bg)' : '#fff',
168
+ backgroundColor: isSelected ? '#fdf2f8' : '#fff',
170
169
  opacity: isDisabled ? 0.5 : 1,
171
170
  display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px',
172
171
  }}
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'; }}
172
+ onMouseEnter={e => { if (!isDisabled) e.currentTarget.style.backgroundColor = '#fdf2f8'; }}
173
+ onMouseLeave={e => { if (!isDisabled) e.currentTarget.style.backgroundColor = isSelected ? '#fdf2f8' : '#fff'; }}
175
174
  onClick={e => {
176
175
  e.preventDefault();
177
176
  if (isDisabled) return;
@@ -188,8 +187,8 @@ export default function MatrixDropdown({
188
187
  <div style={{
189
188
  display: 'flex', alignItems: 'center', justifyContent: 'center',
190
189
  width: '18px', height: '18px', borderRadius: '4px',
191
- border: isSelected ? `2px solid ${focusRingVar}` : '2px solid #d1d5db',
192
- backgroundColor: isSelected ? cellSelectedVar : '#fff',
190
+ border: isSelected ? '2px solid #e20074' : '2px solid #d1d5db',
191
+ backgroundColor: isSelected ? '#e20074' : '#fff',
193
192
  flexShrink: 0, transition: 'all 0.15s',
194
193
  }}>
195
194
  {isSelected && (
@@ -199,11 +198,11 @@ export default function MatrixDropdown({
199
198
  )}
200
199
  </div>
201
200
  )}
202
- <span style={{ fontSize: '15px', fontWeight: isSelected ? 600 : 400, color: isSelected ? focusRingVar : '#374151', userSelect: 'none' }}
201
+ <span style={{ fontSize: '15px', fontWeight: isSelected ? 600 : 400, color: isSelected ? '#e20074' : '#374151', userSelect: 'none' }}
203
202
  dangerouslySetInnerHTML={{ __html: col.label }} />
204
203
  </div>
205
204
  {!multiple && isSelected && (
206
- <svg style={{ width: '18px', height: '18px', color: focusRingVar, flexShrink: 0 }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
205
+ <svg style={{ width: '18px', height: '18px', color: '#e20074', flexShrink: 0 }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
207
206
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
208
207
  </svg>
209
208
  )}
@@ -19,6 +19,7 @@ import { FileUploadScale } from './FileUploadScale';
19
19
  import { HeatmapScale } from './HeatmapScale';
20
20
  import { RankOrderScale } from './RankOrderScale';
21
21
  import RatingScale from './RatingScale';
22
+ import { hintLabelStyle } from '@/lib/surveyUi/labelStyles';
22
23
 
23
24
  type QuestionProps = {
24
25
  question: SurveyQuestion;
@@ -140,6 +141,29 @@ export default function Question({
140
141
 
141
142
  {question.type === QUESTION_TYPE.NPS_SCALE && (
142
143
  <div className="space-y-3">
144
+ {(question.minLabel || question.midLabel || question.maxLabel) && (
145
+ <div className="relative w-full h-6 text-xs font-semibold" style={hintLabelStyle()}>
146
+ <span
147
+ className="absolute left-0 top-0 max-w-[30%] text-left leading-tight"
148
+ dangerouslySetInnerHTML={{ __html: question.minLabel ?? '' }}
149
+ />
150
+ {question.midLabel && (
151
+ <span
152
+ className="absolute top-0 -translate-x-1/2 text-center max-w-[40%] leading-tight"
153
+ style={{
154
+ left: `${question.midLabelIndex !== undefined && question.options.length > 1
155
+ ? (question.midLabelIndex / (question.options.length - 1)) * 100
156
+ : 50}%`,
157
+ }}
158
+ dangerouslySetInnerHTML={{ __html: question.midLabel }}
159
+ />
160
+ )}
161
+ <span
162
+ className="absolute right-0 top-0 max-w-[30%] text-right leading-tight"
163
+ dangerouslySetInnerHTML={{ __html: question.maxLabel ?? '' }}
164
+ />
165
+ </div>
166
+ )}
143
167
  <RatingScale
144
168
  questionId={question.id}
145
169
  options={question.options}
@@ -147,10 +171,6 @@ export default function Question({
147
171
  onSelect={onSelect}
148
172
  variant="nps"
149
173
  buttonStyle={question.buttonStyle}
150
- minLabel={question.minLabel}
151
- midLabel={question.midLabel}
152
- maxLabel={question.maxLabel}
153
- midLabelIndex={question.midLabelIndex}
154
174
  />
155
175
  </div>
156
176
  )}
@@ -25,7 +25,6 @@ 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';
29
28
  import type {
30
29
  RankOrderAnswers,
31
30
  RankOrderOption,
@@ -121,35 +120,15 @@ function RankOrderOptionRow({
121
120
  }: RankOrderOptionRowProps) {
122
121
  return (
123
122
  <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"
123
+ className="flex items-center gap-3 rounded-lg border border-[#e5e5e5] px-4 py-3 transition-colors"
124
+ style={{ backgroundColor: 'var(--cfm-rank-item-bg, #fff)', padding: 'var(--cfm-rank-item-padding, 12px 16px)', marginBottom: 'var(--cfm-rank-item-gap, 12px)' }}
135
125
  >
136
126
  {rankSelect}
137
127
  {dragHandle}
138
128
  {rankBadge !== undefined && (
139
129
  <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
- }}
130
+ className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-sm font-semibold text-white"
131
+ style={{ backgroundColor: 'var(--cfm-rank-badge, var(--cfm-primary))' }}
153
132
  >
154
133
  {rankBadge}
155
134
  </span>
@@ -174,7 +153,7 @@ function DropdownRankLayout({
174
153
  };
175
154
 
176
155
  return (
177
- <div className="space-y-3" style={{ display: 'flex', flexDirection: 'column', gap: 'var(--cfm-rank-item-gap, 12px)' }}>
156
+ <div className="space-y-3">
178
157
  {question.options.map(option => {
179
158
  const currentRank = rankAnswers[option.id];
180
159
 
@@ -188,19 +167,8 @@ function DropdownRankLayout({
188
167
  aria-label={`Rank for ${option.optionLabel || option.id}`}
189
168
  value={currentRank ?? ''}
190
169
  onChange={event => handleRankChange(option.id, event.target.value)}
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)'; }}
170
+ className="h-10 w-16 shrink-0 rounded border bg-white px-2 text-sm outline-none"
171
+ style={{ borderColor: 'var(--cfm-rank-select-border, #d1d5db)' }}
204
172
  >
205
173
  <option value="">-</option>
206
174
  {rankChoices.map(rank => (
@@ -257,8 +225,7 @@ function SortableRankItem({
257
225
  <button
258
226
  type="button"
259
227
  aria-label={`Drag to reorder ${option.optionLabel || option.id}`}
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)' }}
228
+ 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"
262
229
  {...attributes}
263
230
  {...listeners}
264
231
  >
@@ -356,7 +323,7 @@ function DragDropRankLayout({
356
323
  return (
357
324
  <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
358
325
  <SortableContext items={orderedOptionIds} strategy={verticalListSortingStrategy}>
359
- <div className="space-y-3" style={{ display: 'flex', flexDirection: 'column', gap: 'var(--cfm-rank-item-gap, 12px)' }}>
326
+ <div className="space-y-3">
360
327
  {orderedOptionIds.map((optionId, index) => {
361
328
  const option = question.options.find(item => item.id === optionId);
362
329
  if (!option) {