@explorer02/cfm-survey-sdk 0.4.1 → 0.4.3

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 (25) hide show
  1. package/dist/cli/index.js +44 -40
  2. package/dist/cli/index.mjs +42 -38
  3. package/package.json +1 -1
  4. package/templates/docs/templates/CsatMatrixScale.tsx +30 -34
  5. package/templates/docs/templates/LikertMatrixScale.tsx +6 -3
  6. package/templates/docs/templates/RatingScale.tsx +8 -25
  7. package/templates/docs/templates/SliderMatrixScale.tsx +2 -1
  8. package/templates/docs/templates/labelStyles.ts +17 -0
  9. package/templates/docs/templates/selectionStyles.ts +58 -0
  10. package/templates/docs/templates/surveyUiIcons.tsx +6 -1
  11. package/templates/preview-harness/preview-bridge.inline.js +13 -0
  12. package/templates/preview-harness/preview-google-fonts.html +6 -0
  13. package/templates/preview-harness/previewPages.js +3 -2
  14. package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +29 -10
  15. package/templates/preview-harness/vite-app/src/globals.css +4 -3
  16. package/templates/preview-harness/vite-app/src/mergePreviewQuestion.ts +24 -2
  17. package/templates/preview-harness/vite-app/src/preview-live-overrides.css +5 -0
  18. package/templates/previewBridge.ts +14 -0
  19. package/templates/survey-theme.css +2 -0
  20. package/templates/wizard-dist/assets/{PreviewMock-MsONyaq9.js → PreviewMock-DkCiCfU3.js} +1 -1
  21. package/templates/wizard-dist/assets/TypePanel-CUani1B7.js +1 -0
  22. package/templates/wizard-dist/assets/index-COdmy8Xp.js +34 -0
  23. package/templates/wizard-dist/index.html +1 -1
  24. package/templates/wizard-dist/assets/TypePanel-D24BxbHk.js +0 -1
  25. package/templates/wizard-dist/assets/index-C7RSJr09.js +0 -34
