@explorer02/cfm-survey-sdk 0.3.9 → 0.4.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 (29) hide show
  1. package/dist/cli/index.js +35 -35
  2. package/dist/cli/index.mjs +35 -35
  3. package/package.json +1 -1
  4. package/templates/docs/templates/CsatMatrixScale.tsx +36 -23
  5. package/templates/docs/templates/CustomSliderTrack.tsx +2 -2
  6. package/templates/docs/templates/Header.tsx +14 -14
  7. package/templates/docs/templates/LikertMatrixScale.tsx +2 -2
  8. package/templates/docs/templates/Question.tsx +30 -23
  9. package/templates/docs/templates/RankOrderScale.tsx +10 -2
  10. package/templates/docs/templates/SliderMatrixScale.tsx +1 -1
  11. package/templates/docs/templates/uiConfig.ts +85 -0
  12. package/templates/preview-harness/preview-bridge.inline.js +55 -17
  13. package/templates/preview-harness/previewPages.js +2 -2
  14. package/templates/preview-harness/previewPages.ts +1 -1
  15. package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +8 -1
  16. package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +17 -96
  17. package/templates/preview-harness/vite-app/src/SurveyPagePreview.tsx +32 -8
  18. package/templates/preview-harness/vite-app/src/fixtures/questions.ts +47 -16
  19. package/templates/preview-harness/vite-app/src/globals.css +6 -0
  20. package/templates/preview-harness/vite-app/src/mergePreviewQuestion.ts +173 -0
  21. package/templates/preview-harness/vite-app/src/preview-live-overrides.css +22 -0
  22. package/templates/previewBridge.ts +61 -18
  23. package/templates/survey-theme.css +3 -0
  24. package/templates/wizard-dist/assets/{PreviewMock-CS4WqhDB.js → PreviewMock-BlOAvUCN.js} +1 -1
  25. package/templates/wizard-dist/assets/TypePanel-DcbMaw1b.js +1 -0
  26. package/templates/wizard-dist/assets/index-2Bi_7Fdh.js +34 -0
  27. package/templates/wizard-dist/index.html +1 -1
  28. package/templates/wizard-dist/assets/TypePanel-BgTW2c74.js +0 -1
  29. package/templates/wizard-dist/assets/index-hjm-fxWO.js +0 -34
@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react';
2
2
  import type { AnswerValue, SurveyQuestion } from '@explorer02/cfm-survey-sdk';
3
3
  import Question from '@/components/Question';
4
4
  import { PreviewConfigProvider, usePreviewConfig } from './PreviewConfigContext';
5
+ import { mergePreviewQuestion } from './mergePreviewQuestion';
5
6
 
