@explorer02/cfm-survey-sdk 0.2.5 → 0.2.7

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 (39) hide show
  1. package/dist/cli/index.js +45 -45
  2. package/dist/cli/index.mjs +45 -45
  3. package/package.json +1 -1
  4. package/templates/docs/00-integration/agent-execution-flow.md +18 -6
  5. package/templates/docs/00-integration/agent-operating-contract.md +2 -0
  6. package/templates/docs/00-integration/apply-ui-config.md +15 -2
  7. package/templates/docs/00-integration/component-checklist.md +5 -3
  8. package/templates/docs/00-integration/ui-customization-wizard.md +1 -1
  9. package/templates/docs/00-integration/wizard-chrome-contract.md +284 -0
  10. package/templates/docs/00-integration/wizard-config-handoff.md +7 -1
  11. package/templates/docs/00-integration/wizard-customization-catalog.md +391 -0
  12. package/templates/docs/00-integration/wizard-post-customize-build.md +405 -0
  13. package/templates/docs/00-integration/wizard-preview-build-guide.md +20 -4
  14. package/templates/docs/00-integration/wizard-question-type-styling.md +27 -6
  15. package/templates/docs/01-components/19-survey-sticky-chrome.md +16 -55
  16. package/templates/docs/03-ui-specs/README.md +1 -1
  17. package/templates/docs/MANIFEST.json +10 -1
  18. package/templates/docs/index.md +10 -7
  19. package/templates/docs/templates/Footer.tsx +72 -0
  20. package/templates/docs/templates/Header.tsx +110 -0
  21. package/templates/docs/templates/LanguageSelector.tsx +40 -0
  22. package/templates/docs/templates/PreviewBridgeInit.tsx +13 -0
  23. package/templates/docs/templates/ProgressBar.tsx +33 -0
  24. package/templates/docs/templates/Question.tsx +51 -7
  25. package/templates/docs/templates/RatingScale.tsx +66 -59
  26. package/templates/docs/templates/SurveyStickyChrome.tsx +19 -5
  27. package/templates/docs/templates/uiConfig.ts +33 -0
  28. package/templates/docs/templates/verify-agent-build.sh +31 -1
  29. package/templates/preview-harness/preview-bridge.inline.js +18 -4
  30. package/templates/preview-harness/vite-app/src/preview-live-overrides.css +7 -0
  31. package/templates/previewBridge.ts +144 -39
  32. package/templates/survey-theme.css +7 -0
  33. package/templates/wizard-dist/assets/{PreviewMock-AoZdAEPZ.js → PreviewMock-tTMvwuEy.js} +1 -1
  34. package/templates/wizard-dist/assets/{TypePanel-39FnbtvG.js → TypePanel-B9Ls7BSq.js} +1 -1
  35. package/templates/wizard-dist/assets/index-CjkIlOUI.css +1 -0
  36. package/templates/wizard-dist/assets/index-CoCrnnxt.js +34 -0
  37. package/templates/wizard-dist/index.html +2 -2
  38. package/templates/wizard-dist/assets/index-BhWM50Yu.css +0 -1
  39. package/templates/wizard-dist/assets/index-CWKdsJOp.js +0 -34
@@ -29,7 +29,9 @@ templates/docs/
29
29
  ├── 00-integration/
30
30
  │ ├── agent-operating-contract.md ← Anti-hallucination — read FIRST
31
31
  │ ├── agent-execution-flow.md ← Pipeline, client questions, wizard-ready build
32
+ │ ├── wizard-customization-catalog.md ← All wizard controls × types × formats
32
33
  │ ├── wizard-preview-build-guide.md
34
+ │ ├── wizard-post-customize-build.md ← Phase 6c — all formats + build gate
33
35
  │ ├── wizard-question-type-styling.md
34
36
  │ └── …
35
37
  ├── 01-components/ ← Wiring blueprints (per type from MANIFEST)
