@explorer02/cfm-survey-sdk 0.3.9 → 0.4.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 (31) hide show
  1. package/dist/cli/index.js +64 -64
  2. package/dist/cli/index.mjs +63 -63
  3. package/package.json +1 -1
  4. package/templates/docs/templates/CsatMatrixScale.tsx +105 -36
  5. package/templates/docs/templates/CustomSliderTrack.tsx +2 -2
  6. package/templates/docs/templates/Footer.tsx +3 -2
  7. package/templates/docs/templates/Header.tsx +14 -14
  8. package/templates/docs/templates/LikertMatrixScale.tsx +2 -2
  9. package/templates/docs/templates/Question.tsx +30 -23
  10. package/templates/docs/templates/RankOrderScale.tsx +10 -2
  11. package/templates/docs/templates/SliderMatrixScale.tsx +1 -1
  12. package/templates/docs/templates/labelStyles.ts +27 -0
  13. package/templates/docs/templates/uiConfig.ts +100 -0
  14. package/templates/preview-harness/preview-bridge.inline.js +55 -17
  15. package/templates/preview-harness/previewPages.js +2 -2
  16. package/templates/preview-harness/previewPages.ts +1 -1
  17. package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +60 -7
  18. package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +17 -96
  19. package/templates/preview-harness/vite-app/src/SurveyPagePreview.tsx +32 -8
  20. package/templates/preview-harness/vite-app/src/fixtures/questions.ts +53 -25
  21. package/templates/preview-harness/vite-app/src/globals.css +6 -0
  22. package/templates/preview-harness/vite-app/src/mergePreviewQuestion.ts +233 -0
  23. package/templates/preview-harness/vite-app/src/preview-live-overrides.css +30 -0
  24. package/templates/previewBridge.ts +61 -18
  25. package/templates/survey-theme.css +16 -0
  26. package/templates/wizard-dist/assets/{PreviewMock-CS4WqhDB.js → PreviewMock-MsONyaq9.js} +1 -1
  27. package/templates/wizard-dist/assets/TypePanel-D24BxbHk.js +1 -0
  28. package/templates/wizard-dist/assets/index-C7RSJr09.js +34 -0
  29. package/templates/wizard-dist/index.html +1 -1
  30. package/templates/wizard-dist/assets/TypePanel-BgTW2c74.js +0 -1
  31. package/templates/wizard-dist/assets/index-hjm-fxWO.js +0 -34
@@ -17,10 +17,12 @@
17
17
  delete pending[k];
18
18
  }
19
19
  }
20
- applyThemeSideEffects();
21
- }
20
+ applyThemeSideEffects();
21
+ syncCompanyNameVisibility();
22
+ syncQuestionChromeVisibility();
23
+ }
22
24
 
23
- function queue(vars) {
25
+ function queue(vars) {
24
26
  for (var k in vars) {
25
27
  if (Object.prototype.hasOwnProperty.call(vars, k)) pending[k] = vars[k];
26
28
  }
@@ -34,10 +36,12 @@
34
36
  root.style.setProperty(k, vars[k]);
35
37
  }
36
38
  }
37
- applyThemeSideEffects();
38
- }
39
+ applyThemeSideEffects();
40
+ syncCompanyNameVisibility();
41
+ syncQuestionChromeVisibility();
42
+ }
39
43
 
