@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.
- package/dist/cli/index.js +45 -45
- package/dist/cli/index.mjs +45 -45
- package/package.json +1 -1
- package/templates/docs/00-integration/agent-execution-flow.md +18 -6
- package/templates/docs/00-integration/agent-operating-contract.md +2 -0
- package/templates/docs/00-integration/apply-ui-config.md +15 -2
- package/templates/docs/00-integration/component-checklist.md +5 -3
- package/templates/docs/00-integration/ui-customization-wizard.md +1 -1
- package/templates/docs/00-integration/wizard-chrome-contract.md +284 -0
- package/templates/docs/00-integration/wizard-config-handoff.md +7 -1
- package/templates/docs/00-integration/wizard-customization-catalog.md +391 -0
- package/templates/docs/00-integration/wizard-post-customize-build.md +405 -0
- package/templates/docs/00-integration/wizard-preview-build-guide.md +20 -4
- package/templates/docs/00-integration/wizard-question-type-styling.md +27 -6
- package/templates/docs/01-components/19-survey-sticky-chrome.md +16 -55
- package/templates/docs/03-ui-specs/README.md +1 -1
- package/templates/docs/MANIFEST.json +10 -1
- package/templates/docs/index.md +10 -7
- package/templates/docs/templates/Footer.tsx +72 -0
- package/templates/docs/templates/Header.tsx +110 -0
- package/templates/docs/templates/LanguageSelector.tsx +40 -0
- package/templates/docs/templates/PreviewBridgeInit.tsx +13 -0
- package/templates/docs/templates/ProgressBar.tsx +33 -0
- package/templates/docs/templates/Question.tsx +51 -7
- package/templates/docs/templates/RatingScale.tsx +66 -59
- package/templates/docs/templates/SurveyStickyChrome.tsx +19 -5
- package/templates/docs/templates/uiConfig.ts +33 -0
- package/templates/docs/templates/verify-agent-build.sh +31 -1
- package/templates/preview-harness/preview-bridge.inline.js +18 -4
- package/templates/preview-harness/vite-app/src/preview-live-overrides.css +7 -0
- package/templates/previewBridge.ts +144 -39
- package/templates/survey-theme.css +7 -0
- package/templates/wizard-dist/assets/{PreviewMock-AoZdAEPZ.js → PreviewMock-tTMvwuEy.js} +1 -1
- package/templates/wizard-dist/assets/{TypePanel-39FnbtvG.js → TypePanel-B9Ls7BSq.js} +1 -1
- package/templates/wizard-dist/assets/index-CjkIlOUI.css +1 -0
- package/templates/wizard-dist/assets/index-CoCrnnxt.js +34 -0
- package/templates/wizard-dist/index.html +2 -2
- package/templates/wizard-dist/assets/index-BhWM50Yu.css +0 -1
- package/templates/wizard-dist/assets/index-CWKdsJOp.js +0 -34
package/templates/docs/index.md
CHANGED
|
@@ -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 | `
|
|
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:** `
|
|
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-
|
|
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
|
|
105
|
-
|
|
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">
|
|
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 &&
|
|
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=
|
|
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=
|
|
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
|
|
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'
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
isSelected
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
)
|
|
112
|
+
);
|
|
113
|
+
})}
|
|
92
114
|
</div>
|
|
93
115
|
|
|
94
|
-
<div
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
10
|
-
*
|
|
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
|
|
16
|
-
style={{
|
|
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
|
|
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>
|