@@ -8,7 +8,8 @@ import {
8
8
  matrixRadioRingStyle,
9
9
  matrixRadioDotStyle,
10
10
  matrixRowBackgroundStyle,
11
- scaleCellSelectedStyle,
11
+ matrixNumberedCellStyle,
12
+ matrixStatementRowsContainerStyle,
12
13
  unselectedOpacityStyle,
13
14
  emojiSizeStyle,
14
15
  cellSelectedVar,
@@ -16,7 +17,7 @@ import {
16
17
  mcqSelectedAccentVar,
17
18
  mcqSelectedBgVar,
18
19
  } from '@/lib/surveyUi/selectionStyles';
19
- import { csatColumnLabelStyle, ratingColumnLabelStyle, stackAnchorLabelLines, ratingAnchorLabelPillStyle } from '@/lib/surveyUi/labelStyles';
20
+ import { csatColumnLabelStyle, ratingColumnLabelStyle, stackAnchorLabelLines, ratingAnchorLabelPillStyle, csatAnchorLabelPillStyle } from '@/lib/surveyUi/labelStyles';
20
21
 
21
22
  const MATRIX_ROW_LABEL_WIDTH = 'var(--cfm-matrix-row-width, 180px)';
22
23
 
@@ -34,19 +35,25 @@ function anchorLabelPositionPercent(
34
35
  question: CsatMatrixQuestion,
35
36
  ): number {
36
37
  if (labelCount === 1) return 50;
37
- const useFullSpan = isGraphics || question.type === 'RATING_MATRIX';
38
+ const useFullSpan =
39
+ isGraphics || question.type === 'RATING_MATRIX' || question.type === 'CSAT_MATRIX';
38
40
  const startPercent = useFullSpan ? 0 : 0.5 / columnCount;
39
41
  const rangePercent = useFullSpan ? 1 : (columnCount - 1) / columnCount;
40
42
  return (startPercent + (index / (labelCount - 1)) * rangePercent) * 100;
41
43
  }
42
44
 
43
45
  function anchorLabelRowMinHeight(question: CsatMatrixQuestion, hasLabels: boolean): string {
44
- if (question.type === 'RATING_MATRIX' && hasLabels) return '44px';
46
+ if (
47
+ hasLabels &&
48
+ (question.type === 'RATING_MATRIX' || question.type === 'CSAT_MATRIX')
49
+ ) {
50
+ return '44px';
51
+ }
45
52
  return '24px';
46
53
  }
47
54
 
48
55
  function renderAnchorLabelContent(label: string, question: CsatMatrixQuestion): React.ReactNode {
49
- if (question.type !== 'RATING_MATRIX') return label;
56
+ if (question.type !== 'RATING_MATRIX' && question.type !== 'CSAT_MATRIX') return label;
50
57
  const lines = stackAnchorLabelLines(label);
51
58
  if (lines.length === 1) return label;
52
59
  return lines.map((line, idx) => (
@@ -65,6 +72,14 @@ function anchorLabelBoxStyle(question: CsatMatrixQuestion, isGraphics: boolean):
65
72
  ...ratingAnchorLabelPillStyle(),
66
73
  };
67
74
  }
75
+ if (question.type === 'CSAT_MATRIX') {
76
+ return {
77
+ textAlign: 'center',
78
+ fontSize: '12px',
79
+ fontWeight: 600,
80
+ ...csatAnchorLabelPillStyle(),
81
+ };
82
+ }
68
83
  return {
69
84
  textAlign: 'center',
70
85
  fontSize: isGraphics ? '12px' : '13px',
@@ -158,7 +173,7 @@ function CsatMatrixVerticalList({
158
173
  const { statementRows, hasNotApplicableOption: hasNotApplicable } = question;
159
174
 
160
175
  return (
161
- <div style={{ display: 'flex', flexDirection: 'column', gap: '32px' }}>
176
+ <div style={matrixStatementRowsContainerStyle()}>
162
177
  {statementRows.map(row => (
163
178
  <div key={row.id} style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
164
179
  <h3 style={{ fontSize: '15px', fontWeight: 500, color: '#111827', lineHeight: 1.6, margin: 0 }}>
@@ -282,7 +297,9 @@ function CsatMatrixGrid({
282
297
  position: 'relative',
283
298
  minHeight: anchorLabelRowMinHeight(question, Boolean(showLabels)),
284
299
  minWidth: 0,
285
- ...(isGraphics || question.type === 'RATING_MATRIX' ? { margin: '0 8px' } : {}),
300
+ ...(isGraphics || question.type === 'RATING_MATRIX' || question.type === 'CSAT_MATRIX'
301
+ ? { margin: '0 8px' }
302
+ : {}),
286
303
  }}>
287
304
  {labels!.map((label, i) => {
288
305
  const percent = anchorLabelPositionPercent(i, n, m, isGraphics, question);
@@ -343,7 +360,7 @@ function CsatMatrixGrid({
343
360
  )}
344
361
 
345
362
  {/* Grid Rows */}
346
- <div>
363
+ <div style={matrixStatementRowsContainerStyle()}>
347
364
  {statementRows.map((row, rowIdx) => (
348
365
  <div key={row.id} style={{
349
366
  display: 'flex', alignItems: 'center', borderRadius: '8px',
@@ -418,21 +435,8 @@ function CsatMatrixGrid({
418
435
  } : isStar ? {
419
436
  padding: '4px',
420
437
  transform: isHovered ? 'scale(1.1)' : 'scale(1)',
421
- color: isSelected ? cellSelectedVar : focusRingVar,
422
438
  ...(isSelected ? {} : unselectedOpacityStyle()),
423
- } : isNumbered ? {
424
- width: '40px', height: '40px', borderRadius: '8px',
425
- fontSize: '14px', fontWeight: 500,
426
- ...(isSelected
427
- ? scaleCellSelectedStyle(true)
428
- : {
429
- border: '1px solid #d1d5db',
430
- backgroundColor: '#fff',
431
- color: '#111827',
432
- ...unselectedOpacityStyle(),
433
- }),
434
- transition: 'all 0.15s',
435
- } : { padding: '8px', ...(!isSelected ? unselectedOpacityStyle() : {}) }),
439
+ } : isNumbered ? matrixNumberedCellStyle(displayIdx, isSelected) : { padding: '8px', ...(!isSelected ? unselectedOpacityStyle() : {}) }),
436
440
  }}
437
441
  >
438
442
  {(EmojiNode ?? StarNode ?? (isNumbered ? (displayIdx + 1) : RadioNode)) as React.ReactNode}
@@ -523,7 +527,9 @@ function CsatMatrixCarousel({
523
527
  position: 'relative',
524
528
  minHeight: anchorLabelRowMinHeight(question, Boolean(showLabels)),
525
529
  marginBottom: '16px',
526
- ...(isGraphics || question.type === 'RATING_MATRIX' ? { padding: '0 8px' } : {}),
530
+ ...(isGraphics || question.type === 'RATING_MATRIX' || question.type === 'CSAT_MATRIX'
531
+ ? { padding: '0 8px' }
532
+ : {}),
527
533
  }}>
528
534
  {labels!.map((label, i) => {
529
535
  const m = scaleColumns.length;
@@ -601,17 +607,7 @@ function CsatMatrixCarousel({
601
607
  ...(isSelected ? { backgroundColor: 'rgba(252, 231, 243, 0.6)', boxShadow: '0 0 0 2px rgba(226, 0, 116, 0.3)', borderRadius: '50%' } : {}),
602
608
  } : isStar ? {
603
609
  padding: '4px', transform: isHovered ? 'scale(1.1)' : 'scale(1)', opacity: isSelected ? 1 : isHovered ? 1 : 0.5,
604
- } : isNumbered ? {
605
- width: '40px', height: '40px', borderRadius: '8px', fontSize: '14px', fontWeight: 500,
606
- ...(isSelected
607
- ? scaleCellSelectedStyle(true)
608
- : {
609
- border: '1px solid #d1d5db',
610
- backgroundColor: '#fff',
611
- color: '#111827',
612
- ...unselectedOpacityStyle(),
613
- }),
614
- } : { padding: '8px', ...(!isSelected ? unselectedOpacityStyle() : {}) }),
610
+ } : isNumbered ? matrixNumberedCellStyle(displayIdx, isSelected) : { padding: '8px', ...(!isSelected ? unselectedOpacityStyle() : {}) }),
615
611
  }}
616
612
  >
617
613
  {(EmojiNode ?? StarNode ?? (isNumbered ? (displayIdx + 1) : RadioNode)) as React.ReactNode}
@@ -3,7 +3,7 @@ import type { CfmMatrixQuestion, MatrixRowAnswers } from '@explorer02/cfm-survey
3
3
  import { columnSubmitValue } from './surveyUiScaleUtils';
4
4
  import MatrixDropdown from './MatrixDropdown';
5
5
  import { matrixColumnLabelStyle } from '@/lib/surveyUi/labelStyles';
6
- import { focusRingVar, matrixCarouselDotVar, matrixRadioDotStyle, matrixRadioRingStyle, matrixRowBackgroundStyle } from '@/lib/surveyUi/selectionStyles';
6
+ import { focusRingVar, matrixCarouselDotVar, matrixRadioDotStyle, matrixRadioRingStyle, matrixRowBackgroundStyle, matrixStatementRowsContainerStyle } from '@/lib/surveyUi/selectionStyles';
7
7
 
8
8
  const MATRIX_ROW_LABEL_WIDTH = 'var(--cfm-matrix-row-width, 180px)';
9
9
  const BIPOLAR_COL_WIDTH = 'var(--cfm-matrix-bipolar-width, 25%)';
@@ -33,7 +33,7 @@ function LikertMatrixDropdownLayout({
33
33
  const { statementRows, scaleColumns } = question;
34
34
 
35
35
  return (
36
- <div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
36
+ <div style={matrixStatementRowsContainerStyle()}>
37
37
  {statementRows.map(row => (
38
38
  <div key={row.id} style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
39
39
  <div style={{ fontSize: '15px', fontWeight: 500, color: '#374151' }} dangerouslySetInnerHTML={{ __html: row.statementText }} />
@@ -251,7 +251,10 @@ function LikertMatrixGridLayout({
251
251
  <div style={{ width: '100%', maxWidth: '100%', minWidth: 0, overflowX: 'auto', boxSizing: 'border-box' }}>
252
252
  {!repeatColumnHeaders && renderHeader()}
253
253
 
254
- <div style={{ display: 'flex', flexDirection: 'column', gap: repeatColumnHeaders ? '24px' : '0' }}>
254
+ <div style={{
255
+ ...matrixStatementRowsContainerStyle(),
256
+ gap: repeatColumnHeaders ? '24px' : 'var(--cfm-matrix-statement-row-gap, 0px)',
257
+ }}>
255
258
  {gridRows.map((rowItem, rowIdx) => {
256
259
  return (
257
260
  <div key={rowItem.id} style={{ display: 'flex', flexDirection: 'column' }}>
@@ -4,12 +4,13 @@ import type { AnswerValue, NpsScalePoint } from '@explorer02/cfm-survey-sdk';
4
4
  import {
5
5
  scaleRadioRingStyle,
6
6
  scaleRadioDotStyle,
7
- scaleCellSelectedStyle,
7
+ npsNumberBadgeStyle,
8
8
  } from '@/lib/surveyUi/selectionStyles';
9
9
  import {
10
10
  getAccentColor,
11
11
  isScaleAnswerSelected,
12
12
  npsValueFromOption,
13
+ resolveNumberLabelColor,
13
14
  starValueFromOption,
14
15
  } from './surveyUiScaleUtils';
15
16
  import { getEmojiForIndex } from './surveyUiIcons';
@@ -23,25 +24,6 @@ type RatingScaleProps = {
23
24
  buttonStyle?: 'standard' | 'numbered' | 'emoji' | 'pill';
24
25
  };
25
26
 
26
- function resolveBadgeColor(index: number, value: number): string {
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();
31
- if (mode === 'monochrome') {
32
- return (
33
- getComputedStyle(root).getPropertyValue('--cfm-number-mono-color').trim() || getAccentColor(value)
34
- );
35
- }
36
- if (mode === 'individual') {
37
- return (
38
- getComputedStyle(root).getPropertyValue(`--cfm-number-color-${index}`).trim() ||
39
- getAccentColor(value)
40
- );
41
- }
42
- return getAccentColor(value);
43
- }
44
-
45
27
  export default function RatingScale({
46
28
  questionId,
47
29
  options,
@@ -57,7 +39,9 @@ export default function RatingScale({
57
39
  : npsValueFromOption(option as NpsScalePoint, index);
58
40
  const numericValue = typeof value === 'number' ? value : index;
59
41
  const color =
60
- variant === 'nps' ? resolveBadgeColor(index, numericValue) : 'var(--cfm-primary, #e20074)';
42
+ variant === 'nps'
43
+ ? resolveNumberLabelColor(index, getAccentColor(numericValue))
44
+ : 'var(--cfm-primary, #e20074)';
61
45
  return { id: option.id, label: option.optionLabel, value, color };
62
46
  });
63
47
 
@@ -117,15 +101,14 @@ export default function RatingScale({
117
101
  return (
118
102
  <div key={`${questionId}-badge-${option.value}`} className="flex justify-center">
119
103
  <div
120
- className="flex items-center justify-center rounded-[3px] text-[10px] font-bold text-white shadow-sm sm:text-xs"
104
+ className={`flex items-center justify-center rounded-[3px] text-[10px] font-bold shadow-sm sm:text-xs${isSelected ? ' selected' : ''}`}
121
105
  style={{
122
106
  width: 'var(--cfm-nps-cell-size, 24px)',
123
107
  height: 'var(--cfm-nps-cell-size, 24px)',
124
- ...(isSelected
125
- ? scaleCellSelectedStyle(true)
126
- : { backgroundColor: option.color }),
108
+ ...npsNumberBadgeStyle(option.color, isSelected),
127
109
  }}
128
110
  data-cfm-number-badge={index}
111
+ data-cfm-nps-badge
129
112
  title={option.label}
130
113
  >
131
114
  {option.label}
@@ -10,6 +10,7 @@ import type { SliderMatrixQuestion, MatrixRowAnswers } from '@explorer02/cfm-sur
10
10
  import { tickColorFromIndex } from './surveyUiScaleUtils';
11
11
  import { CustomSliderTrack } from './CustomSliderTrack';
12
12
  import { hintLabelStyle, sliderTickLabelStyle } from '@/lib/surveyUi/labelStyles';
13
+ import { matrixStatementRowsContainerStyle } from '@/lib/surveyUi/selectionStyles';
13
14
 
14
15
  const MATRIX_ROW_LABEL_WIDTH = 'var(--cfm-matrix-row-width, 180px)';
15
16
  const SLIDER_ROW_BAND = 'var(--cfm-slider-row-band, var(--cfm-row-band, #f3f4f6))';
@@ -168,7 +169,7 @@ export function SliderMatrixScale({ question, selectedValue = {}, onSelect }: Sl
168
169
  };
169
170
 
170
171
  return (
171
- <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', width: '100%', maxWidth: '100%', minWidth: 0, overflowX: 'auto', boxSizing: 'border-box' }}>
172
+ <div style={{ ...matrixStatementRowsContainerStyle(), width: '100%', maxWidth: '100%', minWidth: 0, overflowX: 'auto', boxSizing: 'border-box' }}>
172
173
  {renderHeaderRow()}
173
174
 
174
175
  {statementRows.map(row => {
@@ -75,6 +75,23 @@ export function stackAnchorLabelLines(label: string): string[] {
75
75
  return [words.slice(0, mid).join(' '), words.slice(mid).join(' ')].filter(Boolean);
76
76
  }
77
77
 
78
+ /** CSAT matrix anchor pills — stacked text, fixed width, same look on every format. */
79
+ export function csatAnchorLabelPillStyle(): CSSProperties {
80
+ return {
81
+ ...columnLabelPillStyle('csat'),
82
+ display: 'inline-flex',
83
+ flexDirection: 'column',
84
+ alignItems: 'center',
85
+ justifyContent: 'center',
86
+ whiteSpace: 'normal',
87
+ maxWidth: '72px',
88
+ minWidth: '52px',
89
+ padding: '3px 6px',
90
+ wordBreak: 'break-word',
91
+ lineHeight: 1.15,
92
+ };
93
+ }
94
+
78
95
  /** Rating matrix anchor pills — stacked text, fixed width, same look on every format. */
79
96
  export function ratingAnchorLabelPillStyle(): CSSProperties {
80
97
  return {
@@ -7,6 +7,7 @@
7
7
  * --cfm-input-focus-ring = focus ring (outer border + inner dot)
8
8
  */
9
9
  import type { CSSProperties } from 'react';
10
+ import { resolveNumberLabelColor } from './surveyUiScaleUtils';
10
11
 
11
12
  /** Terminal hex fallbacks match apps/client — visible before survey-theme.css loads. */
12
13
  export const cellSelectedVar = 'var(--cfm-matrix-selected, var(--cfm-primary, #fbe8f3))';
@@ -173,10 +174,67 @@ export function scaleCellSelectedStyle(isSelected: boolean): CSSProperties {
173
174
  : {};
174
175
  }
175
176
 
177
+ /** NPS numbered badge row — React-owned; do not patch via preview bridge DOM hooks. */
178
+ export function npsNumberBadgeStyle(backgroundColor: string, isSelected: boolean): CSSProperties {
179
+ const base: CSSProperties = {
180
+ color: '#ffffff',
181
+ boxSizing: 'border-box',
182
+ };
183
+ if (isSelected) {
184
+ return {
185
+ ...base,
186
+ border: `2px solid ${focusRingVar}`,
187
+ backgroundColor: cellSelectedVar,
188
+ };
189
+ }
190
+ return {
191
+ ...base,
192
+ backgroundColor,
193
+ border: 'none',
194
+ };
195
+ }
196
+
197
+ /** CSAT / Rating matrix numbered cell (unselected uses traffic/mono/individual badge colors). */
198
+ export function matrixNumberedCellStyle(index: number, isSelected: boolean): CSSProperties {
199
+ const base: CSSProperties = {
200
+ width: '40px',
201
+ height: '40px',
202
+ borderRadius: '8px',
203
+ fontSize: '14px',
204
+ fontWeight: 500,
205
+ transition: 'all 0.15s',
206
+ };
207
+
208
+ if (isSelected) {
209
+ return {
210
+ ...base,
211
+ border: `2px solid ${focusRingVar}`,
212
+ backgroundColor: cellSelectedVar,
213
+ color: 'var(--cfm-matrix-number-text-color, #ffffff)',
214
+ };
215
+ }
216
+
217
+ return {
218
+ ...base,
219
+ border: '1px solid #d1d5db',
220
+ backgroundColor: resolveNumberLabelColor(index),
221
+ color: 'var(--cfm-matrix-number-text-color, #ffffff)',
222
+ ...unselectedOpacityStyle(),
223
+ };
224
+ }
225
+
176
226
  export function unselectedOpacityStyle(): CSSProperties {
177
227
  return { opacity: 'var(--cfm-csat-unselected-opacity, 0.5)' };
178
228
  }
179
229
 
230
+ export function matrixStatementRowsContainerStyle(): CSSProperties {
231
+ return {
232
+ display: 'flex',
233
+ flexDirection: 'column',
234
+ gap: 'var(--cfm-matrix-statement-row-gap, 0px)',
235
+ };
236
+ }
237
+
180
238
  /** Matrix statement row — zebra when --cfm-matrix-zebra-enabled is 1 (wizard multi-statement preview). */
181
239
  export function matrixRowBackgroundStyle(rowIdx: number): CSSProperties {
182
240
  let zebraOn = false;
@@ -58,6 +58,11 @@ export const CsatStarIcons: { filled: React.ReactNode; empty: React.ReactNode }
58
58
  style: { color: 'var(--cfm-csat-accent, var(--cfm-primary, #e20074))', fontSize: 'var(--cfm-csat-emoji-size, 28px)' },
59
59
  }),
60
60
  empty: React.createElement(FaRegStar as React.ElementType, {
61
- style: { color: '#d1d5db', fontSize: 'var(--cfm-csat-emoji-size, 28px)', stroke: '#d1d5db', strokeWidth: '16px' },
61
+ style: {
62
+ color: 'var(--cfm-star-color, var(--cfm-csat-accent, var(--cfm-primary, #e20074)))',
63
+ fontSize: 'var(--cfm-csat-emoji-size, 28px)',
64
+ stroke: 'var(--cfm-star-color, var(--cfm-csat-accent, var(--cfm-primary, #e20074)))',
65
+ strokeWidth: '16px',
66
+ },
62
67
  }),
63
68
  };
@@ -2,10 +2,21 @@
2
2
  var SNAPSHOT = 'CFM_UI_CONFIG_SNAPSHOT';
3
3
  var DELTA = 'CFM_UI_CONFIG_DELTA';
4
4
  var LEGACY = 'CFM_UI_CONFIG';
5
+ var PREVIEW_FONTS_HREF =
6
+ 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Roboto:wght@400;500;600;700&family=Open+Sans:wght@400;500;600;700&family=Lato:wght@400;500;600;700&family=Poppins:wght@400;500;600;700&family=Montserrat:wght@400;500;600;700&family=Nunito:wght@400;500;600;700&family=Raleway:wght@400;500;600;700&family=Source+Sans+3:wght@400;500;600;700&family=Merriweather:wght@400;500;600;700&family=Playfair+Display:wght@400;500;600;700&display=swap';
5
7
  var rafId = null;
6
8
  var pending = {};
7
9
  var liveEdit = false;
8
10
 
11
+ function ensurePreviewFonts() {
12
+ if (document.getElementById('cfm-preview-fonts')) return;
13
+ var link = document.createElement('link');
14
+ link.id = 'cfm-preview-fonts';
15
+ link.rel = 'stylesheet';
16
+ link.href = PREVIEW_FONTS_HREF;
17
+ document.head.appendChild(link);
18
+ }
19
+
9
20
  function flush() {
10
21
  rafId = null;
11
22
  var root = document.documentElement;
@@ -57,6 +68,7 @@ function setDisplay(selector, visible) {
57
68
  var mode = readVar('--cfm-number-label-mode', 'traffic');
58
69
  var mono = readVar('--cfm-number-mono-color', '#9ca3af');
59
70
  document.querySelectorAll('[data-cfm-number-badge]').forEach(function (el) {
71
+ if (el.closest('[data-cfm-nps-area]')) return;
60
72
  if (el.classList.contains('selected')) return;
61
73
  var i = parseInt(el.getAttribute('data-cfm-number-badge'), 10);
62
74
  if (isNaN(i)) return;
@@ -282,6 +294,7 @@ function setDisplay(selector, visible) {
282
294
 
283
295
  if (!liveEdit) {
284
296
  liveEdit = true;
297
+ ensurePreviewFonts();
285
298
  document.documentElement.classList.add('cfm-live-edit');
286
299
  }
287
300
 
@@ -0,0 +1,6 @@
1
+ <link id="cfm-preview-fonts" rel="preconnect" href="https://fonts.googleapis.com" />
2
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
3
+ <link
4
+ rel="stylesheet"
5
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Roboto:wght@400;500;600;700&family=Open+Sans:wght@400;500;600;700&family=Lato:wght@400;500;600;700&family=Poppins:wght@400;500;600;700&family=Montserrat:wght@400;500;600;700&family=Nunito:wght@400;500;600;700&family=Raleway:wght@400;500;600;700&family=Source+Sans+3:wght@400;500;600;700&family=Merriweather:wght@400;500;600;700&family=Playfair+Display:wght@400;500;600;700&display=swap"
6
+ />
@@ -122,14 +122,15 @@ const PREVIEW_PAGE_DEFS = [
122
122
  { key: 'RANK_dropdown', title: 'Rank Dropdown', body: `<div class="cfm-rank-item"><span>Item A</span><select class="cfm-input cfm-rank-select" style="width:80px;margin-left:auto"><option>1</option><option>2</option></select></div>` },
123
123
  ];
124
124
 
125
- function buildPreviewHtml(def, inlineCss, inlineJs) {
125
+ function buildPreviewHtml(def, inlineCss, inlineJs, fontsSnippet) {
126
+ const fonts = fontsSnippet ? `${fontsSnippet}\n` : '';
126
127
  return `<!DOCTYPE html>
127
128
  <html lang="en">
128
129
  <head>
129
130
  <meta charset="UTF-8" />
130
131
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
131
132
  <title>${def.title} — CFM Preview</title>
132
- <style>${inlineCss}\n${sharedStyles}</style>
133
+ ${fonts}<style>${inlineCss}\n${sharedStyles}</style>
133
134
  </head>
134
135
  <body>
135
136
  <div class="cfm-preview-root">${def.body}</div>
@@ -17,26 +17,37 @@ export const PREVIEW_BRIDGE_SNAPSHOT = 'CFM_UI_CONFIG_SNAPSHOT';
17
17
  export const PREVIEW_BRIDGE_DELTA = 'CFM_UI_CONFIG_DELTA';
18
18
  export const PREVIEW_BRIDGE_READY = 'CFM_PREVIEW_READY';
19
19
 
20
- const TRAFFIC = [
21
- '#ef4444', '#f97316', '#eab308', '#84cc16', '#22c55e', '#14b8a6',
22
- '#06b6d4', '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7',
23
- ];
20
+ const SCALE_COLORS = [
21
+ '#e2001a', '#e4251b', '#ec610a', '#f18b00', '#f7b200', '#fcd900',
22
+ '#d2dc00', '#9fca00', '#6bb300', '#339a00', '#008000',
23
+ ] as const;
24
+
25
+ function trafficColor(index: number): string {
26
+ return SCALE_COLORS[index] ?? '#9ca3af';
27
+ }
24
28
 
25
29
  function readVar(name: string, fallback: string): string {
26
30
  return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
27
31
  }
28
32
 
29
- function applyNumberBadgeColors(): void {
33
+ function resolveLiveNumberLabelColor(index: number): string {
30
34
  const mode = readVar('--cfm-number-label-mode', 'traffic');
31
- const mono = readVar('--cfm-number-mono-color', '#9ca3af');
35
+ if (mode === 'monochrome') {
36
+ return readVar('--cfm-number-mono-color', '#9ca3af');
37
+ }
38
+ if (mode === 'individual') {
39
+ return readVar(`--cfm-number-color-${index}`, '#9ca3af');
40
+ }
41
+ return trafficColor(index);
42
+ }
43
+
44
+ function applyNumberBadgeColors(): void {
32
45
  document.querySelectorAll('[data-cfm-number-badge]').forEach((el) => {
46
+ if (el.closest('[data-cfm-nps-area]')) return;
33
47
  if (el.classList.contains('selected')) return;
34
48
  const i = parseInt(el.getAttribute('data-cfm-number-badge') ?? '', 10);
35
49
  if (Number.isNaN(i)) return;
36
- let bg = TRAFFIC[i] ?? '#9ca3af';
37
- if (mode === 'monochrome') bg = mono;
38
- if (mode === 'individual') bg = readVar(`--cfm-number-color-${i}`, '#9ca3af');
39
- (el as HTMLElement).style.backgroundColor = bg;
50
+ (el as HTMLElement).style.backgroundColor = resolveLiveNumberLabelColor(i);
40
51
  });
41
52
  }
42
53
 
@@ -82,11 +93,17 @@ const PreviewConfigContext = createContext<PreviewConfigContextValue>({
82
93
  configRevision: 0,
83
94
  });
84
95
 
96
+ function enableLiveEditMode(): void {
97
+ document.documentElement.classList.add('cfm-live-edit');
98
+ }
99
+
85
100
  export function PreviewConfigProvider({ children }: { children: ReactNode }) {
86
101
  const [previewState, setPreviewState] = useState<PreviewStatePatch>({});
87
102
  const [configRevision, setConfigRevision] = useState(0);
88
103
 
89
104
  useEffect(() => {
105
+ enableLiveEditMode();
106
+
90
107
  const requestSnapshot = () => {
91
108
  try {
92
109
  window.parent.postMessage({ type: PREVIEW_BRIDGE_READY }, '*');
@@ -102,6 +119,8 @@ export function PreviewConfigProvider({ children }: { children: ReactNode }) {
102
119
  if (!data?.type) return;
103
120
  if (data.type !== PREVIEW_BRIDGE_SNAPSHOT && data.type !== PREVIEW_BRIDGE_DELTA) return;
104
121
 
122
+ enableLiveEditMode();
123
+
105
124
  const isSnapshot = data.type === PREVIEW_BRIDGE_SNAPSHOT;
106
125
  let shouldBumpRevision = isSnapshot;
107
126
 
@@ -6,9 +6,9 @@
6
6
 
7
7
  body {
8
8
  margin: 0;
9
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
10
- color: #262626;
11
- background-color: #ffffff;
9
+ font-family: var(--cfm-font-family, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
10
+ color: var(--cfm-text, #262626);
11
+ background-color: var(--cfm-background, #ffffff);
12
12
  max-width: 100%;
13
13
  overflow-x: hidden;
14
14
  box-sizing: border-box;
@@ -19,4 +19,5 @@ body {
19
19
  max-width: 100%;
20
20
  overflow-x: hidden;
21
21
  box-sizing: border-box;
22
+ font-family: inherit;
22
23
  }
@@ -224,10 +224,32 @@ export function mergePreviewQuestion(
224
224
  });
225
225
  }
226
226
 
227
- /** Survey page layout — each question reflects its type-tab customization mapping. */
227
+ /** Survey page layout — each embedded question uses its type-tab customization. */
228
228
  export function mergeSurveyPageQuestion(
229
229
  question: SurveyQuestion,
230
230
  previewState: PreviewStatePatch,
231
231
  ): SurveyQuestion {
232
- return mergePreviewQuestion(question, normalizeSurveyPagePreviewState(previewState));
232
+ const normalized = normalizeSurveyPagePreviewState(previewState);
233
+ let state = normalized;
234
+
235
+ if (question.type === 'NPS_SCALE') {
236
+ state = {
237
+ ...normalized,
238
+ npsButtonStyle:
239
+ previewState.npsButtonStyle
240
+ ?? normalized.npsButtonStyle
241
+ ?? (question.buttonStyle as string | undefined),
242
+ hintMinText: previewState.hintMinText ?? normalized.hintMinText,
243
+ hintMaxText: previewState.hintMaxText ?? normalized.hintMaxText,
244
+ };
245
+ } else if (question.type === 'MCQ') {
246
+ state = {
247
+ ...normalized,
248
+ mcqOptionStyle: previewState.mcqOptionStyle ?? normalized.mcqOptionStyle,
249
+ };
250
+ } else if (question.type === 'CFM_MATRIX') {
251
+ state = { ...normalized, multiStatement: true };
252
+ }
253
+
254
+ return mergePreviewQuestion(question, state);
233
255
  }
@@ -122,6 +122,7 @@ html.cfm-live-edit #root {
122
122
  max-width: 100%;
123
123
  overflow-x: hidden;
124
124
  box-sizing: border-box;
125
+ font-family: var(--cfm-font-family, system-ui, sans-serif) !important;
125
126
  }
126
127
 
127
128
  html.cfm-live-edit [data-cfm-question-area],
@@ -136,3 +137,7 @@ html.cfm-live-edit [data-cfm-nps-area] {
136
137
  max-width: 100%;
137
138
  overflow-x: auto;
138
139
  }
140
+
141
+ html.cfm-live-edit [data-cfm-nps-area] [data-cfm-nps-badge] {
142
+ color: #ffffff !important;
143
+ }
@@ -33,6 +33,18 @@ let rafId: number | null = null;
33
33
  const pendingVars: Record<string, string> = {};
34
34
  let liveEditClass = false;
35
35
 
36
+ const PREVIEW_FONTS_HREF =
37
+ 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Roboto:wght@400;500;600;700&family=Open+Sans:wght@400;500;600;700&family=Lato:wght@400;500;600;700&family=Poppins:wght@400;500;600;700&family=Montserrat:wght@400;500;600;700&family=Nunito:wght@400;500;600;700&family=Raleway:wght@400;500;600;700&family=Source+Sans+3:wght@400;500;600;700&family=Merriweather:wght@400;500;600;700&family=Playfair+Display:wght@400;500;600;700&display=swap';
38
+
39
+ function ensurePreviewFonts(): void {
40
+ if (document.getElementById('cfm-preview-fonts')) return;
41
+ const link = document.createElement('link');
42
+ link.id = 'cfm-preview-fonts';
43
+ link.rel = 'stylesheet';
44
+ link.href = PREVIEW_FONTS_HREF;
45
+ document.head.appendChild(link);
46
+ }
47
+
36
48
  function flushCssVars(): void {
37
49
  rafId = null;
38
50
  const root = document.documentElement;
@@ -285,6 +297,7 @@ function applyThemeSideEffects(): void {
285
297
  const mode = readCssVar('--cfm-number-label-mode', 'traffic');
286
298
  const mono = readCssVar('--cfm-number-mono-color', '#9ca3af');
287
299
  document.querySelectorAll('[data-cfm-number-badge]').forEach((el) => {
300
+ if (el.closest('[data-cfm-nps-area]')) return;
288
301
  if (el.classList.contains('selected')) return;
289
302
  const i = parseInt(el.getAttribute('data-cfm-number-badge') ?? '', 10);
290
303
  if (Number.isNaN(i)) return;
@@ -302,6 +315,7 @@ function applyThemeSideEffects(): void {
302
315
  function setLiveEditMode(on: boolean): void {
303
316
  if (liveEditClass === on) return;
304
317
  liveEditClass = on;
318
+ if (on) ensurePreviewFonts();
305
319
  document.documentElement.classList.toggle('cfm-live-edit', on);
306
320
  }
307
321
 
@@ -98,10 +98,12 @@
98
98
  --cfm-matrix-bipolar-width: 25%;
99
99
  --cfm-matrix-carousel-dot: #2563eb;
100
100
  --cfm-matrix-cell-padding: 8px;
101
+ --cfm-matrix-statement-row-gap: 8px;
101
102
 
102
103
  --cfm-csat-accent: var(--cfm-accent);
103
104
  --cfm-csat-emoji-size: 24px;
104
105
  --cfm-csat-unselected-opacity: 0.5;
106
+ --cfm-matrix-number-text-color: #ffffff;
105
107
  --cfm-csat-ring-color: var(--cfm-accent);
106
108
  --cfm-csat-cell-size: 40px;
107
109
 
@@ -1 +1 @@
1
- import{u as x,j as e}from"./index-C7RSJr09.js";import{b as t}from"./vendor-BwkXDkd3.js";function p(d=150){const l=x(r=>r.config),[a,s]=t.useState(l);return t.useEffect(()=>{const r=window.setTimeout(()=>s(l),d);return()=>window.clearTimeout(r)},[l,d]),a}const h=t.memo(function(){const l=p(150),a=x(n=>n.logoPreviewDataUrl),{global:s}=l,{layout:r,colorScheme:o,buttons:c}=s,m=t.useMemo(()=>r.borderStyle==="sharp"?"rounded-none":r.borderStyle==="pill"?"rounded-[32px]":"rounded-2xl",[r.borderStyle]),i=t.useMemo(()=>r.fontStyle==="serif"?"Georgia, serif":r.fontStyle==="system"?"system-ui, sans-serif":"Inter, sans-serif",[r.fontStyle]),u=a??s.logo.url,f=t.useMemo(()=>({backgroundColor:o.background,color:o.text,borderColor:o.secondary,fontFamily:i}),[o,i]);return e.jsxs("div",{className:`overflow-hidden border shadow-md ${m}`,style:f,children:[r.showHeader&&e.jsxs("div",{className:"flex items-center gap-3 border-b px-5 py-4",style:{borderColor:o.secondary},children:[u&&e.jsx("img",{src:u,alt:"Logo",className:"max-h-6 w-auto object-contain",loading:"lazy"}),e.jsx("span",{className:"text-xs font-semibold tracking-wider",children:s.companyName})]}),e.jsxs("div",{className:"space-y-6 p-6",children:[r.showProgressBar&&e.jsxs("div",{className:"space-y-1.5",children:[e.jsxs("div",{className:"flex justify-between text-[10px] font-semibold text-slate-400",children:[e.jsx("span",{children:"Progress"}),e.jsx("span",{children:"35%"})]}),e.jsx("div",{className:"h-1.5 w-full overflow-hidden rounded-full bg-slate-100",children:e.jsx("div",{className:"h-full w-[35%] rounded-full",style:{backgroundColor:o.accent}})})]}),e.jsxs("div",{className:"space-y-3",children:[e.jsxs("h4",{className:"flex items-start gap-1.5 text-sm font-bold leading-snug",children:[r.showQuestionNumbers&&e.jsx("span",{className:"font-medium text-slate-400",children:"Q1."}),e.jsx("span",{children:s.surveyTitle}),r.showRequiredAsterisk&&e.jsx("span",{className:"text-red-500",children:"*"})]}),e.jsx("p",{className:"text-[11px] text-slate-400",children:"How would you rate your overall experience?"}),e.jsx("div",{className:"grid grid-cols-5 gap-2",children:[1,2,3,4,5].map(n=>e.jsx("div",{className:`flex h-9 items-center justify-center border text-xs font-semibold ${r.borderStyle==="pill"?"rounded-full":r.borderStyle==="sharp"?"rounded-none":"rounded-lg"}`,style:n===1?{backgroundColor:o.secondary,borderColor:o.primary,color:o.primary}:{backgroundColor:o.background,borderColor:o.secondary,color:o.text},children:n},n))})]})]}),e.jsxs("div",{className:"flex items-center justify-between border-t bg-slate-50/50 px-6 py-4",children:[r.showBackButton&&e.jsx("span",{className:`border px-3 py-1.5 text-[10px] font-semibold ${r.borderStyle==="pill"?"rounded-full":r.borderStyle==="sharp"?"rounded-none":"rounded-lg"}`,style:{borderColor:o.secondary,color:o.text},children:c.back}),e.jsx("div",{className:"flex-1"}),r.showNextButton&&e.jsx("span",{className:`px-4 py-1.5 text-[10px] font-bold ${r.borderStyle==="pill"?"rounded-full":r.borderStyle==="sharp"?"rounded-none":"rounded-lg"}`,style:{backgroundColor:o.primary,color:o.buttonText},children:c.next})]})]})});export{h as PreviewMock};
1
+ import{u as x,j as e}from"./index-COdmy8Xp.js";import{b as t}from"./vendor-BwkXDkd3.js";function p(d=150){const l=x(r=>r.config),[a,s]=t.useState(l);return t.useEffect(()=>{const r=window.setTimeout(()=>s(l),d);return()=>window.clearTimeout(r)},[l,d]),a}const h=t.memo(function(){const l=p(150),a=x(n=>n.logoPreviewDataUrl),{global:s}=l,{layout:r,colorScheme:o,buttons:c}=s,m=t.useMemo(()=>r.borderStyle==="sharp"?"rounded-none":r.borderStyle==="pill"?"rounded-[32px]":"rounded-2xl",[r.borderStyle]),i=t.useMemo(()=>r.fontStyle==="serif"?"Georgia, serif":r.fontStyle==="system"?"system-ui, sans-serif":"Inter, sans-serif",[r.fontStyle]),u=a??s.logo.url,f=t.useMemo(()=>({backgroundColor:o.background,color:o.text,borderColor:o.secondary,fontFamily:i}),[o,i]);return e.jsxs("div",{className:`overflow-hidden border shadow-md ${m}`,style:f,children:[r.showHeader&&e.jsxs("div",{className:"flex items-center gap-3 border-b px-5 py-4",style:{borderColor:o.secondary},children:[u&&e.jsx("img",{src:u,alt:"Logo",className:"max-h-6 w-auto object-contain",loading:"lazy"}),e.jsx("span",{className:"text-xs font-semibold tracking-wider",children:s.companyName})]}),e.jsxs("div",{className:"space-y-6 p-6",children:[r.showProgressBar&&e.jsxs("div",{className:"space-y-1.5",children:[e.jsxs("div",{className:"flex justify-between text-[10px] font-semibold text-slate-400",children:[e.jsx("span",{children:"Progress"}),e.jsx("span",{children:"35%"})]}),e.jsx("div",{className:"h-1.5 w-full overflow-hidden rounded-full bg-slate-100",children:e.jsx("div",{className:"h-full w-[35%] rounded-full",style:{backgroundColor:o.accent}})})]}),e.jsxs("div",{className:"space-y-3",children:[e.jsxs("h4",{className:"flex items-start gap-1.5 text-sm font-bold leading-snug",children:[r.showQuestionNumbers&&e.jsx("span",{className:"font-medium text-slate-400",children:"Q1."}),e.jsx("span",{children:s.surveyTitle}),r.showRequiredAsterisk&&e.jsx("span",{className:"text-red-500",children:"*"})]}),e.jsx("p",{className:"text-[11px] text-slate-400",children:"How would you rate your overall experience?"}),e.jsx("div",{className:"grid grid-cols-5 gap-2",children:[1,2,3,4,5].map(n=>e.jsx("div",{className:`flex h-9 items-center justify-center border text-xs font-semibold ${r.borderStyle==="pill"?"rounded-full":r.borderStyle==="sharp"?"rounded-none":"rounded-lg"}`,style:n===1?{backgroundColor:o.secondary,borderColor:o.primary,color:o.primary}:{backgroundColor:o.background,borderColor:o.secondary,color:o.text},children:n},n))})]})]}),e.jsxs("div",{className:"flex items-center justify-between border-t bg-slate-50/50 px-6 py-4",children:[r.showBackButton&&e.jsx("span",{className:`border px-3 py-1.5 text-[10px] font-semibold ${r.borderStyle==="pill"?"rounded-full":r.borderStyle==="sharp"?"rounded-none":"rounded-lg"}`,style:{borderColor:o.secondary,color:o.text},children:c.back}),e.jsx("div",{className:"flex-1"}),r.showNextButton&&e.jsx("span",{className:`px-4 py-1.5 text-[10px] font-bold ${r.borderStyle==="pill"?"rounded-full":r.borderStyle==="sharp"?"rounded-none":"rounded-lg"}`,style:{backgroundColor:o.primary,color:o.buttonText},children:c.next})]})]})});export{h as PreviewMock};