6
7
  type QuestionPreviewProps = {
7
8
  question: SurveyQuestion;
@@ -27,103 +28,14 @@ function resolveNumberLabelColor(index: number): string {
27
28
  return '';
28
29
  }
29
30
 
30
- function applyMatrixRowCount<T extends SurveyQuestion & { statementRows?: { id: string; statementText: string }[] }>(
31
- q: T,
32
- multiStatement: boolean | undefined,
33
- ): T {
34
- if (!('statementRows' in q) || !Array.isArray(q.statementRows)) return q;
35
- const rows = q.statementRows;
36
- if (multiStatement) {
37
- if (rows.length >= 2) return q;
38
- return {
39
- ...q,
40
- statementRows: [
41
- ...rows,
42
- { id: 'r2', statementText: 'Statement Item 2' },
43
- ...(rows.length === 1 ? [{ id: 'r3', statementText: 'Statement Item 3' }] : []),
44
- ],
45
- } as T;
46
- }
47
- return { ...q, statementRows: rows.slice(0, 1) } as T;
48
- }
49
-
50
31
  function QuestionPreviewInner({ question, initialValue }: QuestionPreviewProps) {
51
32
  const { previewState, configRevision } = usePreviewConfig();
52
33
  const [value, setValue] = useState<AnswerValue | undefined>(initialValue);
53
34
 
54
- const mergedQuestion = useMemo(() => {
55
- let q = { ...question } as SurveyQuestion;
56
- const previewKey = previewState.activePreviewKey ?? '';
57
-
58
- if (previewState.textfieldPlaceholder && q.type === 'TEXTFIELD') {
59
- q = { ...q, placeholder: previewState.textfieldPlaceholder };
60
- }
61
-
62
- if (previewState.npsButtonStyle && q.type === 'NPS_SCALE') {
63
- const style = previewState.npsButtonStyle;
64
- const buttonStyle =
65
- style === 'numbered'
66
- ? 'numbered'
67
- : style === 'pill'
68
- ? 'pill'
69
- : style === 'emoji'
70
- ? 'emoji'
71
- : 'standard';
72
- q = { ...q, buttonStyle };
73
- }
74
-
75
- if (previewState.hintMinText !== undefined || previewState.hintMaxText !== undefined) {
76
- if (q.type === 'NPS_SCALE') {
77
- q = {
78
- ...q,
79
- minLabel: previewState.hintMinText ?? q.minLabel,
80
- maxLabel: previewState.hintMaxText ?? q.maxLabel,
81
- };
82
- }
83
- if (q.type === 'SLIDER_MATRIX') {
84
- q = {
85
- ...q,
86
- scaleAnchorLabels: [
87
- previewState.hintMinText ?? q.scaleAnchorLabels?.[0] ?? '',
88
- previewState.hintMaxText ?? q.scaleAnchorLabels?.[1] ?? '',
89
- ],
90
- };
91
- }
92
- }
93
-
94
- if (q.type === 'CFM_MATRIX') {
95
- if (previewKey === 'CFM_bipolar') {
96
- q = {
97
- ...q,
98
- statementLayout: 'bipolar',
99
- showColumnHeaders: false,
100
- gridLayout: 'standard',
101
- transposeTable: false,
102
- };
103
- } else if (previewKey === 'CFM_transpose') {
104
- q = { ...q, statementLayout: 'likert', transposeTable: true, gridLayout: 'standard' };
105
- } else if (previewKey === 'CFM_carousel') {
106
- q = { ...q, statementLayout: 'likert', gridLayout: 'carousel', transposeTable: false };
107
- } else if (previewKey === 'CFM_dropdown') {
108
- q = { ...q, statementLayout: 'likert', gridLayout: 'dropdown', transposeTable: false };
109
- } else {
110
- q = { ...q, statementLayout: 'likert', gridLayout: 'standard', transposeTable: false };
111
- }
112
- }
113
-
114
- if (q.type === 'RANK_ORDER') {
115
- if (previewState.rankVariant === 'drag' || previewKey === 'RANK_drag') {
116
- q = { ...q, interactionMode: 'dragAndDrop', optionDisplay: 'textAndImage' };
117
- }
118
- if (previewState.rankVariant === 'dropdown' || previewKey === 'RANK_dropdown') {
119
- q = { ...q, interactionMode: 'dropdown', optionDisplay: 'textAndImage' };
120
- }
121
- }
122
-
123
- q = applyMatrixRowCount(q, previewState.multiStatement);
124
-
125
- return q;
126
- }, [question, previewState]);
35
+ const mergedQuestion = useMemo(
36
+ () => mergePreviewQuestion(question, previewState),
37
+ [question, previewState],
38
+ );
127
39
 
128
40
  const heatmapInitial =
129
41
  previewState.heatmapPreviewPin && mergedQuestion.type === 'HEATMAP'
@@ -136,16 +48,25 @@ function QuestionPreviewInner({ question, initialValue }: QuestionPreviewProps)
136
48
  ]
137
49
  : initialValue;
138
50
 
51
+ const rankDropdownInitial =
52
+ mergedQuestion.type === 'RANK_ORDER' &&
53
+ (mergedQuestion as { interactionMode?: string }).interactionMode === 'dropdown' &&
54
+ value === undefined
55
+ ? { o1: 1 }
56
+ : undefined;
57
+
58
+ const resolvedValue = heatmapInitial ?? rankDropdownInitial ?? value;
59
+
139
60
  return (
140
61
  <div
141
- className="mx-auto max-w-4xl px-4 py-6"
142
- style={{ fontFamily: 'var(--cfm-font-family)' }}
62
+ className="mx-auto w-full min-w-0 max-w-full overflow-x-auto px-4 py-6 box-border"
63
+ style={{ fontFamily: 'var(--cfm-font-family)', maxWidth: 'min(100%, var(--cfm-max-width, 48rem))' }}
143
64
  data-cfm-question-area
144
65
  >
145
66
  <Question
146
67
  key={configRevision}
147
68
  question={mergedQuestion}
148
- selectedValue={heatmapInitial ?? value}
69
+ selectedValue={resolvedValue}
149
70
  allAnswers={{}}
150
71
  allQuestions={[mergedQuestion]}
151
72
  onSelect={setValue}
@@ -1,26 +1,43 @@
1
- import { useState } from 'react';
2
- import type { AnswerValue } from '@explorer02/cfm-survey-sdk';
1
+ import { useMemo, useState } from 'react';
2
+ import type { AnswerValue, MatrixRowAnswers } from '@explorer02/cfm-survey-sdk';
3
3
  import SurveyStickyChrome from '@/components/SurveyStickyChrome';
4
4
  import LanguageSelector from '@/components/LanguageSelector';
5
5
  import Question from '@/components/Question';
6
6
  import Footer from '@/components/Footer';
7
7
  import { PreviewConfigProvider, usePreviewConfig } from './PreviewConfigContext';
8
+ import { mergeSurveyPageQuestion } from './mergePreviewQuestion';
8
9
  import {
9
10
  FIXTURE_SURVEY_PAGE_MCQ,
11
+ FIXTURE_SURVEY_PAGE_CFM_LIKERT,
10
12
  FIXTURE_SURVEY_PAGE_NPS,
11
13
  PREVIEW_LANGUAGES,
12
14
  } from './fixtures/questions';
13
15
 
14
16
  function SurveyPagePreviewInner() {
15
- const { configRevision } = usePreviewConfig();
17
+ const { previewState, configRevision } = usePreviewConfig();
16
18
  const [lang, setLang] = useState('en');
17
19
  const [mcqAnswer, setMcqAnswer] = useState<AnswerValue>('a');
20
+ const [matrixAnswer, setMatrixAnswer] = useState<MatrixRowAnswers>({});
18
21
  const [npsAnswer, setNpsAnswer] = useState<AnswerValue>(8);
19
22
 
20
- const allQuestions = [FIXTURE_SURVEY_PAGE_MCQ, FIXTURE_SURVEY_PAGE_NPS];
23
+ const mcqQuestion = useMemo(
24
+ () => mergeSurveyPageQuestion(FIXTURE_SURVEY_PAGE_MCQ, previewState),
25
+ [previewState],
26
+ );
27
+ const matrixQuestion = useMemo(
28
+ () => mergeSurveyPageQuestion(FIXTURE_SURVEY_PAGE_CFM_LIKERT, previewState),
29
+ [previewState],
30
+ );
31
+ const npsQuestion = useMemo(
32
+ () => mergeSurveyPageQuestion(FIXTURE_SURVEY_PAGE_NPS, previewState),
33
+ [previewState],
34
+ );
35
+
36
+ const allQuestions = [mcqQuestion, matrixQuestion, npsQuestion];
21
37
  const allAnswers = {
22
- [FIXTURE_SURVEY_PAGE_MCQ.id]: mcqAnswer,
23
- [FIXTURE_SURVEY_PAGE_NPS.id]: npsAnswer,
38
+ [mcqQuestion.id]: mcqAnswer,
39
+ [matrixQuestion.id]: matrixAnswer,
40
+ [npsQuestion.id]: npsAnswer,
24
41
  };
25
42
 
26
43
  return (
@@ -45,14 +62,21 @@ function SurveyPagePreviewInner() {
45
62
 
46
63
  <div className="space-y-8" key={configRevision}>
47
64
  <Question
48
- question={FIXTURE_SURVEY_PAGE_MCQ}
65
+ question={mcqQuestion}
49
66
  selectedValue={mcqAnswer}
50
67
  allAnswers={allAnswers}
51
68
  allQuestions={allQuestions}
52
69
  onSelect={setMcqAnswer}
53
70
  />
54
71
  <Question
55
- question={FIXTURE_SURVEY_PAGE_NPS}
72
+ question={matrixQuestion}
73
+ selectedValue={matrixAnswer}
74
+ allAnswers={allAnswers}
75
+ allQuestions={allQuestions}
76
+ onSelect={setMatrixAnswer}
77
+ />
78
+ <Question
79
+ question={npsQuestion}
56
80
  selectedValue={npsAnswer}
57
81
  allAnswers={allAnswers}
58
82
  allQuestions={allQuestions}
@@ -19,14 +19,37 @@ const LIKERT_COLUMNS = [
19
19
  { id: 'c5', label: 'Strongly Agree' },
20
20
  ];
21
21
 
22
- const MATRIX_ROW_SINGLE = [{ id: 'r1', statementText: 'Statement Item 1' }];
22
+ export const MATRIX_ROW_SINGLE = [{ id: 'r1', statementText: 'Statement Item 1' }];
23
23
 
24
- const MATRIX_ROWS_MULTI = [
24
+ export const MATRIX_ROWS_MULTI = [
25
25
  { id: 'r1', statementText: 'Statement Item 1' },
26
26
  { id: 'r2', statementText: 'Statement Item 2' },
27
27
  { id: 'r3', statementText: 'Statement Item 3' },
28
28
  ];
29
29
 
30
+ /** Bipolar matrix preview rows — left statement + matching right opposite per row. */
31
+ export const BIPOLAR_ROWS_SINGLE = [
32
+ { id: 'r1', statementText: 'Statement 1', oppositeStatementText: 'Opposite 1' },
33
+ ];
34
+
35
+ export const BIPOLAR_ROWS_MULTI = [
36
+ { id: 'r1', statementText: 'Statement 1', oppositeStatementText: 'Opposite 1' },
37
+ { id: 'r2', statementText: 'Statement 2', oppositeStatementText: 'Opposite 2' },
38
+ { id: 'r3', statementText: 'Statement 3', oppositeStatementText: 'Opposite 3' },
39
+ ];
40
+
41
+ const SLIDER_ROW_DEFAULTS = { min: 0, max: 10, step: 1 };
42
+
43
+ export const SLIDER_ROWS_SINGLE = [
44
+ { id: 'r1', statementText: 'Statement Item 1', ...SLIDER_ROW_DEFAULTS },
45
+ ];
46
+
47
+ export const SLIDER_ROWS_MULTI = [
48
+ { id: 'r1', statementText: 'Statement Item 1', ...SLIDER_ROW_DEFAULTS },
49
+ { id: 'r2', statementText: 'Statement Item 2', ...SLIDER_ROW_DEFAULTS },
50
+ { id: 'r3', statementText: 'Statement Item 3', ...SLIDER_ROW_DEFAULTS },
51
+ ];
52
+
30
53
  const SLIDER_TICKS = Array.from({ length: 11 }, (_, i) => ({
31
54
  tickPosition: i,
32
55
  tickLabel: String(i),
@@ -79,7 +102,7 @@ export const FIXTURE_NPS = base({
79
102
  type: 'NPS_SCALE',
80
103
  questionText:
81
104
  'Based on your experience, how likely are you to recommend [Company/Product/Service] to your friends and family?',
82
- buttonStyle: 'standard',
105
+ buttonStyle: 'numbered',
83
106
  minLabel: 'Not Likely At All',
84
107
  maxLabel: 'Extremely Likely',
85
108
  options: Array.from({ length: 11 }, (_, i) => ({
@@ -104,8 +127,8 @@ export const FIXTURE_SURVEY_PAGE_MCQ = base({
104
127
  export const FIXTURE_SURVEY_PAGE_NPS = base({
105
128
  id: 'preview-survey-nps',
106
129
  type: 'NPS_SCALE',
107
- questionNumber: 6,
108
- buttonStyle: 'standard',
130
+ questionNumber: 3,
131
+ buttonStyle: 'numbered',
109
132
  questionText:
110
133
  'Based on your experience, how likely are you to recommend [Company/Product/Service] to your friends and family?',
111
134
  minLabel: 'Not Likely At All',
@@ -116,6 +139,23 @@ export const FIXTURE_SURVEY_PAGE_NPS = base({
116
139
  })),
117
140
  });
118
141
 
142
+ /** Likert matrix with 3 statements — used on survey_page preview for zebra row styling. */
143
+ export const FIXTURE_SURVEY_PAGE_CFM_LIKERT = base({
144
+ id: 'preview-survey-cfm-likert',
145
+ type: 'CFM_MATRIX',
146
+ questionNumber: 2,
147
+ questionText: 'Please rate each statement below.',
148
+ statementLayout: 'likert',
149
+ gridLayout: 'standard',
150
+ transposeTable: false,
151
+ selectionMode: 'single',
152
+ showColumnHeaders: true,
153
+ repeatColumnHeaders: false,
154
+ hasNotApplicableOption: false,
155
+ statementRows: MATRIX_ROWS_MULTI,
156
+ scaleColumns: LIKERT_COLUMNS,
157
+ });
158
+
119
159
  export const FIXTURE_TEXT_SHORT = base({
120
160
  type: 'TEXTFIELD',
121
161
  questionText: 'Question',
@@ -154,13 +194,7 @@ export const FIXTURE_CFM_BIPOLAR = base({
154
194
  showColumnHeaders: false,
155
195
  repeatColumnHeaders: false,
156
196
  hasNotApplicableOption: false,
157
- statementRows: [
158
- {
159
- id: 'r1',
160
- statementText: 'Difficult',
161
- oppositeStatementText: 'Easy',
162
- },
163
- ],
197
+ statementRows: BIPOLAR_ROWS_SINGLE,
164
198
  scaleColumns: Array.from({ length: 5 }, (_, i) => ({ id: `c${i}`, label: '' })),
165
199
  scaleAnchorLabels: ['Hard', '', '', '', 'Easy'],
166
200
  });
@@ -333,10 +367,7 @@ export const FIXTURE_RATING_SLIDER = base({
333
367
 
334
368
  export const FIXTURE_SLIDER = base({
335
369
  type: 'SLIDER_MATRIX',
336
- statementRows: [
337
- { id: 'r1', statementText: 'Statement Item 1', min: 0, max: 10, step: 1 },
338
- { id: 'r2', statementText: 'Statement Item 2', min: 0, max: 10, step: 1 },
339
- ],
370
+ statementRows: SLIDER_ROWS_MULTI.slice(0, 2),
340
371
  scaleAnchorLabels: ['Label 1', 'Label 2'],
341
372
  ticks: {
342
373
  count: 11,
@@ -9,8 +9,14 @@ body {
9
9
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
10
10
  color: #262626;
11
11
  background-color: #ffffff;
12
+ max-width: 100%;
13
+ overflow-x: hidden;
14
+ box-sizing: border-box;
12
15
  }
13
16
 
14
17
  #root {
15
18
  min-height: 100vh;
19
+ max-width: 100%;
20
+ overflow-x: hidden;
21
+ box-sizing: border-box;
16
22
  }
@@ -0,0 +1,173 @@
1
+ import type { SurveyQuestion } from '@explorer02/cfm-survey-sdk';
2
+ import type { PreviewStatePatch } from './PreviewConfigContext';
3
+ import {
4
+ BIPOLAR_ROWS_MULTI,
5
+ BIPOLAR_ROWS_SINGLE,
6
+ MATRIX_ROWS_MULTI,
7
+ MATRIX_ROW_SINGLE,
8
+ SLIDER_ROWS_MULTI,
9
+ SLIDER_ROWS_SINGLE,
10
+ } from './fixtures/questions';
11
+
12
+ function mapNpsButtonStyle(style: string | undefined): SurveyQuestion['buttonStyle'] {
13
+ if (style === 'numbered' || style === 'pill' || style === 'emoji' || style === 'standard') {
14
+ return style;
15
+ }
16
+ return 'numbered';
17
+ }
18
+
19
+ /** Survey page always shows multi-row matrix for zebra styling. */
20
+ export function normalizeSurveyPagePreviewState(previewState: PreviewStatePatch): PreviewStatePatch {
21
+ return {
22
+ ...previewState,
23
+ activePreviewKey: 'survey_page',
24
+ multiStatement: true,
25
+ };
26
+ }
27
+
28
+ type MatrixRowCapable = SurveyQuestion & {
29
+ statementRows?: { id: string; statementText: string; oppositeStatementText?: string }[];
30
+ statementLayout?: string;
31
+ };
32
+
33
+ const MULTI_STATEMENT_MATRIX_TYPES = new Set([
34
+ 'CFM_MATRIX',
35
+ 'CSAT_MATRIX',
36
+ 'RATING_MATRIX',
37
+ 'SLIDER_MATRIX',
38
+ ]);
39
+
40
+ export function applyMatrixRowCount<T extends MatrixRowCapable>(
41
+ q: T,
42
+ multiStatement: boolean | undefined,
43
+ options?: { preserveSurveyPageRows?: boolean },
44
+ ): T {
45
+ if (!MULTI_STATEMENT_MATRIX_TYPES.has(q.type)) return q;
46
+ if (!('statementRows' in q) || !Array.isArray(q.statementRows)) return q;
47
+
48
+ const isBipolar = q.type === 'CFM_MATRIX' && q.statementLayout === 'bipolar';
49
+ const isSlider = q.type === 'SLIDER_MATRIX';
50
+
51
+ if (options?.preserveSurveyPageRows && q.statementRows.length >= 2) return q;
52
+
53
+ if (multiStatement === true) {
54
+ if (isBipolar) {
55
+ return { ...q, statementRows: BIPOLAR_ROWS_MULTI } as T;
56
+ }
57
+ if (isSlider) {
58
+ return { ...q, statementRows: SLIDER_ROWS_MULTI } as T;
59
+ }
60
+ if (q.statementRows.length >= 3) return q;
61
+ return { ...q, statementRows: MATRIX_ROWS_MULTI } as T;
62
+ }
63
+
64
+ if (multiStatement === false) {
65
+ if (isBipolar) {
66
+ return { ...q, statementRows: BIPOLAR_ROWS_SINGLE } as T;
67
+ }
68
+ if (isSlider) {
69
+ return { ...q, statementRows: SLIDER_ROWS_SINGLE } as T;
70
+ }
71
+ return { ...q, statementRows: [...MATRIX_ROW_SINGLE] } as T;
72
+ }
73
+
74
+ // Before bridge snapshot — keep fixture row count (single-row fixtures stay single).
75
+ return q;
76
+ }
77
+
78
+ function applyCfmMatrixLayout(
79
+ q: SurveyQuestion & {
80
+ statementLayout?: string;
81
+ gridLayout?: string;
82
+ transposeTable?: boolean;
83
+ showColumnHeaders?: boolean;
84
+ },
85
+ previewKey: string,
86
+ ): SurveyQuestion {
87
+ if (previewKey === 'survey_page' || previewKey === 'CFM_likert' || !previewKey.startsWith('CFM_')) {
88
+ return {
89
+ ...q,
90
+ statementLayout: 'likert',
91
+ gridLayout: 'standard',
92
+ transposeTable: false,
93
+ showColumnHeaders: true,
94
+ } as SurveyQuestion;
95
+ }
96
+ if (previewKey === 'CFM_bipolar') {
97
+ return {
98
+ ...q,
99
+ statementLayout: 'bipolar',
100
+ showColumnHeaders: false,
101
+ gridLayout: 'standard',
102
+ transposeTable: false,
103
+ } as SurveyQuestion;
104
+ }
105
+ if (previewKey === 'CFM_transpose') {
106
+ return { ...q, statementLayout: 'likert', transposeTable: true, gridLayout: 'standard' } as SurveyQuestion;
107
+ }
108
+ if (previewKey === 'CFM_carousel') {
109
+ return { ...q, statementLayout: 'likert', gridLayout: 'carousel', transposeTable: false } as SurveyQuestion;
110
+ }
111
+ if (previewKey === 'CFM_dropdown') {
112
+ return { ...q, statementLayout: 'likert', gridLayout: 'dropdown', transposeTable: false } as SurveyQuestion;
113
+ }
114
+ return { ...q, statementLayout: 'likert', gridLayout: 'standard', transposeTable: false } as SurveyQuestion;
115
+ }
116
+
117
+ /** Apply wizard bridge previewState onto a fixture question. */
118
+ export function mergePreviewQuestion(
119
+ question: SurveyQuestion,
120
+ previewState: PreviewStatePatch,
121
+ ): SurveyQuestion {
122
+ let q = { ...question } as SurveyQuestion;
123
+ const previewKey = previewState.activePreviewKey ?? '';
124
+ const isSurveyPage = previewKey === 'survey_page';
125
+
126
+ if (previewState.textfieldPlaceholder && q.type === 'TEXTFIELD') {
127
+ q = { ...q, placeholder: previewState.textfieldPlaceholder };
128
+ }
129
+
130
+ if (q.type === 'NPS_SCALE') {
131
+ q = {
132
+ ...q,
133
+ buttonStyle: mapNpsButtonStyle(previewState.npsButtonStyle ?? q.buttonStyle ?? 'numbered'),
134
+ minLabel: previewState.hintMinText ?? q.minLabel,
135
+ maxLabel: previewState.hintMaxText ?? q.maxLabel,
136
+ };
137
+ } else if (previewState.hintMinText !== undefined || previewState.hintMaxText !== undefined) {
138
+ if (q.type === 'SLIDER_MATRIX') {
139
+ q = {
140
+ ...q,
141
+ scaleAnchorLabels: [
142
+ previewState.hintMinText ?? q.scaleAnchorLabels?.[0] ?? '',
143
+ previewState.hintMaxText ?? q.scaleAnchorLabels?.[1] ?? '',
144
+ ],
145
+ };
146
+ }
147
+ }
148
+
149
+ if (q.type === 'CFM_MATRIX') {
150
+ q = applyCfmMatrixLayout(q, isSurveyPage ? 'survey_page' : previewKey);
151
+ }
152
+
153
+ if (q.type === 'RANK_ORDER') {
154
+ if (previewState.rankVariant === 'drag' || previewKey === 'RANK_drag') {
155
+ q = { ...q, interactionMode: 'dragAndDrop', optionDisplay: 'textAndImage' };
156
+ }
157
+ if (previewState.rankVariant === 'dropdown' || previewKey === 'RANK_dropdown') {
158
+ q = { ...q, interactionMode: 'dropdown', optionDisplay: 'textAndImage' };
159
+ }
160
+ }
161
+
162
+ return applyMatrixRowCount(q, previewState.multiStatement, {
163
+ preserveSurveyPageRows: isSurveyPage,
164
+ });
165
+ }
166
+
167
+ /** Survey page layout — each question reflects its type-tab customization mapping. */
168
+ export function mergeSurveyPageQuestion(
169
+ question: SurveyQuestion,
170
+ previewState: PreviewStatePatch,
171
+ ): SurveyQuestion {
172
+ return mergePreviewQuestion(question, normalizeSurveyPagePreviewState(previewState));
173
+ }
@@ -106,3 +106,25 @@ html.cfm-live-edit input[data-cfm-textfield]::placeholder {
106
106
  color: var(--cfm-input-placeholder, #9ca3af) !important;
107
107
  opacity: 1;
108
108
  }
109
+
110
+ /* Keep every question-type preview inside the wizard iframe without horizontal bleed */
111
+ html.cfm-live-edit,
112
+ html.cfm-live-edit body,
113
+ html.cfm-live-edit #root {
114
+ max-width: 100%;
115
+ overflow-x: hidden;
116
+ box-sizing: border-box;
117
+ }
118
+
119
+ html.cfm-live-edit [data-cfm-question-area],
120
+ html.cfm-live-edit .cfm-question-card {
121
+ max-width: 100%;
122
+ min-width: 0;
123
+ overflow-x: auto;
124
+ box-sizing: border-box;
125
+ }
126
+
127
+ html.cfm-live-edit [data-cfm-nps-area] {
128
+ max-width: 100%;
129
+ overflow-x: auto;
130
+ }
@@ -43,6 +43,8 @@ function flushCssVars(): void {
43
43
  delete pendingVars[key];
44
44
  }
45
45
  applyThemeSideEffects();
46
+ syncCompanyNameVisibility();
47
+ syncQuestionChromeVisibility();
46
48
  }
47
49
 
48
50
  function queueCssVars(vars: Record<string, string>): void {
@@ -58,6 +60,8 @@ function applyCssVarsImmediate(vars: Record<string, string>): void {
58
60
  root.style.setProperty(key, value);
59
61
  }
60
62
  applyThemeSideEffects();
63
+ syncCompanyNameVisibility();
64
+ syncQuestionChromeVisibility();
61
65
  }
62
66
 
63
67
  function setDisplay(selector: string, visible: boolean): void {
@@ -66,6 +70,58 @@ function setDisplay(selector: string, visible: boolean): void {
66
70
  });
67
71
  }
68
72
 
73
+ function readCssVar(name: string, fallback: string): string {
74
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
75
+ }
76
+
77
+ function isHeaderLogoVisible(): boolean {
78
+ const logoImg = document.querySelector(
79
+ 'header [data-cfm-logo], .cfm-header [data-cfm-logo]',
80
+ ) as HTMLImageElement | null;
81
+ return Boolean(
82
+ logoImg &&
83
+ logoImg.style.display !== 'none' &&
84
+ Boolean(logoImg.getAttribute('src')),
85
+ );
86
+ }
87
+
88
+ /** Keep [data-cfm-company] beside logo when toggle, name, and logo are all present. */
89
+ function syncCompanyNameVisibility(layoutShowCompany?: boolean): void {
90
+ const showFromCss = readCssVar('--cfm-header-show-company', '1') !== '0';
91
+ const showToggle =
92
+ layoutShowCompany !== undefined ? Boolean(layoutShowCompany) : showFromCss;
93
+
94
+ document.querySelectorAll('[data-cfm-company]').forEach((el) => {
95
+ const htmlEl = el as HTMLElement;
96
+ const hasName = Boolean(htmlEl.textContent?.trim());
97
+ const show = showToggle && hasName && isHeaderLogoVisible();
98
+ htmlEl.style.display = show ? '' : 'none';
99
+ });
100
+ }
101
+
102
+ function syncQuestionChromeVisibility(
103
+ layoutShowNumbers?: boolean,
104
+ layoutShowRequired?: boolean,
105
+ ): void {
106
+ const showNumbers =
107
+ layoutShowNumbers !== undefined
108
+ ? Boolean(layoutShowNumbers)
109
+ : readCssVar('--cfm-show-question-numbers', '1') !== '0';
110
+ const showRequired =
111
+ layoutShowRequired !== undefined
112
+ ? Boolean(layoutShowRequired)
113
+ : readCssVar('--cfm-show-required-asterisk', '1') !== '0';
114
+
115
+ document.querySelectorAll('[data-cfm-question-number]').forEach((el) => {
116
+ const htmlEl = el as HTMLElement;
117
+ const hasNumber = Boolean(htmlEl.textContent?.trim());
118
+ htmlEl.style.display = showNumbers && hasNumber ? '' : 'none';
119
+ });
120
+ document.querySelectorAll('[data-cfm-required]').forEach((el) => {
121
+ (el as HTMLElement).style.display = showRequired ? '' : 'none';
122
+ });
123
+ }
124
+
69
125
  function applyContentPatch(patch: ContentPatch): void {
70
126
  if (patch.logoUrl !== undefined) {
71
127
  document.querySelectorAll('[data-cfm-logo], header img, footer img').forEach((el) => {
@@ -88,23 +144,12 @@ function applyContentPatch(patch: ContentPatch): void {
88
144
  htmlEl.classList.remove('hidden');
89
145
  }
90
146
  });
91
- document.querySelectorAll('[data-cfm-company]').forEach((el) => {
92
- const htmlEl = el as HTMLElement;
93
- if (patch.logoUrl) {
94
- if (!htmlEl.textContent?.trim()) {
95
- htmlEl.style.display = 'none';
96
- }
97
- }
98
- });
99
147
  }
100
148
 
101
149
  if (patch.companyName !== undefined) {
102
150
  const name = patch.companyName ?? '';
103
151
  document.querySelectorAll('[data-cfm-company]').forEach((el) => {
104
152
  (el as HTMLElement).textContent = name;
105
- if (!name) {
106
- (el as HTMLElement).style.display = 'none';
107
- }
108
153
  });
109
154
  document.querySelectorAll('[data-cfm-logo-text]').forEach((el) => {
110
155
  const logoImg = document.querySelector('[data-cfm-logo]') as HTMLImageElement | null;
@@ -167,7 +212,7 @@ function applyContentPatch(patch: ContentPatch): void {
167
212
  if (patch.layoutFlags) {
168
213
  const flags = patch.layoutFlags;
169
214
  if (flags.showCompanyName !== undefined) {
170
- setDisplay('[data-cfm-company]', Boolean(flags.showCompanyName));
215
+ syncCompanyNameVisibility(Boolean(flags.showCompanyName));
171
216
  }
172
217
  if (flags.showHeader !== undefined) {
173
218
  setDisplay('.cfm-header, header.cfm-header', Boolean(flags.showHeader));
@@ -188,10 +233,10 @@ function applyContentPatch(patch: ContentPatch): void {
188
233
  setDisplay('[data-cfm-btn-back]', Boolean(flags.showBackButton));
189
234
  }
190
235
  if (flags.showQuestionNumbers !== undefined) {
191
- setDisplay('[data-cfm-question-number]', Boolean(flags.showQuestionNumbers));
236
+ syncQuestionChromeVisibility(Boolean(flags.showQuestionNumbers), undefined);
192
237
  }
193
238
  if (flags.showRequiredAsterisk !== undefined) {
194
- setDisplay('[data-cfm-required]', Boolean(flags.showRequiredAsterisk));
239
+ syncQuestionChromeVisibility(undefined, Boolean(flags.showRequiredAsterisk));
195
240
  }
196
241
  if (flags.showFooterLogo !== undefined) {
197
242
  setDisplay('[data-cfm-footer-logo]', Boolean(flags.showFooterLogo));
@@ -229,15 +274,13 @@ function applyContentPatch(patch: ContentPatch): void {
229
274
  (el as HTMLElement).style.display = show ? '' : 'none';
230
275
  });
231
276
  }
277
+ syncCompanyNameVisibility();
278
+ syncQuestionChromeVisibility();
232
279
  applyThemeSideEffects();
233
280
  }
234
281
 
235
282
  const TRAFFIC_BADGE = ['#ef4444', '#f97316', '#eab308', '#84cc16', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7'];
236
283
 
237
- function readCssVar(name: string, fallback: string): string {
238
- return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
239
- }
240
-
241
284
  function applyThemeSideEffects(): void {
242
285
  const mode = readCssVar('--cfm-number-label-mode', 'traffic');
243
286
  const mono = readCssVar('--cfm-number-mono-color', '#9ca3af');