40
- function setDisplay(selector, visible) {
44
+ function setDisplay(selector, visible) {
41
45
  document.querySelectorAll(selector).forEach(function (el) {
42
46
  el.style.display = visible ? '' : 'none';
43
47
  });
@@ -81,6 +85,46 @@
81
85
  applyDropzoneStyle();
82
86
  }
83
87
 
88
+ function isHeaderLogoVisible() {
89
+ var logoImg = document.querySelector('header [data-cfm-logo], .cfm-header [data-cfm-logo]');
90
+ return Boolean(
91
+ logoImg &&
92
+ logoImg.style.display !== 'none' &&
93
+ logoImg.getAttribute('src'),
94
+ );
95
+ }
96
+
97
+ function syncCompanyNameVisibility(layoutShowCompany) {
98
+ var showFromCss = readVar('--cfm-header-show-company', '1') !== '0';
99
+ var showToggle =
100
+ layoutShowCompany !== undefined ? !!layoutShowCompany : showFromCss;
101
+
102
+ document.querySelectorAll('[data-cfm-company]').forEach(function (el) {
103
+ var hasName = Boolean((el.textContent || '').trim());
104
+ var show = showToggle && hasName && isHeaderLogoVisible();
105
+ el.style.display = show ? '' : 'none';
106
+ });
107
+ }
108
+
109
+ function syncQuestionChromeVisibility(layoutShowNumbers, layoutShowRequired) {
110
+ var showNumbers =
111
+ layoutShowNumbers !== undefined
112
+ ? !!layoutShowNumbers
113
+ : readVar('--cfm-show-question-numbers', '1') !== '0';
114
+ var showRequired =
115
+ layoutShowRequired !== undefined
116
+ ? !!layoutShowRequired
117
+ : readVar('--cfm-show-required-asterisk', '1') !== '0';
118
+
119
+ document.querySelectorAll('[data-cfm-question-number]').forEach(function (el) {
120
+ var hasNumber = Boolean((el.textContent || '').trim());
121
+ el.style.display = showNumbers && hasNumber ? '' : 'none';
122
+ });
123
+ document.querySelectorAll('[data-cfm-required]').forEach(function (el) {
124
+ el.style.display = showRequired ? '' : 'none';
125
+ });
126
+ }
127
+
84
128
  function applyContent(patch) {
85
129
  if (!patch) return;
86
130
  if (patch.logoUrl !== undefined) {
@@ -103,19 +147,11 @@
103
147
  el.classList.remove('hidden');
104
148
  }
105
149
  });
106
- document.querySelectorAll('[data-cfm-company]').forEach(function (el) {
107
- if (patch.logoUrl && !el.textContent) {
108
- el.style.display = 'none';
109
- }
110
- });
111
150
  }