@@ -77,10 +79,10 @@ Full step-by-step: [`agent-execution-flow.md`](00-integration/agent-execution-fl
77
79
  | **2** Instance ID | Get JWT | `useSurveySDK.md` — **ask client** |
78
80
  | **3** SDK contract | Hook + logic | `client-integration-guide.md`, `question-type-sdk-matrix.md`, logic docs |
79
81
  | **4** Plan | Inventory + copy dispatcher | `component-checklist.md`, per-type MANIFEST docs, `implementation_plan.md` |
80
- | **5** Build | Wizard-ready UI | `constraints.md`, `wizard-preview-build-guide.md`, `wizard-question-type-styling.md`, `03-ui-specs/*` |
82
+ | **5** Build | Wizard-ready UI | `wizard-customization-catalog.md`, `wizard-chrome-contract.md`, `wizard-preview-build-guide.md`, `wizard-question-type-styling.md`, `03-ui-specs/*` |
81
83
  | **6** Verify | Build + dev server | `verify-agent-build.sh` — **ask wizard** |
82
84
  | **6b** Wizard | Optional customize | **`npx cfm-sdk customize`** (default) — EC2 `customize-ui` only if client on another device |
83
- | **6c** Apply | Sync final config | `apply-ui-config.md` |
85
+ | **6c** Apply | Sync final config | `wizard-post-customize-build.md`, `apply-ui-config.md` |
84
86
  | **7** Deploy | Production | **ask deploy** — `aws-deploy.md` or Vercel |
85
87
 
86
88
  **Auto-proceed:** Phase 4 → 5 and Phase 5 → 6 without waiting for approval.
@@ -101,7 +103,7 @@ Every agent build includes these even if the client later skips the wizard:
101
103
  | Scale components | `templates/*Scale*.tsx` etc. | `src/components/` per inventory |
102
104
  | Draft config | agent-written | `./survey-ui-config.json` |
103
105
 
104
- **Chrome:** `data-cfm-logo` / `data-cfm-logo-text`, `data-cfm-company`, `data-cfm-btn-next`, `data-cfm-btn-back`, `data-cfm-survey-title`, `data-cfm-thank-you` see `wizard-preview-build-guide.md`.
106
+ **Chrome:** copy `templates/docs/templates/Header.tsx` etc. see `wizard-chrome-contract.md` for full hook list.
105
107
 
106
108
  **Selection pattern:** focus ring = outer border + inner dot; cell selected = annulus fill — see `wizard-question-type-styling.md`.
107
109
 
@@ -135,7 +137,7 @@ npx cfm-sdk customize # DEFAULT — opens localhost:4200, WAITS until client s
135
137
 
136
138
  **Phase 6b:** Client on same machine → `customize` writes `survey-ui-config.json` directly. Remote client → `customize-ui` + EC2 link (see `wizard-troubleshooting.md`). Apply per `apply-ui-config.md`.
137
139
 
138
- **Phase 6c:** Apply from `survey-ui-config.final.json` per [`wizard-config-handoff.md`](00-integration/wizard-config-handoff.md) + [`apply-ui-config.md`](00-integration/apply-ui-config.md) **every** diff path. Then:
140
+ **Phase 6c:** Apply from `survey-ui-config.json` (local) or `.final.json` (EC2) per [`wizard-post-customize-build.md`](00-integration/wizard-post-customize-build.md) — all question types/formats + chrome — then [`apply-ui-config.md`](00-integration/apply-ui-config.md) for path tables. Then:
139
141
 
140
142
  ```bash
141
143
  npx cfm-sdk confirm-ui-config
@@ -152,9 +154,9 @@ Re-run verify script + build.
152
154
  | **Always first** | `agent-operating-contract.md`, `agent-execution-flow.md`, `mockup-ui-extraction.md`, `index.md`, `client-integration-guide.md`, `constraints.md` |
153
155
  | Phase 3 contract | `question-type-sdk-matrix.md`, `useSurveySDK.md`, logic/navigation docs |
154
156
  | Phase 4 plan | `mockup-ui-extraction.md`, `placeholders-and-custom-fields-wiring.md`, `component-checklist.md`, per-type `03-ui-specs/*` |
155
- | Wizard build | `wizard-preview-build-guide.md`, `wizard-question-type-styling.md` |
157
+ | Wizard build | `wizard-customization-catalog.md`, `wizard-preview-build-guide.md`, `wizard-question-type-styling.md` |
156
158
  | Wizard run | `wizard-config-handoff.md`, `ui-customization-wizard.md`, `wizard-api.md`, `wizard-troubleshooting.md` |
157
- | After wizard (apply all paths) | `apply-ui-config.md` |
159
+ | After wizard (apply all paths) | `wizard-post-customize-build.md`, `apply-ui-config.md` |
158
160
  | Deploy | `aws-deploy.md` (§ Logos & local assets), `setup.md#nextjs-aws-static-export` |
159
161
  | Config audit | `02-reference/config-field-index.md` |
160
162
 
@@ -170,4 +172,5 @@ Re-run verify script + build.
170
172
  | Hint labels | `--cfm-hint-label-*` | NPS min/max anchors |
171
173
  | Number labels | `--cfm-number-*` | Traffic-light badges (unselected only) |
172
174
 
173
- Full per-type table: [`wizard-question-type-styling.md`](00-integration/wizard-question-type-styling.md).
175
+ Full per-type table: [`wizard-question-type-styling.md`](00-integration/wizard-question-type-styling.md).
176
+ Complete wizard control index (all formats): [`wizard-customization-catalog.md`](00-integration/wizard-customization-catalog.md).
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Wizard-ready Footer — copy to src/components/Footer.tsx
3
+ * Required: footer.cfm-footer, data-cfm-copyright, data-cfm-footer-links, data-cfm-footer-logo
4
+ */
5
+ 'use client';
6
+
7
+ export default function Footer() {
8
+ return (
9
+ <footer
10
+ className="cfm-footer w-full"
11
+ style={{
12
+ background: 'var(--cfm-footer-bg, #262626)',
13
+ color: 'var(--cfm-footer-text, #9ca3af)',
14
+ padding: 'var(--cfm-footer-padding-y, 24px) var(--cfm-footer-padding-x, 40px)',
15
+ }}
16
+ >
17
+ <div
18
+ className="cfm-footer-inner mx-auto grid w-full max-w-4xl items-end gap-6"
19
+ style={{
20
+ display: 'var(--cfm-footer-inner-display, grid)',
21
+ flexDirection: 'var(--cfm-footer-inner-direction, row)' as React.CSSProperties['flexDirection'],
22
+ gridTemplateColumns: '1fr 1fr 1fr',
23
+ }}
24
+ >
25
+ <div
26
+ className="space-y-3"
27
+ style={{
28
+ gridColumn: 'var(--cfm-footer-logo-col, 1)',
29
+ justifySelf: 'var(--cfm-footer-logo-justify, start)',
30
+ }}
31
+ >
32
+ <img
33
+ data-cfm-footer-logo
34
+ data-cfm-logo
35
+ src="./logo.svg"
36
+ alt=""
37
+ style={{
38
+ display: 'none',
39
+ width: 'var(--cfm-footer-logo-width, 120px)',
40
+ height: 'var(--cfm-footer-logo-height, 32px)',
41
+ objectFit: 'contain',
42
+ }}
43
+ />
44
+ <p
45
+ data-cfm-copyright
46
+ className="text-xs"
47
+ style={{ color: 'var(--cfm-footer-text, #9ca3af)' }}
48
+ >
49
+ © Company
50
+ </p>
51
+ </div>
52
+
53
+ <nav
54
+ data-cfm-footer-links
55
+ className="flex flex-wrap gap-x-6 gap-y-2 text-xs"
56
+ style={{
57
+ gridColumn: 'var(--cfm-footer-links-col, 3)',
58
+ justifySelf: 'var(--cfm-footer-links-justify, end)',
59
+ }}
60
+ >
61
+ <a
62
+ href="#"
63
+ className="transition-colors hover:opacity-80"
64
+ style={{ color: 'var(--cfm-footer-link, #9ca3af)' }}
65
+ >
66
+ Privacy
67
+ </a>
68
+ </nav>
69
+ </div>
70
+ </footer>
71
+ );
72
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Wizard-ready Header — copy to src/components/Header.tsx
3
+ *
4
+ * Brand row: [logo image] [company name] — company sits after logo when both exist.
5
+ * When no logo file: text fallback via data-cfm-logo-text (company name or empty).
6
+ *
7
+ * After wizard save, logo src MUST come from survey-ui-config.json global.logo.fileName
8
+ * via uiConfig.ts — never hardcode telekom-logo.svg or change fileName manually.
9
+ */
10
+ 'use client';
11
+
12
+ import { getCompanyName, getLogoSrc, shouldShowCompanyName } from '@/lib/uiConfig';
13
+
14
+ type HeaderProps = {
15
+ embedded?: boolean;
16
+ };
17
+
18
+ const seedCompany = getCompanyName();
19
+ const seedLogoSrc = getLogoSrc({ staticExport: true });
20
+ const seedShowCompany = shouldShowCompanyName();
21
+
22
+ export default function Header({ embedded = false }: HeaderProps) {
23
+ const hasLogoFile = Boolean(seedLogoSrc);
24
+
25
+ return (
26
+ <header
27
+ className={`cfm-header relative w-full ${embedded ? '' : 'z-10 shadow-[0_2px_8px_rgba(0,0,0,0.06)]'}`}
28
+ style={{
29
+ height: 'var(--cfm-header-height, 120px)',
30
+ background: 'var(--cfm-header-bg, #fff)',
31
+ borderBottom: '1px solid var(--cfm-header-border, #e5e7eb)',
32
+ boxShadow: embedded ? undefined : 'var(--cfm-header-shadow, 0 2px 8px rgba(0,0,0,0.06))',
33
+ }}
34
+ >
35
+ <div
36
+ className="cfm-header-inner grid h-full w-full items-center"
37
+ style={{
38
+ gridTemplateColumns: '1fr 1fr 1fr',
39
+ padding: `0 var(--cfm-header-padding-x, 40px)`,
40
+ }}
41
+ >
42
+ <div
43
+ className="cfm-header-brand flex items-center"
44
+ style={{
45
+ gridColumn: 'var(--cfm-header-logo-col, 1)',
46
+ justifySelf: 'var(--cfm-header-logo-justify, start)',
47
+ gap: 'var(--cfm-header-brand-gap, 16px)',
48
+ }}
49
+ >
50
+ {hasLogoFile ? (
51
+ <img
52
+ data-cfm-logo
53
+ src={seedLogoSrc!}
54
+ alt={seedCompany || 'Logo'}
55
+ className="shrink-0 object-contain"
56
+ style={{
57
+ width: 'var(--cfm-header-logo-width, 120px)',
58
+ height: 'var(--cfm-header-logo-height, 120px)',
59
+ padding: 'var(--cfm-header-logo-padding, 10px)',
60
+ background: 'var(--cfm-primary)',
61
+ }}
62
+ />
63
+ ) : (
64
+ <img
65
+ data-cfm-logo
66
+ src=""
67
+ alt="Logo"
68
+ className="shrink-0 object-contain"
69
+ style={{ display: 'none' }}
70
+ />
71
+ )}
72
+
73
+ <span
74
+ data-cfm-logo-text
75
+ className="font-semibold"
76
+ style={{
77
+ display: hasLogoFile ? 'none' : seedCompany ? '' : 'none',
78
+ color: 'var(--cfm-header-company-color, var(--cfm-text))',
79
+ fontSize: 'var(--cfm-header-company-size, 18px)',
80
+ fontWeight: 'var(--cfm-header-company-weight, 600)',
81
+ }}
82
+ >
83
+ {seedCompany}
84
+ </span>
85
+
86
+ <span
87
+ data-cfm-company
88
+ className="font-semibold"
89
+ style={{
90
+ display:
91
+ seedShowCompany && (hasLogoFile || seedCompany)
92
+ ? hasLogoFile
93
+ ? ''
94
+ : 'none'
95
+ : 'none',
96
+ marginTop: 'var(--cfm-header-company-margin-top, 0)',
97
+ marginLeft: 'var(--cfm-header-company-margin-left, 0)',
98
+ marginRight: 'var(--cfm-header-company-margin-right, 0)',
99
+ color: 'var(--cfm-header-company-color, var(--cfm-text))',
100
+ fontSize: 'var(--cfm-header-company-size, 18px)',
101
+ fontWeight: 'var(--cfm-header-company-weight, 600)',
102
+ }}
103
+ >
104
+ {seedCompany}
105
+ </span>
106
+ </div>
107
+ </div>
108
+ </header>
109
+ );
110
+ }
@@ -0,0 +1,40 @@
1
+ 'use client';
2
+
3
+ import type { SurveyLanguage } from '@explorer02/cfm-survey-sdk';
4
+
5
+ type LanguageSelectorProps = {
6
+ languages: SurveyLanguage[];
7
+ selectedLanguage: string;
8
+ onChange: (language: string) => void;
9
+ };
10
+
11
+ /** Wizard toggle showLanguageSwitcher targets [data-cfm-language] */
12
+ export default function LanguageSelector({
13
+ languages,
14
+ selectedLanguage,
15
+ onChange,
16
+ }: LanguageSelectorProps) {
17
+ if (languages.length <= 1) return null;
18
+
19
+ return (
20
+ <div data-cfm-language className="mb-6 flex justify-end">
21
+ <select
22
+ value={selectedLanguage}
23
+ onChange={(e) => onChange(e.target.value)}
24
+ className="rounded border px-3 py-1.5 text-sm outline-none"
25
+ style={{
26
+ background: 'var(--cfm-language-bg, #fff)',
27
+ color: 'var(--cfm-language-text, #2563eb)',
28
+ borderColor: 'var(--cfm-language-border, #2563eb)',
29
+ borderRadius: 'var(--cfm-border-radius, 8px)',
30
+ }}
31
+ >
32
+ {languages.map((lang) => (
33
+ <option key={lang.code} value={lang.code}>
34
+ {lang.label}
35
+ </option>
36
+ ))}
37
+ </select>
38
+ </div>
39
+ );
40
+ }
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import { initPreviewBridge } from '@/lib/previewBridge';
5
+
6
+ /** Mount once in root layout — enables localhost dev preview with npx cfm-sdk customize */
7
+ export function PreviewBridgeInit() {
8
+ useEffect(() => {
9
+ const cleanup = initPreviewBridge();
10
+ return cleanup;
11
+ }, []);
12
+ return null;
13
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Wizard-ready ProgressBar — copy to src/components/ProgressBar.tsx
3
+ * Required: data-cfm-progress + class cfm-progress (separate from header toggle)
4
+ */
5
+ 'use client';
6
+
7
+ type ProgressBarProps = {
8
+ progressPercentage: number;
9
+ };
10
+
11
+ export default function ProgressBar({ progressPercentage }: ProgressBarProps) {
12
+ const width = Math.min(100, Math.max(0, progressPercentage));
13
+
14
+ return (
15
+ <div data-cfm-progress className="cfm-progress w-full">
16
+ <div
17
+ className="w-full overflow-hidden rounded-full"
18
+ style={{
19
+ height: 'var(--cfm-progress-height, 12px)',
20
+ background: 'var(--cfm-progress-track, #e5e7eb)',
21
+ }}
22
+ >
23
+ <div
24
+ className="h-full rounded-full transition-all duration-300 ease-out"
25
+ style={{
26
+ width: `${width}%`,
27
+ background: 'var(--cfm-progress-fill, var(--cfm-primary))',
28
+ }}
29
+ />
30
+ </div>
31
+ </div>
32
+ );
33
+ }
@@ -101,13 +101,35 @@ export default function Question({
101
101
  }, [question, allAnswers, allQuestions, customFieldValues]);
102
102
 
103
103
  return (
104
- <section id={question.id} className="space-y-4 rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
105
- <h2 className="text-lg font-medium leading-relaxed text-gray-900">
104
+ <section
105
+ id={question.id}
106
+ className="cfm-question-card space-y-4"
107
+ style={{
108
+ background: 'var(--cfm-card-bg, #fff)',
109
+ borderColor: 'var(--cfm-card-border-color, #e5e7eb)',
110
+ borderWidth: 1,
111
+ borderStyle: 'solid',
112
+ padding: 'var(--cfm-card-padding, 24px)',
113
+ boxShadow: 'var(--cfm-card-shadow, 0 1px 3px rgba(0,0,0,0.1))',
114
+ borderRadius: 'var(--cfm-border-radius, 12px)',
115
+ fontFamily: 'var(--cfm-font-family)',
116
+ }}
117
+ >
118
+ <h2
119
+ className="text-lg font-medium leading-relaxed"
120
+ style={{ color: 'var(--cfm-text, #111827)' }}
121
+ >
106
122
  {question.questionNumber != null && (
107
- <span className="mr-1 font-semibold">{question.questionNumber}.</span>
123
+ <span data-cfm-question-number className="mr-1 font-semibold">
124
+ {question.questionNumber}.
125
+ </span>
108
126
  )}
109
127
  <span dangerouslySetInnerHTML={{ __html: displayQuestionText }} />
110
- {question.required && <span className="text-red-500 ml-1">*</span>}
128
+ {question.required && (
129
+ <span data-cfm-required className="ml-1 text-red-500">
130
+ *
131
+ </span>
132
+ )}
111
133
  </h2>
112
134
  {displayQuestionDescription && (
113
135
  <div
@@ -182,7 +204,14 @@ export default function Question({
182
204
  return (
183
205
  <label
184
206
  key={option.id}
185
- className={`flex items-center gap-4 rounded-lg border py-4 px-5 cursor-pointer transition-all ${isSelected ? 'border-[#e20074] bg-[#fdf2f8]' : 'border-[#e5e5e5] bg-white hover:bg-gray-50/50'}`}
207
+ className="flex cursor-pointer items-center gap-4 transition-all"
208
+ style={{
209
+ borderRadius: 'var(--cfm-mcq-border-radius, 8px)',
210
+ border: `2px solid ${isSelected ? 'var(--cfm-mcq-selected-accent, var(--cfm-primary))' : 'var(--cfm-card-border-color, #e5e7eb)'}`,
211
+ background: isSelected ? 'var(--cfm-mcq-selected-bg, var(--cfm-secondary))' : 'var(--cfm-card-bg, #fff)',
212
+ padding: 'var(--cfm-mcq-card-padding, 12px)',
213
+ marginBottom: 'var(--cfm-mcq-option-gap, 8px)',
214
+ }}
186
215
  >
187
216
  <input
188
217
  type="checkbox"
@@ -211,7 +240,14 @@ export default function Question({
211
240
  return (
212
241
  <label
213
242
  key={option.id}
214
- className={`flex items-center gap-4 rounded-lg border py-4 px-5 cursor-pointer transition-all ${isSelected ? 'border-[#e20074] bg-[#fdf2f8]' : 'border-[#e5e5e5] bg-white hover:bg-gray-50/50'}`}
243
+ className="flex cursor-pointer items-center gap-4 transition-all"
244
+ style={{
245
+ borderRadius: 'var(--cfm-mcq-border-radius, 8px)',
246
+ border: `2px solid ${isSelected ? 'var(--cfm-mcq-selected-accent, var(--cfm-primary))' : 'var(--cfm-card-border-color, #e5e7eb)'}`,
247
+ background: isSelected ? 'var(--cfm-mcq-selected-bg, var(--cfm-secondary))' : 'var(--cfm-card-bg, #fff)',
248
+ padding: 'var(--cfm-mcq-card-padding, 12px)',
249
+ marginBottom: 'var(--cfm-mcq-option-gap, 8px)',
250
+ }}
215
251
  >
216
252
  <input
217
253
  type="radio"
@@ -245,11 +281,19 @@ export default function Question({
245
281
 
246
282
  {question.type === QUESTION_TYPE.TEXTFIELD && (
247
283
  <textarea
284
+ data-cfm-textfield
248
285
  rows={question.inputVariant === 'long' ? 4 : 1}
249
286
  value={(selectedValue as string) ?? ''}
250
287
  onChange={e => onSelect(e.target.value)}
251
288
  placeholder={question.placeholder || 'Type your response here...'}
252
- className="w-full rounded border border-gray-300 p-4 text-sm outline-none focus:border-[#e20074]"
289
+ className="w-full outline-none"
290
+ style={{
291
+ height: question.inputVariant === 'long' ? undefined : 'var(--cfm-input-height, 44px)',
292
+ borderRadius: 'var(--cfm-input-radius, 8px)',
293
+ border: '1px solid var(--cfm-input-border, #d1d5db)',
294
+ padding: 'var(--cfm-input-padding, 12px)',
295
+ color: 'var(--cfm-text)',
296
+ }}
253
297
  />
254
298
  )}
255
299
 
@@ -1,4 +1,11 @@
1
+ 'use client';
2
+
1
3
  import type { AnswerValue, NpsScalePoint } from '@explorer02/cfm-survey-sdk';
4
+ import {
5
+ scaleRadioRingStyle,
6
+ scaleRadioDotStyle,
7
+ scaleCellSelectedStyle,
8
+ } from '@/lib/surveyUi/selectionStyles';
2
9
  import { getAccentColor, npsValueFromOption, starValueFromOption } from './surveyUiScaleUtils';
3
10
  import { getEmojiForIndex } from './surveyUiIcons';
4
11
 
@@ -11,6 +18,25 @@ type RatingScaleProps = {
11
18
  buttonStyle?: 'standard' | 'numbered' | 'emoji';
12
19
  };
13
20
 
21
+ function resolveBadgeColor(index: number, value: number): string {
22
+ const mode = getComputedStyle(document.documentElement)
23
+ .getPropertyValue('--cfm-number-label-mode')
24
+ .trim();
25
+ if (mode === 'monochrome') {
26
+ return (
27
+ getComputedStyle(document.documentElement).getPropertyValue('--cfm-number-mono-color').trim() ||
28
+ 'var(--cfm-primary)'
29
+ );
30
+ }
31
+ if (mode === 'individual') {
32
+ return (
33
+ getComputedStyle(document.documentElement).getPropertyValue(`--cfm-number-color-${index}`).trim() ||
34
+ getAccentColor(value)
35
+ );
36
+ }
37
+ return getAccentColor(value);
38
+ }
39
+
14
40
  export default function RatingScale({
15
41
  questionId,
16
42
  options,
@@ -24,46 +50,33 @@ export default function RatingScale({
24
50
  variant === 'star'
25
51
  ? starValueFromOption(option, index)
26
52
  : npsValueFromOption(option as NpsScalePoint, index);
53
+ const numericValue = typeof value === 'number' ? value : index;
27
54
  const color =
28
- variant === 'nps' && typeof value === 'number' ? getAccentColor(value) : '#e20074';
29
- return {
30
- id: option.id,
31
- label: option.optionLabel,
32
- value,
33
- color,
34
- };
55
+ variant === 'nps' ? resolveBadgeColor(index, numericValue) : 'var(--cfm-primary)';
56
+ return { id: option.id, label: option.optionLabel, value, color };
35
57
  });
36
58
 
37
59
  const columnCount = resolvedOptions.length;
38
60
  const gridStyle = {
39
61
  display: 'grid' as const,
40
62
  gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))`,
41
- gap: '2px',
63
+ gap: 'var(--cfm-nps-cell-gap, 2px)',
42
64
  };
43
65
 
44
66
  if (buttonStyle === 'emoji') {
45
67
  return (
46
- <div role="radiogroup" className="w-full overflow-x-auto">
68
+ <div role="radiogroup" className="w-full overflow-x-auto" data-cfm-nps-area>
47
69
  <div className="min-w-0" style={gridStyle}>
48
70
  {resolvedOptions.map((option, index) => {
49
71
  const isSelected = selectedValue === option.value;
50
72
  return (
51
- <label
52
- key={`${questionId}-${option.value}`}
53
- title={option.label}
54
- className="flex cursor-pointer justify-center"
55
- >
56
- <input
57
- type="radio"
58
- name={questionId}
59
- checked={isSelected}
60
- onChange={() => onSelect(option.value)}
61
- className="sr-only"
62
- />
73
+ <label key={`${questionId}-${option.value}`} title={option.label} className="flex cursor-pointer justify-center">
74
+ <input type="radio" name={questionId} checked={isSelected} onChange={() => onSelect(option.value)} className="sr-only" />
63
75
  <div
64
- className={`flex h-10 w-10 sm:h-12 sm:w-12 items-center justify-center rounded-lg border-2 text-2xl transition-all ${
65
- isSelected ? 'border-[#e20074] bg-[#fdf2f8] shadow-md' : 'border-transparent'
66
- }`}
76
+ style={{
77
+ ...scaleRadioRingStyle(isSelected, { pill: false, size: 'var(--cfm-csat-emoji-size, 48px)' }),
78
+ fontSize: 'var(--cfm-csat-emoji-size, 24px)',
79
+ }}
67
80
  >
68
81
  {getEmojiForIndex(index, columnCount)}
69
82
  </div>
@@ -76,48 +89,42 @@ export default function RatingScale({
76
89
  }
77
90
 
78
91
  return (
79
- <div role="radiogroup" className="w-full overflow-x-auto">
92
+ <div role="radiogroup" className="w-full overflow-x-auto" data-cfm-nps-area>
80
93
  <div className="mb-3 min-w-0" style={gridStyle}>
81
- {resolvedOptions.map(option => (
82
- <div key={`${questionId}-badge-${option.value}`} className="flex justify-center">
83
- <div
84
- className="flex h-5 w-5 sm:h-6 sm:w-6 items-center justify-center rounded-[3px] text-[10px] sm:text-xs font-bold text-white shadow-sm"
85
- style={{ backgroundColor: option.color }}
86
- title={option.label}
87
- >
88
- {option.label}
94
+ {resolvedOptions.map((option, index) => {
95
+ const isSelected = selectedValue === option.value;
96
+ return (
97
+ <div key={`${questionId}-badge-${option.value}`} className="flex justify-center">
98
+ <div
99
+ className="flex items-center justify-center rounded-[3px] text-[10px] font-bold text-white shadow-sm sm:text-xs"
100
+ style={{
101
+ width: 'var(--cfm-nps-cell-size, 24px)',
102
+ height: 'var(--cfm-nps-cell-size, 24px)',
103
+ ...(isSelected
104
+ ? scaleCellSelectedStyle(true)
105
+ : { backgroundColor: option.color }),
106
+ }}
107
+ title={option.label}
108
+ >
109
+ {option.label}
110
+ </div>
89
111
  </div>
90
- </div>
91
- ))}
112
+ );
113
+ })}
92
114
  </div>
93
115
 
94
- <div className="relative w-full rounded-md bg-[#f2f2f2] px-1 py-2">
116
+ <div
117
+ className="relative w-full rounded-md px-1 py-2"
118
+ style={{ background: 'var(--cfm-nps-track-bar, #f2f2f2)' }}
119
+ >
95
120
  <div className="min-w-0" style={gridStyle}>
96
- {resolvedOptions.map(option => {
121
+ {resolvedOptions.map((option) => {
97
122
  const isSelected = selectedValue === option.value;
98
123
  return (
99
- <label
100
- key={`${questionId}-${option.value}`}
101
- title={option.label}
102
- className="flex cursor-pointer justify-center"
103
- >
104
- <input
105
- type="radio"
106
- name={questionId}
107
- checked={isSelected}
108
- onChange={() => onSelect(option.value)}
109
- className="sr-only"
110
- />
111
- <div
112
- className={`flex h-8 w-8 sm:h-10 sm:w-10 items-center justify-center rounded-full border-2 transition-all ${
113
- isSelected ? 'border-[#e20074] bg-[#fbe8f3]' : 'border-transparent bg-white'
114
- }`}
115
- >
116
- <div
117
- className={`h-3 w-3 sm:h-4 sm:w-4 rounded-full ${
118
- isSelected ? 'bg-[#e20074]' : 'bg-gray-300'
119
- }`}
120
- />
124
+ <label key={`${questionId}-${option.value}`} title={option.label} className="flex cursor-pointer justify-center">
125
+ <input type="radio" name={questionId} checked={isSelected} onChange={() => onSelect(option.value)} className="sr-only" />
126
+ <div style={scaleRadioRingStyle(isSelected, { size: 'var(--cfm-nps-cell-size, 40px)' })}>
127
+ <div style={scaleRadioDotStyle(isSelected)} />
121
128
  </div>
122
129
  </label>
123
130
  );
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import Header from '@/components/Header';
2
4
  import ProgressBar from '@/components/ProgressBar';
3
5
 
@@ -6,17 +8,29 @@ type SurveyStickyChromeProps = {
6
8
  };
7
9
 
8
10
  /**
9
- * Sticky top chrome: brand header + progress bar.
10
- * Keeps progress visible while scrolling long pages (e.g. last question below the fold).
11
+ * Sticky chrome header and progress are SEPARATE targets for wizard layout toggles.
12
+ * - showHeader .cfm-header only
13
+ * - showProgressBar → [data-cfm-progress] / .cfm-progress only
14
+ * Do NOT put cfm-header on this wrapper.
11
15
  */
12
16
  export default function SurveyStickyChrome({ progressPercentage }: SurveyStickyChromeProps) {
13
17
  return (
14
18
  <div
15
- className="sticky top-0 z-30 shadow-[0_2px_8px_rgba(0,0,0,0.06)]"
16
- style={{ background: 'var(--cfm-header-bg, #fff)' }}
19
+ className="cfm-sticky-chrome sticky top-0 z-30"
20
+ style={{
21
+ background: 'var(--cfm-header-bg, #fff)',
22
+ boxShadow: 'var(--cfm-header-shadow, 0 2px 8px rgba(0,0,0,0.06))',
23
+ }}
17
24
  >
18
25
  <Header embedded />
19
- <div className="mx-auto w-full max-w-4xl px-4 pb-5 pt-4 sm:px-6 lg:px-8">
26
+ <div
27
+ className="mx-auto w-full px-4 sm:px-6 lg:px-8"
28
+ style={{
29
+ maxWidth: 'var(--cfm-max-width, 48rem)',
30
+ paddingTop: 'var(--cfm-sticky-pt, 16px)',
31
+ paddingBottom: 'var(--cfm-sticky-pb, 20px)',
32
+ }}
33
+ >
20
34
  <ProgressBar progressPercentage={progressPercentage} />
21
35
  </div>
22
36
  </div>