@explorer02/cfm-survey-sdk 0.2.3 → 0.2.5
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.
- package/dist/cli/index.js +141 -33
- package/dist/cli/index.mjs +141 -33
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +3 -1
- package/postinstall.js +9 -5
- package/templates/AGENT.md +60 -19
- package/templates/deploy-wizard-to-s3.sh +52 -0
- package/templates/docs/00-integration/agent-execution-flow.md +293 -0
- package/templates/docs/00-integration/agent-operating-contract.md +268 -0
- package/templates/docs/00-integration/apply-ui-config.md +390 -0
- package/templates/docs/00-integration/aws-deploy.md +117 -3
- package/templates/docs/00-integration/canonical-survey-fixtures.md +94 -0
- package/templates/docs/00-integration/client-integration-guide.md +9 -7
- package/templates/docs/00-integration/client-lib-folder.md +115 -0
- package/templates/docs/00-integration/component-checklist.md +90 -62
- package/templates/docs/00-integration/constraints.md +14 -5
- package/templates/docs/00-integration/custom-field-logic-and-navigation.md +56 -0
- package/templates/docs/00-integration/mockup-ui-extraction.md +113 -0
- package/templates/docs/00-integration/placeholders-and-custom-fields-wiring.md +192 -0
- package/templates/docs/00-integration/placeholders-and-tokens.md +2 -1
- package/templates/docs/00-integration/progress.md +4 -2
- package/templates/docs/00-integration/setup.md +12 -0
- package/templates/docs/00-integration/ui-customization-wizard.md +123 -0
- package/templates/docs/00-integration/useSurveySDK.md +35 -1
- package/templates/docs/00-integration/wizard-api.md +126 -0
- package/templates/docs/00-integration/wizard-config-handoff.md +292 -0
- package/templates/docs/00-integration/wizard-preview-build-guide.md +405 -0
- package/templates/docs/00-integration/wizard-question-type-styling.md +197 -0
- package/templates/docs/00-integration/wizard-troubleshooting.md +80 -0
- package/templates/docs/01-components/01-survey-page.md +18 -8
- package/templates/docs/01-components/02-question.md +2 -2
- package/templates/docs/01-components/03-rating-scale.md +12 -18
- package/templates/docs/01-components/05-csat-matrix-scale.md +18 -26
- package/templates/docs/01-components/06-likert-matrix-scale.md +19 -39
- package/templates/docs/01-components/07-slider-matrix-scale.md +48 -33
- package/templates/docs/01-components/09-custom-slider-track.md +15 -25
- package/templates/docs/01-components/10-header-footer.md +23 -4
- package/templates/docs/01-components/11-progress-bar.md +14 -6
- package/templates/docs/01-components/19-survey-sticky-chrome.md +79 -0
- package/templates/docs/01-components/README.md +2 -2
- package/templates/docs/02-reference/question-types/10-slider-matrix.md +38 -26
- package/templates/docs/02-reference/routing-table.md +1 -0
- package/templates/docs/02-reference/value-derivation.md +1 -1
- package/templates/docs/03-ui-specs/00-question-shell.md +10 -0
- package/templates/docs/03-ui-specs/01-rating.md +18 -0
- package/templates/docs/03-ui-specs/02-radio.md +16 -1
- package/templates/docs/03-ui-specs/03-text.md +12 -1
- package/templates/docs/03-ui-specs/04-csat.md +84 -50
- package/templates/docs/03-ui-specs/06-slider.md +3 -1
- package/templates/docs/03-ui-specs/07-matrix-cfm.md +71 -48
- package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +48 -18
- package/templates/docs/03-ui-specs/09-slider-matrix.md +59 -32
- package/templates/docs/03-ui-specs/10-file-upload.md +11 -0
- package/templates/docs/03-ui-specs/11-text-and-media.md +4 -0
- package/templates/docs/03-ui-specs/12-survey-chrome.md +54 -5
- package/templates/docs/03-ui-specs/13-heatmap.md +12 -1
- package/templates/docs/03-ui-specs/14-rank-order.md +11 -0
- package/templates/docs/03-ui-specs/README.md +4 -2
- package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +1 -1
- package/templates/docs/MANIFEST.json +180 -7
- package/templates/docs/index.md +114 -117
- package/templates/docs/templates/CsatMatrixScale.tsx +637 -0
- package/templates/docs/templates/FileUploadScale.tsx +262 -0
- package/templates/docs/templates/HeatmapScale.tsx +114 -0
- package/templates/docs/templates/LikertMatrixScale.tsx +414 -0
- package/templates/docs/templates/Question.tsx +3 -0
- package/templates/docs/templates/RatingScale.tsx +129 -0
- package/templates/docs/templates/SliderMatrixScale.tsx +248 -0
- package/templates/docs/templates/SurveyStickyChrome.tsx +24 -0
- package/templates/docs/templates/customFieldValues.ts +36 -0
- package/templates/docs/templates/deploy-to-aws.sh +74 -0
- package/templates/docs/templates/implementation_plan.md +64 -10
- package/templates/docs/templates/labelStyles.ts +33 -0
- package/templates/docs/templates/selectionStyles.ts +100 -0
- package/templates/docs/templates/surveyUiIcons.tsx +11 -0
- package/templates/docs/templates/surveyUiScaleUtils.ts +51 -0
- package/templates/docs/templates/verify-agent-build.sh +119 -0
- package/templates/preview-harness/preview-bridge.inline.js +201 -0
- package/templates/preview-harness/previewPages.js +108 -0
- package/templates/preview-harness/previewPages.ts +298 -0
- package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +67 -0
- package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +122 -0
- package/templates/preview-harness/vite-app/src/SurveyPagePreview.tsx +75 -0
- package/templates/preview-harness/vite-app/src/fixtures/questions.ts +311 -0
- package/templates/preview-harness/vite-app/src/globals.css +16 -0
- package/templates/preview-harness/vite-app/src/mount.tsx +9 -0
- package/templates/preview-harness/vite-app/src/preview-live-overrides.css +101 -0
- package/templates/preview-harness/vite-app/stubs/next-dynamic.tsx +25 -0
- package/templates/preview-harness/vite-app/stubs/next-image.tsx +29 -0
- package/templates/previewBridge.ts +153 -0
- package/templates/survey-theme.css +295 -0
- package/templates/survey-ui-config.schema.json +213 -0
- package/templates/wizard-dist/assets/PreviewMock-AoZdAEPZ.js +1 -0
- package/templates/wizard-dist/assets/TypePanel-39FnbtvG.js +1 -0
- package/templates/wizard-dist/assets/index-BhWM50Yu.css +1 -0
- package/templates/wizard-dist/assets/index-CWKdsJOp.js +34 -0
- package/templates/wizard-dist/assets/vendor-BwkXDkd3.js +17 -0
- package/templates/wizard-dist/index.html +20 -0
- package/templates/wizard.html +13 -1129
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/** Preview page HTML bodies keyed by manifest id */
|
|
2
|
+
export type PreviewPageDef = { key: string; title: string; body: string };
|
|
3
|
+
|
|
4
|
+
const sharedStyles = `
|
|
5
|
+
.cfm-header { height:var(--cfm-header-height); background:var(--cfm-header-bg); border-bottom:1px solid var(--cfm-header-border); display:flex; align-items:center; padding:0 var(--cfm-header-padding-x); box-shadow:0 2px 8px rgba(0,0,0,0.06); }
|
|
6
|
+
.cfm-header-logo { width:var(--cfm-header-logo-width); height:var(--cfm-header-logo-height); object-fit:contain; padding:var(--cfm-header-logo-padding); background:var(--cfm-primary); }
|
|
7
|
+
.cfm-header-company { font-size:var(--cfm-header-company-size); font-weight:var(--cfm-header-company-weight); color:var(--cfm-header-company-color); margin-left:16px; }
|
|
8
|
+
.cfm-footer { background:var(--cfm-footer-bg); color:var(--cfm-footer-text); padding:var(--cfm-footer-padding-y) var(--cfm-footer-padding-x); display:flex; flex-wrap:wrap; gap:16px; justify-content:space-between; align-items:flex-end; }
|
|
9
|
+
.cfm-footer-links { display:flex; gap:24px; flex-wrap:wrap; }
|
|
10
|
+
.cfm-footer-links a { color:var(--cfm-footer-link); text-decoration:none; font-size:12px; }
|
|
11
|
+
.cfm-progress { height:var(--cfm-progress-height); background:var(--cfm-progress-track); border-radius:999px; overflow:hidden; margin:var(--cfm-sticky-pt) 0 var(--cfm-sticky-pb); }
|
|
12
|
+
.cfm-progress-fill { height:100%; width:45%; background:var(--cfm-progress-fill); border-radius:999px; }
|
|
13
|
+
.cfm-mcq-option { border:2px solid #e5e7eb; border-radius:var(--cfm-mcq-border-radius); padding:var(--cfm-mcq-card-padding); margin-bottom:var(--cfm-mcq-option-gap); display:flex; align-items:center; gap:8px; }
|
|
14
|
+
.cfm-mcq-option.selected { border-color:var(--cfm-mcq-selected-accent); background:var(--cfm-mcq-selected-bg); }
|
|
15
|
+
.cfm-nps-row { display:flex; gap:var(--cfm-nps-cell-gap); flex-wrap:wrap; }
|
|
16
|
+
.cfm-nps-cell { width:var(--cfm-nps-cell-size); height:var(--cfm-nps-cell-size); border-radius:var(--cfm-border-radius); background:var(--cfm-nps-track-bar); display:flex; align-items:center; justify-content:center; font-size:14px; }
|
|
17
|
+
.cfm-nps-cell.selected { background:var(--cfm-nps-selected-fill); border:2px solid var(--cfm-accent); }
|
|
18
|
+
.cfm-matrix-row { display:flex; align-items:center; padding:8px 0; background:var(--cfm-row-band); margin-bottom:4px; }
|
|
19
|
+
.cfm-matrix-label { width:var(--cfm-matrix-row-width); flex-shrink:0; padding:0 16px; font-size:14px; font-weight:500; }
|
|
20
|
+
.cfm-matrix-cells { display:flex; gap:8px; flex:1; justify-content:center; }
|
|
21
|
+
.cfm-matrix-cell { width:var(--cfm-csat-cell-size); height:var(--cfm-csat-cell-size); border-radius:50%; border:2px solid #d1d5db; }
|
|
22
|
+
.cfm-matrix-cell.selected { border-color:var(--cfm-csat-ring-color); background:var(--cfm-selected-bg); }
|
|
23
|
+
.cfm-emoji { font-size:var(--cfm-csat-emoji-size); opacity:var(--cfm-csat-unselected-opacity); }
|
|
24
|
+
.cfm-emoji.selected { opacity:1; outline:2px solid var(--cfm-csat-ring-color); border-radius:8px; }
|
|
25
|
+
.cfm-star { color:var(--cfm-star-color); font-size:24px; opacity:var(--cfm-star-unselected-opacity); }
|
|
26
|
+
.cfm-star.selected { opacity:1; }
|
|
27
|
+
.cfm-badge { width:40px; height:40px; border-radius:8px; background:var(--cfm-badge-color); color:white; display:flex; align-items:center; justify-content:center; font-weight:600; }
|
|
28
|
+
.cfm-slider-track { height:8px; background:var(--cfm-slider-track); border-radius:4px; position:relative; margin:24px 0; }
|
|
29
|
+
.cfm-slider-thumb { width:20px; height:20px; background:var(--cfm-slider-thumb); border-radius:50%; position:absolute; top:-6px; left:60%; }
|
|
30
|
+
.cfm-tick { width:var(--cfm-slider-tick-size); height:var(--cfm-slider-tick-size); background:var(--cfm-slider-tick); border-radius:4px; font-size:11px; display:flex; align-items:center; justify-content:center; color:white; }
|
|
31
|
+
.cfm-dropzone { border:2px dashed var(--cfm-upload-accent); border-radius:var(--cfm-border-radius); padding:var(--cfm-upload-padding); text-align:center; color:var(--cfm-text); }
|
|
32
|
+
.cfm-input { width:100%; height:var(--cfm-input-height); border:1px solid var(--cfm-input-border); border-radius:var(--cfm-input-radius); padding:0 var(--cfm-input-padding); font-family:inherit; }
|
|
33
|
+
.cfm-input:focus { outline:2px solid var(--cfm-input-focus-ring); }
|
|
34
|
+
.cfm-rank-item { background:var(--cfm-rank-item-bg); padding:var(--cfm-rank-item-padding); margin-bottom:var(--cfm-rank-item-gap); border-radius:var(--cfm-border-radius); display:flex; align-items:center; gap:12px; }
|
|
35
|
+
.cfm-rank-badge { background:var(--cfm-rank-badge); color:white; width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:12px; }
|
|
36
|
+
.cfm-handle { color:var(--cfm-rank-handle); cursor:grab; }
|
|
37
|
+
.cfm-pin { width:var(--cfm-heatmap-pin-size); height:var(--cfm-heatmap-pin-size); background:var(--cfm-heatmap-pin); border:2px solid var(--cfm-heatmap-pin-border, white); border-radius:50%; position:absolute; }
|
|
38
|
+
.cfm-media { max-width:var(--cfm-media-max-width); border-radius:var(--cfm-media-radius); background:#e5e7eb; height:200px; display:flex; align-items:center; justify-content:center; }
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
function wrapPage(title: string, body: string): string {
|
|
42
|
+
return `<!DOCTYPE html>
|
|
43
|
+
<html lang="en">
|
|
44
|
+
<head>
|
|
45
|
+
<meta charset="UTF-8" />
|
|
46
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
47
|
+
<title>${title} — CFM Preview</title>
|
|
48
|
+
<link rel="stylesheet" href="../shared/survey-theme.css" />
|
|
49
|
+
<style>${sharedStyles}</style>
|
|
50
|
+
</head>
|
|
51
|
+
<body>
|
|
52
|
+
<div class="cfm-preview-root">${body}</div>
|
|
53
|
+
<script src="../shared/preview-bridge.inline.js"></script>
|
|
54
|
+
</body>
|
|
55
|
+
</html>`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const PREVIEW_PAGE_DEFS: PreviewPageDef[] = [
|
|
59
|
+
{
|
|
60
|
+
key: 'header',
|
|
61
|
+
title: 'Header',
|
|
62
|
+
body: `<header class="cfm-header">
|
|
63
|
+
<img data-cfm-logo class="cfm-header-logo" src="" alt="Logo" style="display:none" />
|
|
64
|
+
<span data-cfm-company class="cfm-header-company">Company Name</span>
|
|
65
|
+
</header>`,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
key: 'footer',
|
|
69
|
+
title: 'Footer',
|
|
70
|
+
body: `<footer class="cfm-footer">
|
|
71
|
+
<div>
|
|
72
|
+
<img data-cfm-logo style="width:var(--cfm-footer-logo-width);height:var(--cfm-footer-logo-height);object-fit:contain;display:none" src="" alt="" />
|
|
73
|
+
<p data-cfm-copyright style="font-size:12px;margin-top:8px">© Company</p>
|
|
74
|
+
</div>
|
|
75
|
+
<nav data-cfm-footer-links class="cfm-footer-links"></nav>
|
|
76
|
+
</footer>`,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: 'chrome',
|
|
80
|
+
title: 'Chrome',
|
|
81
|
+
body: `<header class="cfm-header">
|
|
82
|
+
<img data-cfm-logo class="cfm-header-logo" src="" alt="" style="display:none" />
|
|
83
|
+
<span data-cfm-company class="cfm-header-company">Company</span>
|
|
84
|
+
</header>
|
|
85
|
+
<div class="cfm-progress"><div class="cfm-progress-fill"></div></div>
|
|
86
|
+
<p data-cfm-survey-title style="font-size:20px;font-weight:var(--cfm-heading-weight);margin:16px 0">Survey Title</p>
|
|
87
|
+
<div style="display:flex;gap:12px;margin-top:24px">
|
|
88
|
+
<button data-cfm-btn-back class="cfm-btn-back">Back</button>
|
|
89
|
+
<button data-cfm-btn-next class="cfm-btn-primary">Next</button>
|
|
90
|
+
</div>`,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
key: 'survey_page',
|
|
94
|
+
title: 'Survey Page',
|
|
95
|
+
body: `<header class="cfm-header">
|
|
96
|
+
<img data-cfm-logo class="cfm-header-logo" src="" alt="" style="display:none" />
|
|
97
|
+
<span data-cfm-company class="cfm-header-company">Company</span>
|
|
98
|
+
</header>
|
|
99
|
+
<div data-cfm-progress class="cfm-progress"><div class="cfm-progress-fill" style="width:35%"></div></div>
|
|
100
|
+
<div data-cfm-language style="text-align:right;margin:16px 0"><select class="cfm-input" style="width:auto"><option>English</option><option>Deutsch</option></select></div>
|
|
101
|
+
<p style="font-weight:600"><span data-cfm-question-number>1.</span> Sample question <span data-cfm-required style="color:red">*</span></p>
|
|
102
|
+
<div class="cfm-mcq-option selected"><span>◉</span> Option A</div>
|
|
103
|
+
<p style="font-weight:600;margin-top:24px"><span data-cfm-question-number>2.</span> NPS question</p>
|
|
104
|
+
<div class="cfm-nps-row">${Array.from({ length: 11 }, (_, i) => `<div class="cfm-nps-cell">${i}</div>`).join('')}</div>
|
|
105
|
+
<div style="display:flex;gap:12px;margin-top:24px">
|
|
106
|
+
<button data-cfm-btn-back class="cfm-btn-back">Back</button>
|
|
107
|
+
<button data-cfm-btn-next class="cfm-btn-primary">Next</button>
|
|
108
|
+
</div>
|
|
109
|
+
<footer class="cfm-footer" style="margin-top:32px">
|
|
110
|
+
<p data-cfm-copyright>© Company</p>
|
|
111
|
+
<nav data-cfm-footer-links class="cfm-footer-links"></nav>
|
|
112
|
+
</footer>`,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
key: 'intro',
|
|
116
|
+
title: 'Intro',
|
|
117
|
+
body: `<h1 data-cfm-survey-title style="font-size:28px;font-weight:var(--cfm-heading-weight)">Welcome</h1>
|
|
118
|
+
<p style="margin:16px 0;color:#6b7280">Please take a few minutes to share your feedback.</p>
|
|
119
|
+
<button class="cfm-btn-primary">Start Survey</button>`,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
key: 'end',
|
|
123
|
+
title: 'End',
|
|
124
|
+
body: `<h1 style="font-size:28px;font-weight:var(--cfm-heading-weight)">Thank you!</h1>
|
|
125
|
+
<p data-cfm-thank-you style="margin:16px 0">Thank you for your feedback.</p>`,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
key: 'MCQ_single',
|
|
129
|
+
title: 'MCQ Single',
|
|
130
|
+
body: `<p style="font-weight:600;margin-bottom:16px">1. Select one option</p>
|
|
131
|
+
<div data-cfm-mcq-style="outlined">
|
|
132
|
+
<div class="cfm-mcq-option selected"><span>◉</span> Option A</div>
|
|
133
|
+
<div class="cfm-mcq-option"><span>○</span> Option B</div>
|
|
134
|
+
<div class="cfm-mcq-option"><span>○</span> Option C</div>
|
|
135
|
+
</div>`,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
key: 'MCQ_multiple',
|
|
139
|
+
title: 'MCQ Multiple',
|
|
140
|
+
body: `<p style="font-weight:600;margin-bottom:16px">Select all that apply</p>
|
|
141
|
+
<div class="cfm-mcq-option selected"><span>☑</span> Option A</div>
|
|
142
|
+
<div class="cfm-mcq-option"><span>☐</span> Option B</div>`,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
key: 'TEXTFIELD_short',
|
|
146
|
+
title: 'Text Short',
|
|
147
|
+
body: `<p style="font-weight:600;margin-bottom:8px">Your name</p>
|
|
148
|
+
<input class="cfm-input" placeholder="Type here..." />`,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
key: 'TEXTFIELD_long',
|
|
152
|
+
title: 'Text Long',
|
|
153
|
+
body: `<p style="font-weight:600;margin-bottom:8px">Comments</p>
|
|
154
|
+
<textarea class="cfm-input" style="height:120px;padding:12px" placeholder="Type your response..."></textarea>`,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
key: 'NPS_SCALE',
|
|
158
|
+
title: 'NPS',
|
|
159
|
+
body: `<p style="font-weight:600;margin-bottom:8px">How likely are you to recommend us?</p>
|
|
160
|
+
<div style="display:flex;justify-content:space-between;font-size:12px;color:var(--cfm-nps-label-color);margin-bottom:8px"><span>Not likely</span><span>Very likely</span></div>
|
|
161
|
+
<div class="cfm-nps-row">${Array.from({ length: 11 }, (_, i) => `<div class="cfm-nps-cell${i === 8 ? ' selected' : ''}">${i}</div>`).join('')}</div>`,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
key: 'CFM_likert',
|
|
165
|
+
title: 'CFM Likert',
|
|
166
|
+
body: matrixPreview('Statement item 1', 'radio'),
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
key: 'CFM_bipolar',
|
|
170
|
+
title: 'CFM Bipolar',
|
|
171
|
+
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>`,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
key: 'CFM_transpose',
|
|
175
|
+
title: 'CFM Transpose',
|
|
176
|
+
body: matrixPreview('Scale item (transposed row)', 'checkbox'),
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
key: 'CFM_carousel',
|
|
180
|
+
title: 'CFM Carousel',
|
|
181
|
+
body: `${matrixPreview('Row 1 of 3', 'radio')}<div style="text-align:center;margin-top:12px"><span style="color:var(--cfm-matrix-carousel-dot)">●</span> ○ ○</div>`,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
key: 'CFM_dropdown',
|
|
185
|
+
title: 'CFM Dropdown',
|
|
186
|
+
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>`,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
key: 'CSAT_emoji',
|
|
190
|
+
title: 'CSAT Emoji',
|
|
191
|
+
body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">How satisfied?</div><div class="cfm-matrix-cells"><span class="cfm-emoji selected">😞</span><span class="cfm-emoji">😐</span><span class="cfm-emoji">🙂</span><span class="cfm-emoji">😊</span><span class="cfm-emoji">🤩</span></div></div>`,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
key: 'CSAT_star',
|
|
195
|
+
title: 'CSAT Star',
|
|
196
|
+
body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">Rate us</div><div class="cfm-matrix-cells"><span class="cfm-star selected">★</span><span class="cfm-star selected">★</span><span class="cfm-star">★</span><span class="cfm-star">★</span><span class="cfm-star">★</span></div></div>`,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
key: 'CSAT_numbered',
|
|
200
|
+
title: 'CSAT Numbered',
|
|
201
|
+
body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">Score</div><div class="cfm-matrix-cells"><div class="cfm-badge">1</div><div class="cfm-badge">2</div><div class="cfm-badge" style="opacity:1">3</div><div class="cfm-badge">4</div><div class="cfm-badge">5</div></div></div>`,
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
key: 'CSAT_graphics',
|
|
205
|
+
title: 'CSAT Graphics',
|
|
206
|
+
body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">Slider row</div><div style="flex:1;padding:0 16px"><div class="cfm-slider-track"><div class="cfm-slider-thumb"></div></div></div></div>`,
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
key: 'CSAT_carousel',
|
|
210
|
+
title: 'CSAT Carousel',
|
|
211
|
+
body: `${matrixPreview('Carousel row', 'emoji')}<div style="text-align:center"><span style="color:var(--cfm-csat-accent)">●</span> ○</div>`,
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
key: 'CSAT_dropdown',
|
|
215
|
+
title: 'CSAT Dropdown',
|
|
216
|
+
body: `<div class="cfm-matrix-label">Statement</div><select class="cfm-input" style="margin-top:8px"><option>Choose rating...</option></select>`,
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
key: 'RATING_star',
|
|
220
|
+
title: 'Rating Star',
|
|
221
|
+
body: `<div class="cfm-matrix-cells" style="justify-content:flex-start;padding:16px"><span class="cfm-star selected">★</span><span class="cfm-star selected">★</span><span class="cfm-star">★</span><span class="cfm-star">★</span><span class="cfm-star">★</span></div>`,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
key: 'RATING_numbered',
|
|
225
|
+
title: 'Rating Numbered',
|
|
226
|
+
body: `<div class="cfm-matrix-cells" style="padding:16px">${[1, 2, 3, 4, 5].map((n) => `<div class="cfm-badge">${n}</div>`).join('')}</div>`,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
key: 'RATING_radio',
|
|
230
|
+
title: 'Rating Radio',
|
|
231
|
+
body: matrixPreview('Recommend to a friend', 'radio'),
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
key: 'RATING_emoji',
|
|
235
|
+
title: 'Rating Emoji',
|
|
236
|
+
body: `<div class="cfm-matrix-cells" style="padding:16px"><span class="cfm-emoji">🙂</span><span class="cfm-emoji selected">😊</span><span class="cfm-emoji">🤩</span></div>`,
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
key: 'RATING_graphics',
|
|
240
|
+
title: 'Rating Graphics',
|
|
241
|
+
body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">Experience</div><div style="flex:1;padding:0 16px"><div class="cfm-slider-track"><div class="cfm-slider-thumb"></div></div></div></div>`,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
key: 'RATING_slider',
|
|
245
|
+
title: 'Rating Slider',
|
|
246
|
+
body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">Rate on slider</div><div style="flex:1;padding:0 16px"><div class="cfm-slider-track"><div class="cfm-slider-thumb"></div></div></div></div>`,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
key: 'SLIDER_matrix',
|
|
250
|
+
title: 'Slider Matrix',
|
|
251
|
+
body: `<div style="display:flex;gap:4px;margin-bottom:16px;padding-left:var(--cfm-matrix-row-width)">${[0, 1, 2, 3, 4, 5].map((n) => `<div class="cfm-tick">${n}</div>`).join('')}</div>
|
|
252
|
+
<div class="cfm-matrix-row"><div class="cfm-matrix-label">Statement 1</div><div style="flex:1;padding:0 16px"><div class="cfm-slider-track"><div class="cfm-slider-thumb"></div></div></div></div>`,
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
key: 'FILE_UPLOAD',
|
|
256
|
+
title: 'File Upload',
|
|
257
|
+
body: `<div data-cfm-dropzone-style="dashed" class="cfm-dropzone">Drop files here or click to upload</div>`,
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
key: 'TEXT_AND_MEDIA',
|
|
261
|
+
title: 'Text & Media',
|
|
262
|
+
body: `<div class="cfm-media">Media preview</div><p style="font-size:var(--cfm-media-caption-size);color:var(--cfm-media-caption-color);margin-top:8px">Caption text</p>`,
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
key: 'HEATMAP',
|
|
266
|
+
title: 'Heatmap',
|
|
267
|
+
body: `<div style="position:relative;background:#e5e7eb;height:240px;border-radius:var(--cfm-border-radius)"><div class="cfm-pin" style="top:40%;left:55%"></div><div class="cfm-pin" style="top:60%;left:30%"></div></div>`,
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
key: 'RANK_drag',
|
|
271
|
+
title: 'Rank Drag',
|
|
272
|
+
body: `<div class="cfm-rank-item"><span class="cfm-handle">⠿</span><span class="cfm-rank-badge">1</span> Item A</div><div class="cfm-rank-item"><span class="cfm-handle">⠿</span><span class="cfm-rank-badge">2</span> Item B</div>`,
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
key: 'RANK_dropdown',
|
|
276
|
+
title: 'Rank Dropdown',
|
|
277
|
+
body: `<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px"><span>Item A</span><select class="cfm-input" style="width:80px"><option>1</option><option>2</option></select></div>`,
|
|
278
|
+
},
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
function cells(n: number): string {
|
|
282
|
+
return Array.from({ length: n }, (_, i) =>
|
|
283
|
+
`<div class="cfm-matrix-cell${i === 2 ? ' selected' : ''}"></div>`,
|
|
284
|
+
).join('');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function matrixPreview(label: string, mode: string): string {
|
|
288
|
+
if (mode === 'emoji') {
|
|
289
|
+
return `<div class="cfm-matrix-row"><div class="cfm-matrix-label">${label}</div><div class="cfm-matrix-cells"><span class="cfm-emoji">😞</span><span class="cfm-emoji selected">🙂</span><span class="cfm-emoji">😊</span></div></div>`;
|
|
290
|
+
}
|
|
291
|
+
return `<div class="cfm-matrix-row"><div class="cfm-matrix-label">${label}</div><div class="cfm-matrix-cells">${cells(5)}</div></div>`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function buildPreviewHtml(def: PreviewPageDef): string {
|
|
295
|
+
return wrapPage(def.title, def.body);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export const PREVIEW_KEYS = PREVIEW_PAGE_DEFS.map((d) => d.key);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type PreviewStatePatch = {
|
|
4
|
+
multiStatement?: boolean;
|
|
5
|
+
textfieldPlaceholder?: string;
|
|
6
|
+
npsButtonStyle?: string;
|
|
7
|
+
mcqOptionStyle?: string;
|
|
8
|
+
dropzoneStyle?: string;
|
|
9
|
+
heatmapPreviewPin?: { x: number; y: number };
|
|
10
|
+
rankVariant?: 'drag' | 'dropdown';
|
|
11
|
+
hintMinText?: string;
|
|
12
|
+
hintMaxText?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const PREVIEW_BRIDGE_SNAPSHOT = 'CFM_UI_CONFIG_SNAPSHOT';
|
|
16
|
+
export const PREVIEW_BRIDGE_DELTA = 'CFM_UI_CONFIG_DELTA';
|
|
17
|
+
|
|
18
|
+
type PreviewConfigContextValue = {
|
|
19
|
+
previewState: PreviewStatePatch;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const PreviewConfigContext = createContext<PreviewConfigContextValue>({
|
|
23
|
+
previewState: {},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export function PreviewConfigProvider({ children }: { children: ReactNode }) {
|
|
27
|
+
const [previewState, setPreviewState] = useState<PreviewStatePatch>({});
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const handler = (event: MessageEvent) => {
|
|
31
|
+
const data = event.data;
|
|
32
|
+
if (!data?.type) return;
|
|
33
|
+
if (data.type !== PREVIEW_BRIDGE_SNAPSHOT && data.type !== PREVIEW_BRIDGE_DELTA) return;
|
|
34
|
+
if (data.contentPatch?.previewState) {
|
|
35
|
+
setPreviewState((prev) => ({ ...prev, ...data.contentPatch.previewState }));
|
|
36
|
+
}
|
|
37
|
+
if (data.contentPatch?.domAttributes) {
|
|
38
|
+
const root = document.documentElement;
|
|
39
|
+
for (const [key, value] of Object.entries(data.contentPatch.domAttributes)) {
|
|
40
|
+
root.setAttribute(key, value as string);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
window.addEventListener('message', handler);
|
|
46
|
+
try {
|
|
47
|
+
const channel = new BroadcastChannel('cfm-wizard-preview');
|
|
48
|
+
channel.addEventListener('message', handler);
|
|
49
|
+
return () => {
|
|
50
|
+
window.removeEventListener('message', handler);
|
|
51
|
+
channel.close();
|
|
52
|
+
};
|
|
53
|
+
} catch {
|
|
54
|
+
return () => window.removeEventListener('message', handler);
|
|
55
|
+
}
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<PreviewConfigContext.Provider value={{ previewState }}>
|
|
60
|
+
{children}
|
|
61
|
+
</PreviewConfigContext.Provider>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function usePreviewConfig() {
|
|
66
|
+
return useContext(PreviewConfigContext);
|
|
67
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import type { AnswerValue, SurveyQuestion } from '@explorer02/cfm-survey-sdk';
|
|
3
|
+
import Question from '@/components/Question';
|
|
4
|
+
import { PreviewConfigProvider, usePreviewConfig } from './PreviewConfigContext';
|
|
5
|
+
|
|
6
|
+
type QuestionPreviewProps = {
|
|
7
|
+
question: SurveyQuestion;
|
|
8
|
+
initialValue?: AnswerValue;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function resolveNumberLabelColor(index: number): string {
|
|
12
|
+
const mode = getComputedStyle(document.documentElement)
|
|
13
|
+
.getPropertyValue('--cfm-number-label-mode')
|
|
14
|
+
.trim();
|
|
15
|
+
if (mode === 'monochrome') {
|
|
16
|
+
return getComputedStyle(document.documentElement)
|
|
17
|
+
.getPropertyValue('--cfm-number-mono-color')
|
|
18
|
+
.trim() || 'var(--cfm-primary)';
|
|
19
|
+
}
|
|
20
|
+
if (mode === 'individual') {
|
|
21
|
+
return (
|
|
22
|
+
getComputedStyle(document.documentElement)
|
|
23
|
+
.getPropertyValue(`--cfm-number-color-${index}`)
|
|
24
|
+
.trim() || '#9CA3AF'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function QuestionPreviewInner({ question, initialValue }: QuestionPreviewProps) {
|
|
31
|
+
const { previewState } = usePreviewConfig();
|
|
32
|
+
const [value, setValue] = useState<AnswerValue | undefined>(initialValue);
|
|
33
|
+
|
|
34
|
+
const mergedQuestion = useMemo(() => {
|
|
35
|
+
let q = { ...question } as SurveyQuestion;
|
|
36
|
+
|
|
37
|
+
if (previewState.textfieldPlaceholder && q.type === 'TEXTFIELD') {
|
|
38
|
+
q = { ...q, placeholder: previewState.textfieldPlaceholder };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (previewState.npsButtonStyle && q.type === 'NPS_SCALE') {
|
|
42
|
+
const style = previewState.npsButtonStyle;
|
|
43
|
+
q = {
|
|
44
|
+
...q,
|
|
45
|
+
buttonStyle: style === 'pill' ? 'numbered' : (style as 'standard' | 'numbered'),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (previewState.hintMinText || previewState.hintMaxText) {
|
|
50
|
+
if (q.type === 'NPS_SCALE') {
|
|
51
|
+
q = {
|
|
52
|
+
...q,
|
|
53
|
+
minLabel: previewState.hintMinText ?? q.minLabel,
|
|
54
|
+
maxLabel: previewState.hintMaxText ?? q.maxLabel,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (q.type === 'SLIDER_MATRIX') {
|
|
58
|
+
q = {
|
|
59
|
+
...q,
|
|
60
|
+
scaleAnchorLabels: [
|
|
61
|
+
previewState.hintMinText ?? q.scaleAnchorLabels?.[0] ?? '',
|
|
62
|
+
previewState.hintMaxText ?? q.scaleAnchorLabels?.[1] ?? '',
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (previewState.multiStatement && 'statementRows' in q && q.statementRows.length === 1) {
|
|
69
|
+
const extra = {
|
|
70
|
+
id: 'r2',
|
|
71
|
+
statementText: 'Second statement row',
|
|
72
|
+
};
|
|
73
|
+
q = { ...q, statementRows: [...q.statementRows, extra] } as typeof q;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (previewState.rankVariant === 'drag' && q.type === 'RANK_ORDER') {
|
|
77
|
+
q = { ...q, interactionMode: 'dragAndDrop', optionDisplay: 'textOnly' };
|
|
78
|
+
}
|
|
79
|
+
if (previewState.rankVariant === 'dropdown' && q.type === 'RANK_ORDER') {
|
|
80
|
+
q = { ...q, interactionMode: 'dropdown', optionDisplay: 'textOnly' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return q;
|
|
84
|
+
}, [question, previewState]);
|
|
85
|
+
|
|
86
|
+
const heatmapInitial =
|
|
87
|
+
previewState.heatmapPreviewPin && mergedQuestion.type === 'HEATMAP'
|
|
88
|
+
? [
|
|
89
|
+
{
|
|
90
|
+
id: 'preview-pin',
|
|
91
|
+
x: previewState.heatmapPreviewPin.x,
|
|
92
|
+
y: previewState.heatmapPreviewPin.y,
|
|
93
|
+
},
|
|
94
|
+
]
|
|
95
|
+
: initialValue;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div
|
|
99
|
+
className="cfm-question-card mx-auto max-w-3xl p-6"
|
|
100
|
+
style={{ fontFamily: 'var(--cfm-font-family)' }}
|
|
101
|
+
data-cfm-question-area
|
|
102
|
+
>
|
|
103
|
+
<Question
|
|
104
|
+
question={mergedQuestion}
|
|
105
|
+
selectedValue={heatmapInitial ?? value}
|
|
106
|
+
allAnswers={{}}
|
|
107
|
+
allQuestions={[mergedQuestion]}
|
|
108
|
+
onSelect={setValue}
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function QuestionPreview(props: QuestionPreviewProps) {
|
|
115
|
+
return (
|
|
116
|
+
<PreviewConfigProvider>
|
|
117
|
+
<QuestionPreviewInner {...props} />
|
|
118
|
+
</PreviewConfigProvider>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export { resolveNumberLabelColor };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { AnswerValue } from '@explorer02/cfm-survey-sdk';
|
|
3
|
+
import SurveyStickyChrome from '@/components/SurveyStickyChrome';
|
|
4
|
+
import LanguageSelector from '@/components/LanguageSelector';
|
|
5
|
+
import Question from '@/components/Question';
|
|
6
|
+
import Footer from '@/components/Footer';
|
|
7
|
+
import { FIXTURE_SURVEY_PAGE_NPS, PREVIEW_LANGUAGES } from './fixtures/questions';
|
|
8
|
+
|
|
9
|
+
export function SurveyPagePreview() {
|
|
10
|
+
const [lang, setLang] = useState('en');
|
|
11
|
+
const [answer, setAnswer] = useState<AnswerValue | undefined>();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
className="flex min-h-screen flex-col"
|
|
16
|
+
style={{
|
|
17
|
+
background: 'var(--cfm-background)',
|
|
18
|
+
fontFamily: 'var(--cfm-font-family)',
|
|
19
|
+
}}
|
|
20
|
+
>
|
|
21
|
+
<SurveyStickyChrome progressPercentage={75} />
|
|
22
|
+
|
|
23
|
+
<main
|
|
24
|
+
className="mx-auto w-full flex-1 px-4 py-10 sm:px-6 lg:px-8"
|
|
25
|
+
style={{ maxWidth: 'var(--cfm-max-width)' }}
|
|
26
|
+
>
|
|
27
|
+
<LanguageSelector
|
|
28
|
+
languages={PREVIEW_LANGUAGES}
|
|
29
|
+
selectedLanguage={lang}
|
|
30
|
+
onChange={setLang}
|
|
31
|
+
/>
|
|
32
|
+
|
|
33
|
+
<div className="space-y-12">
|
|
34
|
+
<Question
|
|
35
|
+
question={FIXTURE_SURVEY_PAGE_NPS}
|
|
36
|
+
selectedValue={answer}
|
|
37
|
+
allAnswers={answer ? { [FIXTURE_SURVEY_PAGE_NPS.id]: answer } : {}}
|
|
38
|
+
allQuestions={[FIXTURE_SURVEY_PAGE_NPS]}
|
|
39
|
+
onSelect={setAnswer}
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div className="mt-8 flex items-center justify-start gap-4">
|
|
44
|
+
<button
|
|
45
|
+
data-cfm-btn-back
|
|
46
|
+
type="button"
|
|
47
|
+
className="border px-6 py-2 text-sm font-semibold transition-all hover:opacity-90"
|
|
48
|
+
style={{
|
|
49
|
+
borderColor: 'var(--cfm-primary)',
|
|
50
|
+
color: 'var(--cfm-primary)',
|
|
51
|
+
backgroundColor: 'transparent',
|
|
52
|
+
borderRadius: 'var(--cfm-border-radius)',
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
Back
|
|
56
|
+
</button>
|
|
57
|
+
<button
|
|
58
|
+
data-cfm-btn-next
|
|
59
|
+
type="button"
|
|
60
|
+
className="px-6 py-2 text-sm font-semibold transition-opacity hover:opacity-90"
|
|
61
|
+
style={{
|
|
62
|
+
backgroundColor: 'var(--cfm-primary)',
|
|
63
|
+
color: 'var(--cfm-button-text)',
|
|
64
|
+
borderRadius: 'var(--cfm-border-radius)',
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
Submit
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
</main>
|
|
71
|
+
|
|
72
|
+
<Footer />
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|