112
151
  if (patch.companyName !== undefined) {
113
152
  var name = patch.companyName || '';
114
153
  document.querySelectorAll('[data-cfm-company]').forEach(function (el) {
115
154
  el.textContent = name;
116
- if (!name) {
117
- el.style.display = 'none';
118
- }
119
155
  });
120
156
  document.querySelectorAll('[data-cfm-logo-text]').forEach(function (el) {
121
157
  var logoImg = document.querySelector('[data-cfm-logo]');
@@ -176,7 +212,7 @@
176
212
  if (patch.layoutFlags) {
177
213
  var flags = patch.layoutFlags;
178
214
  if (flags.showCompanyName !== undefined) {
179
- setDisplay('[data-cfm-company]', flags.showCompanyName);
215
+ syncCompanyNameVisibility(flags.showCompanyName);
180
216
  }
181
217
  if (flags.showHeader !== undefined) {
182
218
  setDisplay('.cfm-header, header.cfm-header', flags.showHeader);
@@ -197,10 +233,10 @@
197
233
  setDisplay('[data-cfm-btn-back]', flags.showBackButton);
198
234
  }
199
235
  if (flags.showQuestionNumbers !== undefined) {
200
- setDisplay('[data-cfm-question-number]', flags.showQuestionNumbers);
236
+ syncQuestionChromeVisibility(flags.showQuestionNumbers, undefined);
201
237
  }
202
238
  if (flags.showRequiredAsterisk !== undefined) {
203
- setDisplay('[data-cfm-required]', flags.showRequiredAsterisk);
239
+ syncQuestionChromeVisibility(undefined, flags.showRequiredAsterisk);
204
240
  }
205
241
  if (flags.showFooterLogo !== undefined) {
206
242
  setDisplay('[data-cfm-footer-logo]', flags.showFooterLogo);
@@ -234,6 +270,8 @@
234
270
  applyMultiStatement(!!patch.previewState.multiStatement);
235
271
  }
236
272
  }
273
+ syncCompanyNameVisibility();
274
+ syncQuestionChromeVisibility();
237
275
  applyThemeSideEffects();
238
276
  }
239
277
 
@@ -97,8 +97,8 @@ const PREVIEW_PAGE_DEFS = [
97
97
  { key: 'TEXTFIELD_short', title: 'Text Short', body: `<p style="font-weight:600;margin-bottom:8px">Your name</p><input class="cfm-input" data-cfm-textfield placeholder="Type here..." />` },
98
98
  { key: 'TEXTFIELD_long', title: 'Text Long', body: `<p style="font-weight:600;margin-bottom:8px">Comments</p><textarea class="cfm-input" data-cfm-textfield style="height:120px;padding:12px" placeholder="Type your response..."></textarea>` },
99
99
  { key: 'NPS_SCALE', title: 'NPS', body: npsPreview() },
100
- { key: 'CFM_likert', title: 'CFM Likert', body: `${matrixPreview('Statement item 1', 'radio')}<div class="cfm-matrix-row" data-cfm-matrix-row-extra style="display:none"><div class="cfm-matrix-label">Statement item 2</div><div class="cfm-matrix-cells">${cells(5)}</div></div>` },
101
- { key: 'CFM_bipolar', title: 'CFM Bipolar', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width,25%)">Left statement</div><div class="cfm-matrix-cells">${cells(5)}</div><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width,25%);text-align:right">Right statement</div></div>` },
100
+ { key: 'CFM_likert', title: 'CFM Likert', body: `${matrixPreview('Statement item 1', 'radio')}<div class="cfm-matrix-row" data-cfm-matrix-row-extra style="display:none"><div class="cfm-matrix-label">Statement item 2</div><div class="cfm-matrix-cells">${cells(5)}</div></div><div class="cfm-matrix-row" data-cfm-matrix-row-extra style="display:none"><div class="cfm-matrix-label">Statement item 3</div><div class="cfm-matrix-cells">${cells(5)}</div></div>` },
101
+ { key: 'CFM_bipolar', title: 'CFM Bipolar', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width,25%)">Statement 1</div><div class="cfm-matrix-cells">${cells(5)}</div><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width,25%);text-align:right">Opposite 1</div></div><div class="cfm-matrix-row" data-cfm-matrix-row-extra style="display:none"><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width,25%)">Statement 2</div><div class="cfm-matrix-cells">${cells(5)}</div><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width,25%);text-align:right">Opposite 2</div></div><div class="cfm-matrix-row" data-cfm-matrix-row-extra style="display:none"><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width,25%)">Statement 3</div><div class="cfm-matrix-cells">${cells(5)}</div><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width,25%);text-align:right">Opposite 3</div></div>` },
102
102
  { key: 'CFM_transpose', title: 'CFM Transpose', body: matrixPreview('Scale item (transposed row)', 'radio') },
103
103
  { key: 'CFM_carousel', title: 'CFM Carousel', body: `${matrixPreview('Row 1 of 3', 'radio')}<div style="text-align:center;margin-top:12px"><span style="color:var(--cfm-matrix-selected)">●</span> ○ ○</div>` },
104
104
  { key: 'CFM_dropdown', title: 'CFM Dropdown', body: `<div class="cfm-matrix-label" style="width:100%;margin-bottom:8px">Statement 1</div><select class="cfm-input"><option>Select...</option><option>Col 1</option><option>Col 2</option></select>` },
@@ -141,7 +141,7 @@ export const PREVIEW_PAGE_DEFS: PreviewPageDef[] = [
141
141
  {
142
142
  key: 'CFM_bipolar',
143
143
  title: 'CFM Bipolar',
144
- body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width)">Left statement</div><div class="cfm-matrix-cells">${cells(5)}</div><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width);text-align:right">Right statement</div></div>`,
144
+ body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width)">Statement 1</div><div class="cfm-matrix-cells">${cells(5)}</div><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width);text-align:right">Opposite 1</div></div><div class="cfm-matrix-row" data-cfm-matrix-row-extra style="display:none"><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width)">Statement 2</div><div class="cfm-matrix-cells">${cells(5)}</div><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width);text-align:right">Opposite 2</div></div><div class="cfm-matrix-row" data-cfm-matrix-row-extra style="display:none"><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width)">Statement 3</div><div class="cfm-matrix-cells">${cells(5)}</div><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width);text-align:right">Opposite 3</div></div>`,
145
145
  },
146
146
  {
147
147
  key: 'CFM_transpose',
@@ -15,6 +15,7 @@ export type PreviewStatePatch = {
15
15
 
16
16
  export const PREVIEW_BRIDGE_SNAPSHOT = 'CFM_UI_CONFIG_SNAPSHOT';
17
17
  export const PREVIEW_BRIDGE_DELTA = 'CFM_UI_CONFIG_DELTA';
18
+ export const PREVIEW_BRIDGE_READY = 'CFM_PREVIEW_READY';
18
19
 
19
20
  const TRAFFIC = [
20
21
  '#ef4444', '#f97316', '#eab308', '#84cc16', '#22c55e', '#14b8a6',
@@ -51,6 +52,26 @@ function applyThemeSideEffects(): void {
51
52
  applyDropzoneStyle();
52
53
  }
53
54
 
55
+ function previewStateFromCssVars(
56
+ cssVars: Record<string, string>,
57
+ prev: PreviewStatePatch,
58
+ ): PreviewStatePatch {
59
+ const next: PreviewStatePatch = { ...prev };
60
+ if (cssVars['--cfm-matrix-multi-statement'] !== undefined) {
61
+ next.multiStatement = cssVars['--cfm-matrix-multi-statement'] === '1';
62
+ }
63
+ if (cssVars['--cfm-nps-button-style']) {
64
+ next.npsButtonStyle = cssVars['--cfm-nps-button-style'];
65
+ }
66
+ if (cssVars['--cfm-hint-min-text']) {
67
+ next.hintMinText = cssVars['--cfm-hint-min-text'];
68
+ }
69
+ if (cssVars['--cfm-hint-max-text']) {
70
+ next.hintMaxText = cssVars['--cfm-hint-max-text'];
71
+ }
72
+ return next;
73
+ }
74
+
54
75
  type PreviewConfigContextValue = {
55
76
  previewState: PreviewStatePatch;
56
77
  configRevision: number;
@@ -66,26 +87,58 @@ export function PreviewConfigProvider({ children }: { children: ReactNode }) {
66
87
  const [configRevision, setConfigRevision] = useState(0);
67
88
 
68
89
  useEffect(() => {
90
+ const requestSnapshot = () => {
91
+ try {
92
+ window.parent.postMessage({ type: PREVIEW_BRIDGE_READY }, '*');
93
+ } catch {
94
+ /* ignore */
95
+ }
96
+ };
97
+
98
+ requestSnapshot();
99
+
69
100
  const handler = (event: MessageEvent) => {
70
101
  const data = event.data;
71
102
  if (!data?.type) return;
72
103
  if (data.type !== PREVIEW_BRIDGE_SNAPSHOT && data.type !== PREVIEW_BRIDGE_DELTA) return;
73
- if (data.contentPatch?.previewState) {
104
+
105
+ const isSnapshot = data.type === PREVIEW_BRIDGE_SNAPSHOT;
106
+ let shouldBumpRevision = isSnapshot;
107
+
108
+ if (data.cssVars) {
109
+ const root = document.documentElement;
110
+ for (const [key, value] of Object.entries(data.cssVars)) {
111
+ root.style.setProperty(key, value as string);
112
+ }
113
+ shouldBumpRevision = true;
114
+ }
115
+
116
+ if (isSnapshot) {
117
+ const patchState = (data.contentPatch?.previewState ?? {}) as PreviewStatePatch;
118
+ const nextState = data.cssVars
119
+ ? previewStateFromCssVars(data.cssVars, patchState)
120
+ : patchState;
121
+ setPreviewState(nextState);
122
+ shouldBumpRevision = true;
123
+ } else if (data.contentPatch?.previewState) {
74
124
  setPreviewState((prev) => ({ ...prev, ...data.contentPatch.previewState }));
125
+ shouldBumpRevision = true;
126
+ } else if (data.cssVars) {
127
+ setPreviewState((prev) => previewStateFromCssVars(data.cssVars, prev));
128
+ shouldBumpRevision = true;
75
129
  }
130
+
76
131
  if (data.contentPatch?.domAttributes) {
77
132
  const root = document.documentElement;
78
133
  for (const [key, value] of Object.entries(data.contentPatch.domAttributes)) {
79
134
  root.setAttribute(key, value as string);
80
135
  }
136
+ shouldBumpRevision = true;
81
137
  }
82
- if (data.cssVars) {
83
- const root = document.documentElement;
84
- for (const [key, value] of Object.entries(data.cssVars)) {
85
- root.style.setProperty(key, value as string);
86
- }
138
+
139
+ if (shouldBumpRevision) {
140
+ setConfigRevision((r) => r + 1);
87
141
  }
88
- setConfigRevision((r) => r + 1);
89
142
  applyThemeSideEffects();
90
143
  };
91
144
 
@@ -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, configRevision],
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, configRevision],
26
+ );
27
+ const matrixQuestion = useMemo(
28
+ () => mergeSurveyPageQuestion(FIXTURE_SURVEY_PAGE_CFM_LIKERT, previewState),
29
+ [previewState, configRevision],
30
+ );
31
+ const npsQuestion = useMemo(
32
+ () => mergeSurveyPageQuestion(FIXTURE_SURVEY_PAGE_NPS, previewState),
33
+ [previewState, configRevision],
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',
@@ -141,7 +181,7 @@ export const FIXTURE_CFM_LIKERT = base({
141
181
  showColumnHeaders: true,
142
182
  repeatColumnHeaders: false,
143
183
  hasNotApplicableOption: false,
144
- statementRows: MATRIX_ROWS_MULTI,
184
+ statementRows: MATRIX_ROW_SINGLE,
145
185
  scaleColumns: LIKERT_COLUMNS,
146
186
  });
147
187
 
@@ -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
  });
@@ -187,7 +221,7 @@ export const FIXTURE_CFM_CAROUSEL = base({
187
221
  showColumnHeaders: true,
188
222
  repeatColumnHeaders: false,
189
223
  hasNotApplicableOption: false,
190
- statementRows: MATRIX_ROWS_MULTI,
224
+ statementRows: MATRIX_ROW_SINGLE,
191
225
  scaleColumns: LIKERT_COLUMNS,
192
226
  });
193
227
 
@@ -233,7 +267,7 @@ export const FIXTURE_CSAT_NUMBERED = base({
233
267
  type: 'CSAT_MATRIX',
234
268
  displayStyle: 'numbered',
235
269
  gridLayout: 'standard',
236
- statementRows: MATRIX_ROWS_MULTI,
270
+ statementRows: MATRIX_ROW_SINGLE,
237
271
  scaleColumns: LIKERT_COLUMNS,
238
272
  scaleAnchorLabels: LIKERT_ANCHOR_LABELS,
239
273
  });
@@ -273,7 +307,7 @@ export const FIXTURE_RATING_STAR = base({
273
307
  displayStyle: 'star',
274
308
  gridLayout: 'standard',
275
309
  showColumnHeaders: true,
276
- statementRows: MATRIX_ROWS_MULTI,
310
+ statementRows: MATRIX_ROW_SINGLE,
277
311
  scaleColumns: RATING_TEN_COLUMNS,
278
312
  scaleAnchorLabels: LIKERT_ANCHOR_LABELS,
279
313
  });
@@ -282,7 +316,7 @@ export const FIXTURE_RATING_NUMBERED = base({
282
316
  type: 'RATING_MATRIX',
283
317
  displayStyle: 'numbered',
284
318
  gridLayout: 'standard',
285
- statementRows: MATRIX_ROWS_MULTI,
319
+ statementRows: MATRIX_ROW_SINGLE,
286
320
  scaleColumns: RATING_TEN_COLUMNS,
287
321
  scaleAnchorLabels: LIKERT_ANCHOR_LABELS,
288
322
  });
@@ -318,10 +352,7 @@ export const FIXTURE_RATING_SLIDER = base({
318
352
  type: 'RATING_MATRIX',
319
353
  displayStyle: 'graphics',
320
354
  gridLayout: 'standard',
321
- statementRows: [
322
- { id: 'r1', statementText: 'Statement Item 1' },
323
- { id: 'r2', statementText: 'Statement Item 2' },
324
- ],
355
+ statementRows: MATRIX_ROW_SINGLE,
325
356
  scaleColumns: [
326
357
  { id: 'c1', label: 'Likely' },
327
358
  { id: 'c2', label: 'Unlikely' },
@@ -333,10 +364,7 @@ export const FIXTURE_RATING_SLIDER = base({
333
364
 
334
365
  export const FIXTURE_SLIDER = base({
335
366
  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
- ],
367
+ statementRows: SLIDER_ROWS_MULTI.slice(0, 2),
340
368
  scaleAnchorLabels: ['Label 1', 'Label 2'],
341
369
  ticks: {
342
370
  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
  }