@explorer02/cfm-survey-sdk 0.2.9 → 0.3.1

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 (47) 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/client-lib-folder.md +1 -0
  7. package/templates/docs/00-integration/component-checklist.md +0 -1
  8. package/templates/docs/00-integration/ui-customization-wizard.md +38 -29
  9. package/templates/docs/00-integration/wizard-chrome-contract.md +3 -12
  10. package/templates/docs/00-integration/wizard-config-handoff.md +19 -11
  11. package/templates/docs/00-integration/wizard-post-customize-build.md +0 -6
  12. package/templates/docs/00-integration/wizard-preview-build-guide.md +17 -0
  13. package/templates/docs/00-integration/wizard-question-type-styling.md +3 -3
  14. package/templates/docs/00-integration/wizard-troubleshooting.md +4 -20
  15. package/templates/docs/01-components/03-rating-scale.md +8 -3
  16. package/templates/docs/02-reference/value-derivation.md +2 -0
  17. package/templates/docs/03-ui-specs/01-rating.md +15 -1
  18. package/templates/docs/index.md +1 -1
  19. package/templates/docs/templates/CsatMatrixScale.tsx +59 -53
  20. package/templates/docs/templates/CustomSliderTrack.tsx +8 -17
  21. package/templates/docs/templates/FileUploadScale.tsx +4 -12
  22. package/templates/docs/templates/Header.tsx +49 -32
  23. package/templates/docs/templates/HeatmapScale.tsx +2 -5
  24. package/templates/docs/templates/LikertMatrixScale.tsx +45 -57
  25. package/templates/docs/templates/MatrixDropdown.tsx +11 -12
  26. package/templates/docs/templates/Question.tsx +24 -4
  27. package/templates/docs/templates/RankOrderScale.tsx +9 -42
  28. package/templates/docs/templates/RatingScale.tsx +73 -75
  29. package/templates/docs/templates/SliderMatrixScale.tsx +3 -4
  30. package/templates/docs/templates/selectionStyles.ts +3 -2
  31. package/templates/docs/templates/surveyUiScaleUtils.ts +15 -1
  32. package/templates/docs/templates/verify-agent-build.sh +0 -17
  33. package/templates/preview-harness/preview-bridge.inline.js +61 -14
  34. package/templates/preview-harness/previewPages.js +84 -48
  35. package/templates/preview-harness/previewPages.ts +7 -5
  36. package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +22 -14
  37. package/templates/preview-harness/vite-app/src/fixtures/questions.ts +13 -2
  38. package/templates/preview-harness/vite-app/src/preview-live-overrides.css +6 -11
  39. package/templates/previewBridge.ts +38 -5
  40. package/templates/survey-theme.css +17 -17
  41. package/templates/wizard-dist/assets/{PreviewMock-CysRws9G.js → PreviewMock-Bax7oRAL.js} +1 -1
  42. package/templates/wizard-dist/assets/{TypePanel-DWX1LL3q.js → TypePanel-D2t4FPSd.js} +1 -1
  43. package/templates/wizard-dist/assets/index-BhWM50Yu.css +1 -0
  44. package/templates/wizard-dist/assets/index-c5lka74l.js +34 -0
  45. package/templates/wizard-dist/index.html +2 -2
  46. package/templates/wizard-dist/assets/index-DIuc7AU2.js +0 -34
  47. package/templates/wizard-dist/assets/index-pCM8n0FT.css +0 -1
@@ -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) {
@@ -6,8 +6,12 @@ import {
6
6
  scaleRadioDotStyle,
7
7
  scaleCellSelectedStyle,
8
8
  } from '@/lib/surveyUi/selectionStyles';
9
- import { hintLabelStyle } from '@/lib/surveyUi/labelStyles';
10
- import { getAccentColor, npsValueFromOption, starValueFromOption } from './surveyUiScaleUtils';
9
+ import {
10
+ getAccentColor,
11
+ isScaleAnswerSelected,
12
+ npsValueFromOption,
13
+ starValueFromOption,
14
+ } from './surveyUiScaleUtils';
11
15
  import { getEmojiForIndex } from './surveyUiIcons';
12
16
 
13
17
  type RatingScaleProps = {
@@ -17,25 +21,21 @@ type RatingScaleProps = {
17
21
  onSelect: (value: AnswerValue) => void;
18
22
  variant?: 'nps' | 'star';
19
23
  buttonStyle?: 'standard' | 'numbered' | 'emoji';
20
- minLabel?: string;
21
- midLabel?: string;
22
- maxLabel?: string;
23
- midLabelIndex?: number;
24
24
  };
25
25
 
26
26
  function resolveBadgeColor(index: number, value: number): string {
27
- const mode = getComputedStyle(document.documentElement)
28
- .getPropertyValue('--cfm-number-label-mode')
29
- .trim();
27
+ if (typeof document === 'undefined') return getAccentColor(value);
28
+
29
+ const root = document.documentElement;
30
+ const mode = getComputedStyle(root).getPropertyValue('--cfm-number-label-mode').trim();
30
31
  if (mode === 'monochrome') {
31
32
  return (
32
- getComputedStyle(document.documentElement).getPropertyValue('--cfm-number-mono-color').trim() ||
33
- 'var(--cfm-primary)'
33
+ getComputedStyle(root).getPropertyValue('--cfm-number-mono-color').trim() || getAccentColor(value)
34
34
  );
35
35
  }
36
36
  if (mode === 'individual') {
37
37
  return (
38
- getComputedStyle(document.documentElement).getPropertyValue(`--cfm-number-color-${index}`).trim() ||
38
+ getComputedStyle(root).getPropertyValue(`--cfm-number-color-${index}`).trim() ||
39
39
  getAccentColor(value)
40
40
  );
41
41
  }
@@ -49,10 +49,6 @@ export default function RatingScale({
49
49
  onSelect,
50
50
  variant = 'nps',
51
51
  buttonStyle = 'numbered',
52
- minLabel,
53
- midLabel,
54
- maxLabel,
55
- midLabelIndex,
56
52
  }: RatingScaleProps) {
57
53
  const resolvedOptions = options.map((option, index) => {
58
54
  const value =
@@ -61,7 +57,7 @@ export default function RatingScale({
61
57
  : npsValueFromOption(option as NpsScalePoint, index);
62
58
  const numericValue = typeof value === 'number' ? value : index;
63
59
  const color =
64
- variant === 'nps' ? resolveBadgeColor(index, numericValue) : 'var(--cfm-primary)';
60
+ variant === 'nps' ? resolveBadgeColor(index, numericValue) : 'var(--cfm-primary, #e20074)';
65
61
  return { id: option.id, label: option.optionLabel, value, color };
66
62
  });
67
63
 
@@ -71,47 +67,33 @@ export default function RatingScale({
71
67
  gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))`,
72
68
  gap: 'var(--cfm-nps-cell-gap, 2px)',
73
69
  };
74
-
75
- const hintRow = (minLabel || midLabel || maxLabel) ? (
76
- <div className="relative mb-3 h-6 w-full text-xs font-semibold">
77
- <span
78
- className="absolute left-0 top-0 max-w-[30%] text-left leading-tight"
79
- style={hintLabelStyle()}
80
- dangerouslySetInnerHTML={{ __html: minLabel ?? '' }}
81
- />
82
- {midLabel && (
83
- <span
84
- className="absolute top-0 -translate-x-1/2 max-w-[40%] text-center leading-tight"
85
- style={{
86
- ...hintLabelStyle(),
87
- left: `${midLabelIndex !== undefined && columnCount > 1
88
- ? (midLabelIndex / (columnCount - 1)) * 100
89
- : 50}%`,
90
- }}
91
- dangerouslySetInnerHTML={{ __html: midLabel }}
92
- />
93
- )}
94
- <span
95
- className="absolute right-0 top-0 max-w-[30%] text-right leading-tight"
96
- style={hintLabelStyle()}
97
- dangerouslySetInnerHTML={{ __html: maxLabel ?? '' }}
98
- />
99
- </div>
100
- ) : null;
70
+ const showNumberBadges = buttonStyle !== 'standard';
101
71
 
102
72
  if (buttonStyle === 'emoji') {
103
73
  return (
104
74
  <div role="radiogroup" className="w-full overflow-x-auto" data-cfm-nps-area>
105
- {hintRow}
106
75
  <div className="min-w-0" style={gridStyle}>
107
76
  {resolvedOptions.map((option, index) => {
108
- const isSelected = selectedValue === option.value;
77
+ const isSelected = isScaleAnswerSelected(selectedValue, option.value);
109
78
  return (
110
- <label key={`${questionId}-${option.value}`} title={option.label} className="flex cursor-pointer justify-center">
111
- <input type="radio" name={questionId} checked={isSelected} onChange={() => onSelect(option.value)} className="sr-only" />
79
+ <label
80
+ key={`${questionId}-${option.value}`}
81
+ title={option.label}
82
+ className="flex cursor-pointer justify-center"
83
+ >
84
+ <input
85
+ type="radio"
86
+ name={questionId}
87
+ checked={isSelected}
88
+ onChange={() => onSelect(option.value)}
89
+ className="sr-only"
90
+ />
112
91
  <div
113
92
  style={{
114
- ...scaleRadioRingStyle(isSelected, { pill: false, size: 'var(--cfm-csat-emoji-size, 48px)' }),
93
+ ...scaleRadioRingStyle(isSelected, {
94
+ pill: false,
95
+ size: 'var(--cfm-csat-emoji-size, 48px)',
96
+ }),
115
97
  fontSize: 'var(--cfm-csat-emoji-size, 24px)',
116
98
  }}
117
99
  >
@@ -127,29 +109,30 @@ export default function RatingScale({
127
109
 
128
110
  return (
129
111
  <div role="radiogroup" className="w-full overflow-x-auto" data-cfm-nps-area>
130
- {hintRow}
131
- <div className="mb-3 min-w-0" style={gridStyle}>
132
- {resolvedOptions.map((option, index) => {
133
- const isSelected = selectedValue === option.value;
134
- return (
135
- <div key={`${questionId}-badge-${option.value}`} className="flex justify-center">
136
- <div
137
- className="flex items-center justify-center rounded-[3px] text-[10px] font-bold text-white shadow-sm sm:text-xs"
138
- style={{
139
- width: 'var(--cfm-nps-cell-size, 24px)',
140
- height: 'var(--cfm-nps-cell-size, 24px)',
141
- ...(isSelected
142
- ? scaleCellSelectedStyle(true)
143
- : { backgroundColor: option.color }),
144
- }}
145
- title={option.label}
146
- >
147
- {option.label}
112
+ {showNumberBadges && (
113
+ <div className="mb-3 min-w-0" style={gridStyle}>
114
+ {resolvedOptions.map((option) => {
115
+ const isSelected = isScaleAnswerSelected(selectedValue, option.value);
116
+ return (
117
+ <div key={`${questionId}-badge-${option.value}`} className="flex justify-center">
118
+ <div
119
+ className="flex items-center justify-center rounded-[3px] text-[10px] font-bold text-white shadow-sm sm:text-xs"
120
+ style={{
121
+ width: 'var(--cfm-nps-cell-size, 24px)',
122
+ height: 'var(--cfm-nps-cell-size, 24px)',
123
+ ...(isSelected
124
+ ? scaleCellSelectedStyle(true)
125
+ : { backgroundColor: option.color }),
126
+ }}
127
+ title={option.label}
128
+ >
129
+ {option.label}
130
+ </div>
148
131
  </div>
149
- </div>
150
- );
151
- })}
152
- </div>
132
+ );
133
+ })}
134
+ </div>
135
+ )}
153
136
 
154
137
  <div
155
138
  className="relative w-full rounded-md px-1 py-2"
@@ -157,11 +140,26 @@ export default function RatingScale({
157
140
  >
158
141
  <div className="min-w-0" style={gridStyle}>
159
142
  {resolvedOptions.map((option) => {
160
- const isSelected = selectedValue === option.value;
143
+ const isSelected = isScaleAnswerSelected(selectedValue, option.value);
161
144
  return (
162
- <label key={`${questionId}-${option.value}`} title={option.label} className="flex cursor-pointer justify-center">
163
- <input type="radio" name={questionId} checked={isSelected} onChange={() => onSelect(option.value)} className="sr-only" />
164
- <div style={scaleRadioRingStyle(isSelected, { size: 'var(--cfm-nps-cell-size, 40px)' })}>
145
+ <label
146
+ key={`${questionId}-${option.value}`}
147
+ title={option.label}
148
+ className="flex cursor-pointer justify-center"
149
+ >
150
+ <input
151
+ type="radio"
152
+ name={questionId}
153
+ checked={isSelected}
154
+ onChange={() => onSelect(option.value)}
155
+ className="sr-only"
156
+ />
157
+ <div
158
+ className="flex items-center justify-center rounded-full border-2 transition-all"
159
+ style={scaleRadioRingStyle(isSelected, {
160
+ size: 'var(--cfm-nps-cell-size, 40px)',
161
+ })}
162
+ >
165
163
  <div style={scaleRadioDotStyle(isSelected)} />
166
164
  </div>
167
165
  </label>
@@ -7,7 +7,6 @@
7
7
 
8
8
  import React from 'react';
9
9
  import type { SliderMatrixQuestion, MatrixRowAnswers } from '@explorer02/cfm-survey-sdk';
10
- import { sliderTickLabelStyle } from '@/lib/surveyUi/labelStyles';
11
10
  import { tickColorFromIndex } from './surveyUiScaleUtils';
12
11
  import { CustomSliderTrack } from './CustomSliderTrack';
13
12
 
@@ -98,7 +97,7 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
98
97
  width: `${sliceWidth}%`, display: 'flex', justifyContent: 'center',
99
98
  }}>
100
99
  {sliderType === 'graphics' ? (
101
- <div style={{ wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2', fontWeight: 500, fontSize: '13px', ...sliderTickLabelStyle() }}>
100
+ <div style={{ wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2', fontWeight: 500, color: '#4b5563', fontSize: '13px' }}>
102
101
  {tv.label}
103
102
  </div>
104
103
  ) : (
@@ -122,7 +121,7 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
122
121
  position: 'absolute', left: `${percentage}%`, transform: 'translateX(-50%)',
123
122
  width: `${sliceWidth}%`, display: 'flex', justifyContent: 'center',
124
123
  }}>
125
- <span style={{ wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2', ...sliderTickLabelStyle() }}>{m}</span>
124
+ <span style={{ wordBreak: 'break-word', textAlign: 'center', lineHeight: '1.2' }}>{m}</span>
126
125
  </div>
127
126
  );
128
127
  })
@@ -152,7 +151,7 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
152
151
  return (
153
152
  <div key={row.id} style={{
154
153
  display: 'flex', width: '100%', alignItems: 'center',
155
- backgroundColor: 'var(--cfm-slider-row-band)', borderRadius: '12px', padding: '16px', boxSizing: 'border-box',
154
+ backgroundColor: '#f3f4f6', borderRadius: '12px', padding: '16px', boxSizing: 'border-box',
156
155
  }}>
157
156
  <div style={{ flex: '0 0 25%', paddingRight: '16px', fontSize: '15px', color: '#111827', wordWrap: 'break-word', display: 'flex', alignItems: 'center' }}>
158
157
  {row.statementText !== 'Question' && row.statementText !== '' ? (