@explorer02/cfm-survey-sdk 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/docs/00-integration/client-lib-folder.md +1 -0
- package/templates/docs/00-integration/wizard-chrome-contract.md +2 -1
- package/templates/docs/00-integration/wizard-preview-build-guide.md +17 -0
- package/templates/docs/01-components/03-rating-scale.md +8 -3
- package/templates/docs/02-reference/value-derivation.md +2 -0
- package/templates/docs/03-ui-specs/01-rating.md +15 -1
- package/templates/docs/templates/Header.tsx +1 -0
- package/templates/docs/templates/RatingScale.tsx +73 -37
- package/templates/docs/templates/selectionStyles.ts +3 -2
- package/templates/docs/templates/surveyUiScaleUtils.ts +15 -1
- package/templates/preview-harness/preview-bridge.inline.js +44 -0
- package/templates/preview-harness/previewPages.js +76 -48
- package/templates/previewBridge.ts +33 -0
package/package.json
CHANGED
|
@@ -81,6 +81,7 @@ Exports used by portable scale templates:
|
|
|
81
81
|
| `tickColorFromIndex(index, total)` | SLIDER_MATRIX header tick badge colors |
|
|
82
82
|
| `matrixColumnStoredValue(col)` | Matrix answer storage — column `id` or `null` for N/A |
|
|
83
83
|
| `npsValueFromOption` / `starValueFromOption` | NPS badge numeric values |
|
|
84
|
+
| `isScaleAnswerSelected` | NPS/star `checked` state — loose number compare |
|
|
84
85
|
| `nonNaScaleColumns` | Filter N/A column from scale columns |
|
|
85
86
|
|
|
86
87
|
Prefer SDK `matrixColumnStoredValue` when importing from package; template re-exports the same contract for co-located components.
|
|
@@ -22,7 +22,8 @@ Pipeline: [`wizard-preview-build-guide.md`](wizard-preview-build-guide.md) · [`
|
|
|
22
22
|
| Scale selection colors not updating | Import `selectionStyles.ts`; use `var(--cfm-matrix-selected)` / `var(--cfm-input-focus-ring)` |
|
|
23
23
|
| Company name not showing after logo | Copy `Header.tsx` template — brand row is `[logo][data-cfm-company]` in one flex container |
|
|
24
24
|
| Company shows when empty | Bridge clears `[data-cfm-company]` when `companyName` is `""` — do not hardcode "Company Name" as visible default |
|
|
25
|
-
| NPS
|
|
25
|
+
| NPS clicks do nothing / no visible selection before wizard | Copy latest `RatingScale.tsx` + `selectionStyles.ts` + `surveyUiScaleUtils.ts`; use `isScaleAnswerSelected()`; radio track row must use `label` + `onChange` → `onSelect(number)` — see `03-ui-specs/01-rating.md` |
|
|
26
|
+
| NPS colors / rings not updating | Copy `RatingScale.tsx` + `selectionStyles.ts` — hex fallbacks in `selectionStyles.ts` for pre-theme builds |
|
|
26
27
|
| Text placeholder color not updating | `data-cfm-textfield` + `--cfm-input-placeholder` in CSS / `::placeholder` rule |
|
|
27
28
|
| Matrix 2nd row toggle | Harness adds row via `previewMultiStatement` — `LikertMatrixScale` must render all `statementRows` from props |
|
|
28
29
|
| Blank page after deploy | **Never** reference `UI_CONFIG` without import — use `src/lib/uiConfig.ts` |
|
|
@@ -50,6 +50,23 @@ If components use **hardcoded** colors (`bg-[#e20074]`) with no `var(--cfm-*)` h
|
|
|
50
50
|
|
|
51
51
|
**Default for production wizard:** agent components. Use templates-only for SDK smoke tests, not client delivery.
|
|
52
52
|
|
|
53
|
+
### Wizard static preview vs production survey
|
|
54
|
+
|
|
55
|
+
During `npx cfm-sdk customize`, the right panel loads **SDK static HTML** from `preview-harness/previewPages.js` — not your React tree. Layout matches the original survey-page mock; **customization** is driven by `--cfm-*` CSS vars + `preview-bridge.inline.js`.
|
|
56
|
+
|
|
57
|
+
| Wizard control | Static preview (iframe) | Production survey (after Phase 6c) |
|
|
58
|
+
|----------------|-------------------------|-------------------------------------|
|
|
59
|
+
| NPS hints + number labels | Hint text + badge colors via bridge | Copy `RatingScale.tsx` + `Question.tsx` hint row |
|
|
60
|
+
| Matrix multi-row toggle | Shows/hides `[data-cfm-matrix-row-extra]` | `LikertMatrixScale` maps all `statementRows` |
|
|
61
|
+
| Matrix row width + label items | `--cfm-matrix-row-width`, column label vars | `LikertMatrixScale` + `labelStyles.ts` |
|
|
62
|
+
| CSAT/Rating star/numbered/radio | Theme focus ring + cell selected on mock cells | `CsatMatrixScale.tsx` + `selectionStyles.ts` |
|
|
63
|
+
| Slider track + tick labels | `--cfm-slider-*` on static slider mock | `SliderMatrixScale` + `CustomSliderTrack` |
|
|
64
|
+
| File upload dropzone | `--cfm-upload-*` on `.cfm-dropzone` | `FileUploadScale.tsx` |
|
|
65
|
+
| Rank / heatmap | `--cfm-rank-*` / `--cfm-heatmap-pin-*` | `RankOrderScale.tsx` / `HeatmapScale.tsx` |
|
|
66
|
+
| Logo size slider | `--cfm-header-logo-width/height` + `max-height` in header | `Header.tsx` via `uiConfig.ts` |
|
|
67
|
+
|
|
68
|
+
**Rule:** If a wizard control updates the static preview but not localhost dev, the agent has not copied the matching template from `docs/templates/` — see [`wizard-chrome-contract.md`](wizard-chrome-contract.md).
|
|
69
|
+
|
|
53
70
|
---
|
|
54
71
|
|
|
55
72
|
## Required project artifacts (Phase 5)
|
|
@@ -21,9 +21,14 @@ Render a horizontal sequence of numbered badges with traffic-light colors (NPS 0
|
|
|
21
21
|
## UI/UX Rules (CRITICAL)
|
|
22
22
|
|
|
23
23
|
1. **Shape**: Square badges with rounded corners — not full circles
|
|
24
|
-
2. **
|
|
25
|
-
3. **
|
|
26
|
-
4. **
|
|
24
|
+
2. **Interactive row**: Only the **radio track row** accepts clicks (`label` + hidden `input type="radio"`)
|
|
25
|
+
3. **Selection compare**: Use `isScaleAnswerSelected(selectedValue, optionValue)` — store/compare **numbers**, not option ids
|
|
26
|
+
4. **Unselected State**: Gray ring (`#d1d5db`) + white fill on track row
|
|
27
|
+
5. **Selected State**: Focus ring `#e20074` (or `--cfm-input-focus-ring`) + annulus `#fbe8f3` (or `--cfm-matrix-selected`) + filled dot
|
|
28
|
+
6. **Tooltips**: `title={option.label}` on every clickable cell
|
|
29
|
+
7. **`buttonStyle: 'standard'`**: Radio track only — omit numbered badge row
|
|
30
|
+
|
|
31
|
+
Reference implementation: [`apps/client/src/components/RatingScale.tsx`](../../../../apps/client/src/components/RatingScale.tsx)
|
|
27
32
|
|
|
28
33
|
## SDK Integration Checklist (agent)
|
|
29
34
|
|
|
@@ -39,6 +39,8 @@ function npsValueFromOption(option: NpsScalePoint, index: number): number {
|
|
|
39
39
|
|
|
40
40
|
NPS questions use `question.type === 'NPS_SCALE'`. Store a **number** in `SurveyAnswers`. Badge colors: traffic-light table keyed by numeric value (0–10).
|
|
41
41
|
|
|
42
|
+
Compare selection with `isScaleAnswerSelected(selectedValue, npsValueFromOption(option, index))` — do not compare to `option.id`.
|
|
43
|
+
|
|
42
44
|
## MCQ
|
|
43
45
|
|
|
44
46
|
- Selected answer: **`option.id`** (string), not a separate `optionValue` field
|
|
@@ -31,11 +31,22 @@ When `reverseScaleOrder === true`, map visual index `i` to stored index `options
|
|
|
31
31
|
## Answer Mutation
|
|
32
32
|
|
|
33
33
|
```typescript
|
|
34
|
+
import { isScaleAnswerSelected, npsValueFromOption } from '@/lib/surveyUiScaleUtils';
|
|
35
|
+
|
|
34
36
|
const value = npsValueFromOption(option, index);
|
|
35
|
-
onSelect(value) // number
|
|
37
|
+
onSelect(value); // number — never option.id
|
|
36
38
|
// → onAction CHANGE { questionId, answerValue: number }
|
|
39
|
+
|
|
40
|
+
// Compare stored answer to option value (handles string/number drift):
|
|
41
|
+
const isSelected = isScaleAnswerSelected(selectedValue, value);
|
|
37
42
|
```
|
|
38
43
|
|
|
44
|
+
**Selection row (required):** Only the **radio track row** is interactive — use `<label>` + hidden `<input type="radio">` + `onChange={() => onSelect(option.value)}`. Mirror [`apps/client/src/components/RatingScale.tsx`](../../../../apps/client/src/components/RatingScale.tsx) (reference monorepo).
|
|
45
|
+
|
|
46
|
+
**Pre-customization defaults:** Copy `selectionStyles.ts` with `#e20074` / `#fbe8f3` hex fallbacks so selected rings are visible before `survey-theme.css` loads. Import `survey-theme.css` in `globals.css` for full wizard tokens.
|
|
47
|
+
|
|
48
|
+
**`buttonStyle`:** `standard` = radio track only (no numbered badge row). `numbered` = badge row + track. `emoji` = emoji cells.
|
|
49
|
+
|
|
39
50
|
See [`02-reference/value-derivation.md`](../02-reference/value-derivation.md).
|
|
40
51
|
|
|
41
52
|
## Agent Checklist
|
|
@@ -59,6 +70,9 @@ See [`02-reference/value-derivation.md`](../02-reference/value-derivation.md).
|
|
|
59
70
|
### Wizard checklist
|
|
60
71
|
|
|
61
72
|
- [ ] `selectionStyles.ts` imported in `RatingScale.tsx`
|
|
73
|
+
- [ ] `isScaleAnswerSelected()` used for `checked` state — not raw `===` on `selectedValue`
|
|
74
|
+
- [ ] Radio track: `label` + `sr-only` radio + `onChange` → `onSelect(number)` (see client `RatingScale.tsx`)
|
|
62
75
|
- [ ] Selected radio: outer border + dot = focus ring; annulus = cell selected
|
|
63
76
|
- [ ] Selected numbered badge: border = focus ring, fill = cell selected, text white
|
|
64
77
|
- [ ] Unselected badges keep traffic-light / per-number `--cfm-number-*` colors
|
|
78
|
+
- [ ] `buttonStyle: 'standard'` hides badge row; track row remains clickable
|
|
@@ -56,6 +56,7 @@ export default function Header({ embedded = false }: HeaderProps) {
|
|
|
56
56
|
style={{
|
|
57
57
|
width: 'var(--cfm-header-logo-width, 120px)',
|
|
58
58
|
height: 'var(--cfm-header-logo-height, 120px)',
|
|
59
|
+
maxHeight: 'calc(var(--cfm-header-height, 120px) - 8px)',
|
|
59
60
|
padding: 'var(--cfm-header-logo-padding, 10px)',
|
|
60
61
|
background: 'transparent',
|
|
61
62
|
}}
|
|
@@ -6,7 +6,12 @@ import {
|
|
|
6
6
|
scaleRadioDotStyle,
|
|
7
7
|
scaleCellSelectedStyle,
|
|
8
8
|
} from '@/lib/surveyUi/selectionStyles';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
getAccentColor,
|
|
11
|
+
isScaleAnswerSelected,
|
|
12
|
+
npsValueFromOption,
|
|
13
|
+
starValueFromOption,
|
|
14
|
+
} from './surveyUiScaleUtils';
|
|
10
15
|
import { getEmojiForIndex } from './surveyUiIcons';
|
|
11
16
|
|
|
12
17
|
type RatingScaleProps = {
|
|
@@ -19,18 +24,18 @@ type RatingScaleProps = {
|
|
|
19
24
|
};
|
|
20
25
|
|
|
21
26
|
function resolveBadgeColor(index: number, value: number): string {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
if (typeof document === 'undefined') return getAccentColor(value);
|
|
28
|
+
|
|
29
|
+
const root = document.documentElement;
|
|
30
|
+
const mode = getComputedStyle(root).getPropertyValue('--cfm-number-label-mode').trim();
|
|
25
31
|
if (mode === 'monochrome') {
|
|
26
32
|
return (
|
|
27
|
-
getComputedStyle(
|
|
28
|
-
'var(--cfm-primary)'
|
|
33
|
+
getComputedStyle(root).getPropertyValue('--cfm-number-mono-color').trim() || getAccentColor(value)
|
|
29
34
|
);
|
|
30
35
|
}
|
|
31
36
|
if (mode === 'individual') {
|
|
32
37
|
return (
|
|
33
|
-
getComputedStyle(
|
|
38
|
+
getComputedStyle(root).getPropertyValue(`--cfm-number-color-${index}`).trim() ||
|
|
34
39
|
getAccentColor(value)
|
|
35
40
|
);
|
|
36
41
|
}
|
|
@@ -52,7 +57,7 @@ export default function RatingScale({
|
|
|
52
57
|
: npsValueFromOption(option as NpsScalePoint, index);
|
|
53
58
|
const numericValue = typeof value === 'number' ? value : index;
|
|
54
59
|
const color =
|
|
55
|
-
variant === 'nps' ? resolveBadgeColor(index, numericValue) : 'var(--cfm-primary)';
|
|
60
|
+
variant === 'nps' ? resolveBadgeColor(index, numericValue) : 'var(--cfm-primary, #e20074)';
|
|
56
61
|
return { id: option.id, label: option.optionLabel, value, color };
|
|
57
62
|
});
|
|
58
63
|
|
|
@@ -62,19 +67,33 @@ export default function RatingScale({
|
|
|
62
67
|
gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))`,
|
|
63
68
|
gap: 'var(--cfm-nps-cell-gap, 2px)',
|
|
64
69
|
};
|
|
70
|
+
const showNumberBadges = buttonStyle !== 'standard';
|
|
65
71
|
|
|
66
72
|
if (buttonStyle === 'emoji') {
|
|
67
73
|
return (
|
|
68
74
|
<div role="radiogroup" className="w-full overflow-x-auto" data-cfm-nps-area>
|
|
69
75
|
<div className="min-w-0" style={gridStyle}>
|
|
70
76
|
{resolvedOptions.map((option, index) => {
|
|
71
|
-
const isSelected = selectedValue
|
|
77
|
+
const isSelected = isScaleAnswerSelected(selectedValue, option.value);
|
|
72
78
|
return (
|
|
73
|
-
<label
|
|
74
|
-
|
|
79
|
+
<label
|
|
80
|
+
key={`${questionId}-${option.value}`}
|
|
81
|
+
title={option.label}
|
|
82
|
+
className="flex cursor-pointer justify-center"
|
|
83
|
+
>
|
|
84
|
+
<input
|
|
85
|
+
type="radio"
|
|
86
|
+
name={questionId}
|
|
87
|
+
checked={isSelected}
|
|
88
|
+
onChange={() => onSelect(option.value)}
|
|
89
|
+
className="sr-only"
|
|
90
|
+
/>
|
|
75
91
|
<div
|
|
76
92
|
style={{
|
|
77
|
-
...scaleRadioRingStyle(isSelected, {
|
|
93
|
+
...scaleRadioRingStyle(isSelected, {
|
|
94
|
+
pill: false,
|
|
95
|
+
size: 'var(--cfm-csat-emoji-size, 48px)',
|
|
96
|
+
}),
|
|
78
97
|
fontSize: 'var(--cfm-csat-emoji-size, 24px)',
|
|
79
98
|
}}
|
|
80
99
|
>
|
|
@@ -90,28 +109,30 @@ export default function RatingScale({
|
|
|
90
109
|
|
|
91
110
|
return (
|
|
92
111
|
<div role="radiogroup" className="w-full overflow-x-auto" data-cfm-nps-area>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<div
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
112
|
+
{showNumberBadges && (
|
|
113
|
+
<div className="mb-3 min-w-0" style={gridStyle}>
|
|
114
|
+
{resolvedOptions.map((option) => {
|
|
115
|
+
const isSelected = isScaleAnswerSelected(selectedValue, option.value);
|
|
116
|
+
return (
|
|
117
|
+
<div key={`${questionId}-badge-${option.value}`} className="flex justify-center">
|
|
118
|
+
<div
|
|
119
|
+
className="flex items-center justify-center rounded-[3px] text-[10px] font-bold text-white shadow-sm sm:text-xs"
|
|
120
|
+
style={{
|
|
121
|
+
width: 'var(--cfm-nps-cell-size, 24px)',
|
|
122
|
+
height: 'var(--cfm-nps-cell-size, 24px)',
|
|
123
|
+
...(isSelected
|
|
124
|
+
? scaleCellSelectedStyle(true)
|
|
125
|
+
: { backgroundColor: option.color }),
|
|
126
|
+
}}
|
|
127
|
+
title={option.label}
|
|
128
|
+
>
|
|
129
|
+
{option.label}
|
|
130
|
+
</div>
|
|
110
131
|
</div>
|
|
111
|
-
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
|
|
132
|
+
);
|
|
133
|
+
})}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
115
136
|
|
|
116
137
|
<div
|
|
117
138
|
className="relative w-full rounded-md px-1 py-2"
|
|
@@ -119,11 +140,26 @@ export default function RatingScale({
|
|
|
119
140
|
>
|
|
120
141
|
<div className="min-w-0" style={gridStyle}>
|
|
121
142
|
{resolvedOptions.map((option) => {
|
|
122
|
-
const isSelected = selectedValue
|
|
143
|
+
const isSelected = isScaleAnswerSelected(selectedValue, option.value);
|
|
123
144
|
return (
|
|
124
|
-
<label
|
|
125
|
-
|
|
126
|
-
|
|
145
|
+
<label
|
|
146
|
+
key={`${questionId}-${option.value}`}
|
|
147
|
+
title={option.label}
|
|
148
|
+
className="flex cursor-pointer justify-center"
|
|
149
|
+
>
|
|
150
|
+
<input
|
|
151
|
+
type="radio"
|
|
152
|
+
name={questionId}
|
|
153
|
+
checked={isSelected}
|
|
154
|
+
onChange={() => onSelect(option.value)}
|
|
155
|
+
className="sr-only"
|
|
156
|
+
/>
|
|
157
|
+
<div
|
|
158
|
+
className="flex items-center justify-center rounded-full border-2 transition-all"
|
|
159
|
+
style={scaleRadioRingStyle(isSelected, {
|
|
160
|
+
size: 'var(--cfm-nps-cell-size, 40px)',
|
|
161
|
+
})}
|
|
162
|
+
>
|
|
127
163
|
<div style={scaleRadioDotStyle(isSelected)} />
|
|
128
164
|
</div>
|
|
129
165
|
</label>
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { CSSProperties } from 'react';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
export const
|
|
11
|
+
/** Terminal hex fallbacks match apps/client RatingScale — visible before survey-theme.css loads. */
|
|
12
|
+
export const cellSelectedVar = 'var(--cfm-matrix-selected, var(--cfm-primary, #fbe8f3))';
|
|
13
|
+
export const focusRingVar = 'var(--cfm-input-focus-ring, var(--cfm-primary, #e20074))';
|
|
13
14
|
|
|
14
15
|
const unselectedRing = '#d1d5db';
|
|
15
16
|
const unselectedDot = '#d1d5db';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Portable scale utilities — copy to src/lib/surveyUiScaleUtils.ts or co-locate with components.
|
|
3
3
|
*/
|
|
4
|
-
import type { NpsScalePoint, ScaleColumn } from '@explorer02/cfm-survey-sdk';
|
|
4
|
+
import type { AnswerValue, NpsScalePoint, ScaleColumn } from '@explorer02/cfm-survey-sdk';
|
|
5
5
|
|
|
6
6
|
export const SCALE_COLORS: readonly string[] = [
|
|
7
7
|
'#e2001a', '#e4251b', '#ec610a', '#f18b00', '#f7b200', '#fcd900',
|
|
@@ -32,6 +32,20 @@ export function npsValueFromOption(option: NpsScalePoint, index: number): number
|
|
|
32
32
|
return Number.isFinite(numeric) && option.optionLabel.trim() !== '' ? numeric : index;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** Loose match for NPS/star stored answers (number) vs derived option values. */
|
|
36
|
+
export function isScaleAnswerSelected(
|
|
37
|
+
selectedValue: AnswerValue | undefined,
|
|
38
|
+
optionValue: number,
|
|
39
|
+
): boolean {
|
|
40
|
+
if (selectedValue === undefined || selectedValue === null) return false;
|
|
41
|
+
if (typeof selectedValue === 'number') return selectedValue === optionValue;
|
|
42
|
+
if (typeof selectedValue === 'string') {
|
|
43
|
+
const parsed = Number(selectedValue);
|
|
44
|
+
return Number.isFinite(parsed) && parsed === optionValue;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
35
49
|
/** Matches SDK matrixColumnStoredValue — scale-matrix answers store column ids. */
|
|
36
50
|
export function matrixColumnStoredValue(column: { id: string; label: string }): string | null {
|
|
37
51
|
return column.id === 'na' ? null : column.id;
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
delete pending[k];
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
applyThemeSideEffects();
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function queue(vars) {
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
root.style.setProperty(k, vars[k]);
|
|
34
35
|
}
|
|
35
36
|
}
|
|
37
|
+
applyThemeSideEffects();
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
function setDisplay(selector, visible) {
|
|
@@ -41,6 +43,44 @@
|
|
|
41
43
|
});
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
var TRAFFIC = ['#ef4444', '#f97316', '#eab308', '#84cc16', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7'];
|
|
47
|
+
|
|
48
|
+
function readVar(name, fallback) {
|
|
49
|
+
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function applyNumberBadgeColors() {
|
|
53
|
+
var mode = readVar('--cfm-number-label-mode', 'traffic');
|
|
54
|
+
var mono = readVar('--cfm-number-mono-color', '#9ca3af');
|
|
55
|
+
document.querySelectorAll('[data-cfm-number-badge]').forEach(function (el) {
|
|
56
|
+
if (el.classList.contains('selected')) return;
|
|
57
|
+
var i = parseInt(el.getAttribute('data-cfm-number-badge'), 10);
|
|
58
|
+
if (isNaN(i)) return;
|
|
59
|
+
var bg = TRAFFIC[i] || '#9ca3af';
|
|
60
|
+
if (mode === 'monochrome') bg = mono;
|
|
61
|
+
if (mode === 'individual') bg = readVar('--cfm-number-color-' + i, '#9ca3af');
|
|
62
|
+
el.style.backgroundColor = bg;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function applyMultiStatement(show) {
|
|
67
|
+
document.querySelectorAll('[data-cfm-matrix-row-extra]').forEach(function (el) {
|
|
68
|
+
el.style.display = show ? '' : 'none';
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function applyDropzoneStyle() {
|
|
73
|
+
var style = readVar('--cfm-upload-border-style', 'dashed');
|
|
74
|
+
document.querySelectorAll('[data-cfm-dropzone], .cfm-dropzone').forEach(function (el) {
|
|
75
|
+
el.style.borderStyle = style;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function applyThemeSideEffects() {
|
|
80
|
+
applyNumberBadgeColors();
|
|
81
|
+
applyDropzoneStyle();
|
|
82
|
+
}
|
|
83
|
+
|
|
44
84
|
function applyContent(patch) {
|
|
45
85
|
if (!patch) return;
|
|
46
86
|
if (patch.logoUrl !== undefined) {
|
|
@@ -188,7 +228,11 @@
|
|
|
188
228
|
el.textContent = patch.previewState.hintMaxText || '';
|
|
189
229
|
});
|
|
190
230
|
}
|
|
231
|
+
if (patch.previewState.multiStatement !== undefined) {
|
|
232
|
+
applyMultiStatement(!!patch.previewState.multiStatement);
|
|
233
|
+
}
|
|
191
234
|
}
|
|
235
|
+
applyThemeSideEffects();
|
|
192
236
|
}
|
|
193
237
|
|
|
194
238
|
function handleMessage(event) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
/** Static wizard preview pages — layout matches previewPages.ts; tokens via --cfm-* + preview-bridge. */
|
|
3
4
|
const sharedStyles = `
|
|
4
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); }
|
|
5
|
-
.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:transparent; }
|
|
6
|
+
.cfm-header-logo { width:var(--cfm-header-logo-width); height:var(--cfm-header-logo-height); max-height:calc(var(--cfm-header-height, 120px) - 8px); object-fit:contain; padding:var(--cfm-header-logo-padding); background:transparent; }
|
|
6
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; }
|
|
7
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; }
|
|
8
9
|
.cfm-footer-links { display:flex; gap:24px; flex-wrap:wrap; }
|
|
@@ -11,90 +12,117 @@ const sharedStyles = `
|
|
|
11
12
|
.cfm-progress-fill { height:100%; width:45%; background:var(--cfm-progress-fill); border-radius:999px; }
|
|
12
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; }
|
|
13
14
|
.cfm-mcq-option.selected { border-color:var(--cfm-mcq-selected-accent); background:var(--cfm-mcq-selected-bg); }
|
|
14
|
-
.cfm-nps-row { display:flex; gap:var(--cfm-nps-cell-gap); flex-wrap:wrap; }
|
|
15
|
-
.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,#f2f2f2); display:flex; align-items:center; justify-content:center; font-size:14px; box-sizing:border-box; }
|
|
16
|
-
.cfm-nps-cell.selected { border:2px solid var(--cfm-input-focus-ring); background:var(--cfm-matrix-selected); color:#fff; }
|
|
17
15
|
.cfm-hint-row { display:flex; justify-content:space-between; font-size:12px; font-weight:600; margin-bottom:8px; }
|
|
18
16
|
.cfm-hint-min, .cfm-hint-max { color:var(--cfm-hint-label-color,#6b7280); background:var(--cfm-hint-label-bg,transparent); padding:2px 4px; border-radius:4px; }
|
|
19
|
-
.cfm-
|
|
17
|
+
.cfm-nps-badges { display:grid; grid-template-columns:repeat(11,minmax(0,1fr)); gap:var(--cfm-nps-cell-gap,2px); margin-bottom:8px; }
|
|
18
|
+
.cfm-nps-badge { width:var(--cfm-nps-cell-size,24px); height:var(--cfm-nps-cell-size,24px); margin:0 auto; border-radius:3px; font-size:10px; font-weight:700; color:#fff; display:flex; align-items:center; justify-content:center; box-sizing:border-box; }
|
|
19
|
+
.cfm-nps-badge.selected { border:2px solid var(--cfm-input-focus-ring); background:var(--cfm-matrix-selected); }
|
|
20
|
+
.cfm-nps-track { background:var(--cfm-nps-track-bar,#f2f2f2); border-radius:6px; padding:8px 4px; }
|
|
21
|
+
.cfm-nps-row { display:grid; grid-template-columns:repeat(11,minmax(0,1fr)); gap:var(--cfm-nps-cell-gap,2px); }
|
|
22
|
+
.cfm-nps-radio { width:var(--cfm-nps-cell-size,32px); height:var(--cfm-nps-cell-size,32px); margin:0 auto; border-radius:50%; border:2px solid #d1d5db; background:#fff; box-sizing:border-box; display:flex; align-items:center; justify-content:center; }
|
|
23
|
+
.cfm-nps-radio.selected { border-color:var(--cfm-input-focus-ring); background:var(--cfm-matrix-selected); }
|
|
24
|
+
.cfm-nps-radio.selected::after { content:''; width:33%; height:33%; border-radius:50%; background:var(--cfm-input-focus-ring); }
|
|
20
25
|
.cfm-matrix-row { display:flex; align-items:center; padding:8px 0; background:var(--cfm-row-band); margin-bottom:4px; }
|
|
21
|
-
.cfm-matrix-label { width:var(--cfm-matrix-row-width); flex-shrink:0; padding:0 16px; font-size:14px; font-weight:500; }
|
|
22
|
-
.cfm-matrix-cells { display:flex; gap:8px; flex:1; justify-content:center; }
|
|
26
|
+
.cfm-matrix-label { width:var(--cfm-matrix-row-width,180px); flex-shrink:0; padding:0 16px; font-size:14px; font-weight:500; box-sizing:border-box; }
|
|
27
|
+
.cfm-matrix-cells { display:flex; gap:8px; flex:1; justify-content:center; align-items:center; }
|
|
28
|
+
.cfm-matrix-col-label { font-size:13px; font-weight:500; text-align:center; color:var(--cfm-csat-column-label-color,var(--cfm-matrix-column-label-color,#4b5563)); background:var(--cfm-csat-column-label-bg,var(--cfm-matrix-column-label-bg,transparent)); padding:2px 6px; border-radius:4px; }
|
|
23
29
|
.cfm-matrix-cell { width:var(--cfm-csat-cell-size,18px); height:var(--cfm-csat-cell-size,18px); border-radius:50%; border:2px solid #d1d5db; box-sizing:border-box; }
|
|
24
30
|
.cfm-matrix-cell.selected { border-color:var(--cfm-input-focus-ring); background:var(--cfm-matrix-selected); }
|
|
25
|
-
.cfm-emoji { font-size:var(--cfm-csat-emoji-size); opacity:var(--cfm-csat-unselected-opacity); }
|
|
26
|
-
.cfm-emoji.selected { opacity:1; outline:2px solid var(--cfm-input-focus-ring); border-radius:8px;
|
|
27
|
-
.cfm-star { color:var(--cfm-input-focus-ring); font-size:var(--cfm-csat-emoji-size,24px); opacity:var(--cfm-csat-unselected-opacity); }
|
|
31
|
+
.cfm-emoji { font-size:var(--cfm-csat-emoji-size,28px); opacity:var(--cfm-csat-unselected-opacity,0.5); line-height:1; }
|
|
32
|
+
.cfm-emoji.selected { opacity:1; outline:2px solid var(--cfm-input-focus-ring); border-radius:8px; }
|
|
33
|
+
.cfm-star { color:var(--cfm-input-focus-ring); font-size:var(--cfm-csat-emoji-size,24px); opacity:var(--cfm-csat-unselected-opacity,0.5); }
|
|
28
34
|
.cfm-star.selected { opacity:1; color:var(--cfm-matrix-selected); }
|
|
29
|
-
.cfm-badge { width:40px; height:40px; border-radius:8px;
|
|
35
|
+
.cfm-badge { width:40px; height:40px; border-radius:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:600; border:1px solid #d1d5db; box-sizing:border-box; }
|
|
30
36
|
.cfm-badge.selected { border:2px solid var(--cfm-input-focus-ring); background:var(--cfm-matrix-selected); color:#fff; }
|
|
31
|
-
.cfm-slider-track { height:8px; background:var(--cfm-slider-track); border-radius:4px; position:relative; margin:24px 0; }
|
|
32
|
-
.cfm-slider-thumb { width:20px; height:20px; background:var(--cfm-slider-thumb); border-radius:50%; position:absolute; top:-6px; left:60%; }
|
|
33
|
-
.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:
|
|
34
|
-
.cfm-
|
|
37
|
+
.cfm-slider-track { height:8px; background:var(--cfm-slider-track,#e5e7eb); border-radius:4px; position:relative; margin:24px 0; }
|
|
38
|
+
.cfm-slider-thumb { width:20px; height:20px; background:var(--cfm-slider-thumb,var(--cfm-input-focus-ring)); border-radius:50%; position:absolute; top:-6px; left:60%; border:2px solid var(--cfm-input-focus-ring); box-sizing:border-box; }
|
|
39
|
+
.cfm-tick { min-width:var(--cfm-slider-tick-size,24px); height:var(--cfm-slider-tick-size,24px); background:var(--cfm-slider-tick,#6b7280); border-radius:4px; font-size:11px; display:flex; align-items:center; justify-content:center; color:#fff; padding:0 4px; }
|
|
40
|
+
.cfm-slider-tick-label { font-size:12px; font-weight:500; color:var(--cfm-slider-tick-label-color,#4b5563); background:var(--cfm-slider-tick-label-bg,transparent); padding:2px 4px; border-radius:4px; }
|
|
41
|
+
.cfm-dropzone { border-width:2px; border-style:var(--cfm-upload-border-style,dashed); border-color:var(--cfm-upload-border-color,#d1d5db); border-radius:var(--cfm-border-radius,12px); padding:var(--cfm-upload-padding,24px); background:var(--cfm-upload-fill,#f9fafb); text-align:center; color:var(--cfm-text); }
|
|
35
42
|
.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; box-sizing:border-box; }
|
|
36
|
-
.cfm-
|
|
37
|
-
.cfm-rank-
|
|
38
|
-
.cfm-
|
|
39
|
-
.cfm-
|
|
43
|
+
.cfm-input:focus { outline:2px solid var(--cfm-input-focus-ring); }
|
|
44
|
+
.cfm-rank-item { background:var(--cfm-rank-item-bg,#fff); padding:var(--cfm-rank-item-padding,12px 16px); margin-bottom:var(--cfm-rank-item-gap,8px); border-radius:var(--cfm-border-radius,8px); display:flex; align-items:center; gap:12px; }
|
|
45
|
+
.cfm-rank-badge { background:var(--cfm-rank-badge,var(--cfm-primary)); color:#fff; width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:12px; flex-shrink:0; }
|
|
46
|
+
.cfm-handle { color:var(--cfm-rank-handle,#9ca3af); cursor:grab; }
|
|
47
|
+
.cfm-rank-select { border-color:var(--cfm-rank-select-border,#d1d5db) !important; }
|
|
48
|
+
.cfm-pin { width:var(--cfm-heatmap-pin-size,16px); height:var(--cfm-heatmap-pin-size,16px); background:var(--cfm-heatmap-pin,#f97316); border:2px solid var(--cfm-heatmap-pin-border,#fff); border-radius:50%; position:absolute; box-sizing:border-box; }
|
|
40
49
|
.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; }
|
|
41
50
|
.cfm-btn-primary { background:var(--cfm-primary); color:var(--cfm-button-text); border:none; border-radius:var(--cfm-border-radius); padding:10px 20px; }
|
|
42
51
|
.cfm-btn-back { background:transparent; color:var(--cfm-primary); border:1px solid var(--cfm-primary); border-radius:var(--cfm-border-radius); padding:10px 20px; }
|
|
43
52
|
`;
|
|
44
53
|
|
|
54
|
+
const TRAFFIC = ['#ef4444', '#f97316', '#eab308', '#84cc16', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7'];
|
|
55
|
+
|
|
45
56
|
function cells(n) {
|
|
46
57
|
return Array.from({ length: n }, (_, i) =>
|
|
47
58
|
`<div class="cfm-matrix-cell${i === 2 ? ' selected' : ''}"></div>`,
|
|
48
59
|
).join('');
|
|
49
60
|
}
|
|
50
61
|
|
|
51
|
-
function
|
|
52
|
-
return
|
|
62
|
+
function colLabels(n) {
|
|
63
|
+
return Array.from({ length: n }, (_, i) => `<span class="cfm-matrix-col-label">${i + 1}</span>`).join('');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function matrixPreview(label, mode) {
|
|
67
|
+
if (mode === 'emoji') {
|
|
68
|
+
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>`;
|
|
69
|
+
}
|
|
70
|
+
return `<div class="cfm-matrix-row" style="background:transparent;margin-bottom:4px"><div class="cfm-matrix-label"></div><div class="cfm-matrix-cells">${colLabels(5)}</div></div>
|
|
71
|
+
<div class="cfm-matrix-row"><div class="cfm-matrix-label">${label}</div><div class="cfm-matrix-cells">${cells(5)}</div></div>`;
|
|
53
72
|
}
|
|
54
73
|
|
|
55
|
-
function
|
|
56
|
-
|
|
74
|
+
function npsPreview() {
|
|
75
|
+
const badges = Array.from({ length: 11 }, (_, i) =>
|
|
76
|
+
`<div class="cfm-nps-badge${i === 8 ? ' selected' : ''}" data-cfm-number-badge="${i}">${i}</div>`,
|
|
77
|
+
).join('');
|
|
78
|
+
const radios = Array.from({ length: 11 }, (_, i) =>
|
|
79
|
+
`<div class="cfm-nps-radio${i === 8 ? ' selected' : ''}"></div>`,
|
|
80
|
+
).join('');
|
|
81
|
+
return `<p style="font-weight:600;margin-bottom:8px">How likely are you to recommend us?</p>
|
|
82
|
+
<div class="cfm-hint-row"><span class="cfm-hint-min" data-cfm-hint-min>Not likely</span><span class="cfm-hint-max" data-cfm-hint-max>Very likely</span></div>
|
|
83
|
+
<div class="cfm-nps-badges">${badges}</div>
|
|
84
|
+
<div class="cfm-nps-track"><div class="cfm-nps-row">${radios}</div></div>`;
|
|
57
85
|
}
|
|
58
86
|
|
|
59
87
|
const npsCells = Array.from({ length: 11 }, (_, i) =>
|
|
60
|
-
`<div class="cfm-nps-
|
|
88
|
+
`<div class="cfm-nps-radio${i === 8 ? ' selected' : ''}"></div>`,
|
|
61
89
|
).join('');
|
|
62
90
|
|
|
63
91
|
const PREVIEW_PAGE_DEFS = [
|
|
64
92
|
{ key: 'header', title: 'Header', body: `<header class="cfm-header"><img data-cfm-logo class="cfm-header-logo" src="" alt="Logo" style="display:none" /><span data-cfm-company class="cfm-header-company">Company Name</span></header>` },
|
|
65
93
|
{ key: 'footer', title: 'Footer', body: `<footer class="cfm-footer"><div><img data-cfm-logo style="width:var(--cfm-footer-logo-width);height:var(--cfm-footer-logo-height);object-fit:contain;display:none" src="" alt="" /><p data-cfm-copyright style="font-size:12px;margin-top:8px">© Company</p></div><nav data-cfm-footer-links class="cfm-footer-links"></nav></footer>` },
|
|
66
94
|
{ key: 'chrome', title: 'Chrome', body: `<header class="cfm-header"><img data-cfm-logo class="cfm-header-logo" src="" alt="" style="display:none" /><span data-cfm-company class="cfm-header-company">Company</span></header><div class="cfm-progress"><div class="cfm-progress-fill"></div></div><p data-cfm-survey-title style="font-size:20px;font-weight:var(--cfm-heading-weight);margin:16px 0">Survey Title</p><div style="display:flex;gap:12px;margin-top:24px"><button data-cfm-btn-back class="cfm-btn-back">Back</button><button data-cfm-btn-next class="cfm-btn-primary">Next</button></div>` },
|
|
67
|
-
{ key: 'survey_page', title: 'Survey Page', body: `<header class="cfm-header"><img data-cfm-logo class="cfm-header-logo" src="" alt="" style="display:none" /><span data-cfm-company class="cfm-header-company">Company</span></header><div data-cfm-progress class="cfm-progress"><div class="cfm-progress-fill" style="width:35%"></div></div><div data-cfm-language style="text-align:right;margin:16px 0"><select class="cfm-input" style="width:auto"><option>English</option></select></div><p style="font-weight:600"><span data-cfm-question-number>1.</span> Sample question <span data-cfm-required style="color:red">*</span></p><div class="cfm-mcq-option selected"><span>◉</span> Option A</div><p style="font-weight:600;margin-top:24px"><span data-cfm-question-number>2.</span> NPS question</p><div class="cfm-nps-row">${npsCells}</div><div style="display:flex;gap:12px;margin-top:24px"><button data-cfm-btn-back class="cfm-btn-back">Back</button><button data-cfm-btn-next class="cfm-btn-primary">Next</button></div><footer class="cfm-footer" style="margin-top:32px"><p data-cfm-copyright>© Company</p><nav data-cfm-footer-links class="cfm-footer-links"></nav></footer>` },
|
|
68
|
-
{ key: 'intro', title: 'Intro', body: `<h1 data-cfm-survey-title style="font-size:28px;font-weight:var(--cfm-heading-weight)">Welcome</h1><p style="margin:16px 0;color:#6b7280">Please share your feedback.</p><button class="cfm-btn-primary">Start Survey</button>` },
|
|
95
|
+
{ key: 'survey_page', title: 'Survey Page', body: `<header class="cfm-header"><img data-cfm-logo class="cfm-header-logo" src="" alt="" style="display:none" /><span data-cfm-company class="cfm-header-company">Company</span></header><div data-cfm-progress class="cfm-progress"><div class="cfm-progress-fill" style="width:35%"></div></div><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><p style="font-weight:600"><span data-cfm-question-number>1.</span> Sample question <span data-cfm-required style="color:red">*</span></p><div class="cfm-mcq-option selected"><span>◉</span> Option A</div><p style="font-weight:600;margin-top:24px"><span data-cfm-question-number>2.</span> NPS question</p><div class="cfm-nps-track"><div class="cfm-nps-row">${npsCells}</div></div><div style="display:flex;gap:12px;margin-top:24px"><button data-cfm-btn-back class="cfm-btn-back">Back</button><button data-cfm-btn-next class="cfm-btn-primary">Next</button></div><footer class="cfm-footer" style="margin-top:32px"><p data-cfm-copyright>© Company</p><nav data-cfm-footer-links class="cfm-footer-links"></nav></footer>` },
|
|
96
|
+
{ key: 'intro', title: 'Intro', body: `<h1 data-cfm-survey-title style="font-size:28px;font-weight:var(--cfm-heading-weight)">Welcome</h1><p style="margin:16px 0;color:#6b7280">Please take a few minutes to share your feedback.</p><button class="cfm-btn-primary">Start Survey</button>` },
|
|
69
97
|
{ key: 'end', title: 'End', body: `<h1 style="font-size:28px;font-weight:var(--cfm-heading-weight)">Thank you!</h1><p data-cfm-thank-you style="margin:16px 0">Thank you for your feedback.</p>` },
|
|
70
|
-
{ key: 'MCQ_single', title: 'MCQ Single', body: `<p style="font-weight:600;margin-bottom:16px">1. Select one option</p><div data-cfm-mcq-style="outlined"><div class="cfm-mcq-option selected"><span>◉</span> Option A</div><div class="cfm-mcq-option"><span>○</span> Option B</div></div>` },
|
|
98
|
+
{ key: 'MCQ_single', title: 'MCQ Single', body: `<p style="font-weight:600;margin-bottom:16px">1. Select one option</p><div data-cfm-mcq-style="outlined"><div class="cfm-mcq-option selected"><span>◉</span> Option A</div><div class="cfm-mcq-option"><span>○</span> Option B</div><div class="cfm-mcq-option"><span>○</span> Option C</div></div>` },
|
|
71
99
|
{ key: 'MCQ_multiple', title: 'MCQ Multiple', body: `<p style="font-weight:600;margin-bottom:16px">Select all that apply</p><div class="cfm-mcq-option selected"><span>☑</span> Option A</div><div class="cfm-mcq-option"><span>☐</span> Option B</div>` },
|
|
72
|
-
{ key: 'TEXTFIELD_short', title: 'Text Short', body: `<p style="font-weight:600;margin-bottom:8px">Your name</p><input class="cfm-input" placeholder="Type here..." />` },
|
|
73
|
-
{ key: 'TEXTFIELD_long', title: 'Text Long', body: `<p style="font-weight:600;margin-bottom:8px">Comments</p><textarea class="cfm-input" style="height:120px;padding:12px" placeholder="Type your response..."></textarea>` },
|
|
74
|
-
{ key: 'NPS_SCALE', title: 'NPS', body:
|
|
75
|
-
{ key: 'CFM_likert', title: 'CFM Likert', body: matrixPreview('Statement item 1') },
|
|
76
|
-
{ key: 'CFM_bipolar', title: 'CFM Bipolar', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width)">Left</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</div></div>` },
|
|
77
|
-
{ key: 'CFM_transpose', title: 'CFM Transpose', body: matrixPreview('Scale item (transposed)') },
|
|
78
|
-
{ key: 'CFM_carousel', title: 'CFM Carousel', body: `${matrixPreview('Row 1 of 3')}<div style="text-align:center;margin-top:12px"><span style="color:var(--cfm-matrix-
|
|
79
|
-
{ key: 'CFM_dropdown', title: 'CFM Dropdown', body: `<div class="cfm-matrix-label">Statement 1</div><select class="cfm-input"
|
|
80
|
-
{ key: 'CSAT_emoji', title: 'CSAT Emoji', 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></div></div>` },
|
|
81
|
-
{ key: 'CSAT_star', title: 'CSAT Star', 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></div></div>` },
|
|
82
|
-
{ key: 'CSAT_numbered', title: 'CSAT Numbered', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">Score</div><div class="cfm-matrix-cells"
|
|
100
|
+
{ key: 'TEXTFIELD_short', title: 'Text Short', body: `<p style="font-weight:600;margin-bottom:8px">Your name</p><input class="cfm-input" data-cfm-textfield placeholder="Type here..." />` },
|
|
101
|
+
{ key: 'TEXTFIELD_long', title: 'Text Long', body: `<p style="font-weight:600;margin-bottom:8px">Comments</p><textarea class="cfm-input" data-cfm-textfield style="height:120px;padding:12px" placeholder="Type your response..."></textarea>` },
|
|
102
|
+
{ key: 'NPS_SCALE', title: 'NPS', body: npsPreview() },
|
|
103
|
+
{ key: 'CFM_likert', title: 'CFM Likert', body: `${matrixPreview('Statement item 1', 'radio')}<div class="cfm-matrix-row" data-cfm-matrix-row-extra style="display:none"><div class="cfm-matrix-label">Statement item 2</div><div class="cfm-matrix-cells">${cells(5)}</div></div>` },
|
|
104
|
+
{ key: 'CFM_bipolar', title: 'CFM Bipolar', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width,25%)">Left statement</div><div class="cfm-matrix-cells">${cells(5)}</div><div class="cfm-matrix-label" style="width:var(--cfm-matrix-bipolar-width,25%);text-align:right">Right statement</div></div>` },
|
|
105
|
+
{ key: 'CFM_transpose', title: 'CFM Transpose', body: matrixPreview('Scale item (transposed row)', 'radio') },
|
|
106
|
+
{ key: 'CFM_carousel', title: 'CFM Carousel', body: `${matrixPreview('Row 1 of 3', 'radio')}<div style="text-align:center;margin-top:12px"><span style="color:var(--cfm-matrix-selected)">●</span> ○ ○</div>` },
|
|
107
|
+
{ key: 'CFM_dropdown', title: 'CFM Dropdown', body: `<div class="cfm-matrix-label" style="width:100%;margin-bottom:8px">Statement 1</div><select class="cfm-input"><option>Select...</option><option>Col 1</option><option>Col 2</option></select>` },
|
|
108
|
+
{ key: 'CSAT_emoji', title: 'CSAT Emoji', body: `<div class="cfm-matrix-row" style="background:transparent;margin-bottom:4px"><div class="cfm-matrix-label"></div><div class="cfm-matrix-cells">${colLabels(5)}</div></div><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>` },
|
|
109
|
+
{ key: 'CSAT_star', title: 'CSAT Star', 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>` },
|
|
110
|
+
{ key: 'CSAT_numbered', title: 'CSAT Numbered', body: `<div class="cfm-matrix-row"><div class="cfm-matrix-label">Score</div><div class="cfm-matrix-cells">${[1,2,3,4,5].map((n,i)=>`<div class="cfm-badge${i===2?' selected':''}" data-cfm-number-badge="${i}">${n}</div>`).join('')}</div></div>` },
|
|
83
111
|
{ key: 'CSAT_graphics', title: 'CSAT Graphics', 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>` },
|
|
84
|
-
{ key: 'CSAT_carousel', title: 'CSAT Carousel', body: `${matrixPreview('Carousel row')}<div style="text-align:center"><span style="color:var(--cfm-
|
|
112
|
+
{ key: 'CSAT_carousel', title: 'CSAT Carousel', body: `${matrixPreview('Carousel row', 'emoji')}<div style="text-align:center"><span style="color:var(--cfm-matrix-selected)">●</span> ○</div>` },
|
|
85
113
|
{ key: 'CSAT_dropdown', title: 'CSAT Dropdown', body: `<div class="cfm-matrix-label">Statement</div><select class="cfm-input" style="margin-top:8px"><option>Choose rating...</option></select>` },
|
|
86
|
-
{ key: 'RATING_star', title: 'Rating Star', 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></div>` },
|
|
87
|
-
{ key: 'RATING_numbered', title: 'Rating Numbered', body: `<div class="cfm-matrix-cells" style="padding:16px"
|
|
88
|
-
{ key: 'RATING_radio', title: 'Rating Radio', body: matrixPreview('Recommend to a friend') },
|
|
114
|
+
{ key: 'RATING_star', title: 'Rating Star', 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>` },
|
|
115
|
+
{ key: 'RATING_numbered', title: 'Rating Numbered', body: `<div class="cfm-matrix-cells" style="padding:16px">${[1,2,3,4,5].map((n,i)=>`<div class="cfm-badge${i===1?' selected':''}" data-cfm-number-badge="${i}">${n}</div>`).join('')}</div>` },
|
|
116
|
+
{ key: 'RATING_radio', title: 'Rating Radio', body: matrixPreview('Recommend to a friend', 'radio') },
|
|
89
117
|
{ key: 'RATING_emoji', title: 'Rating Emoji', 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>` },
|
|
90
118
|
{ key: 'RATING_graphics', title: 'Rating Graphics', 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>` },
|
|
91
119
|
{ key: 'RATING_slider', title: 'Rating Slider', 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>` },
|
|
92
|
-
{ key: 'SLIDER_matrix', title: 'Slider Matrix', body: `<div
|
|
93
|
-
{ key: 'FILE_UPLOAD', title: 'File Upload', body: `<div
|
|
94
|
-
{ key: 'TEXT_AND_MEDIA', title: 'Text & Media', body: `<div class="cfm-media">Media preview</div>` },
|
|
95
|
-
{ key: 'HEATMAP', title: 'Heatmap', 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>` },
|
|
120
|
+
{ key: 'SLIDER_matrix', title: 'Slider Matrix', body: `<div style="display:flex;gap:4px;margin-bottom:16px;padding-left:var(--cfm-matrix-row-width,180px)">${[0,1,2,3,4,5].map((n,i)=>`<div class="cfm-tick" data-cfm-number-badge="${i}">${n}</div>`).join('')}</div><div style="display:flex;gap:8px;margin-bottom:8px;padding-left:var(--cfm-matrix-row-width,180px)"><span class="cfm-slider-tick-label">Low</span><span class="cfm-slider-tick-label" style="margin-left:auto">High</span></div><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><div class="cfm-matrix-row" data-cfm-matrix-row-extra style="display:none"><div class="cfm-matrix-label">Statement 2</div><div style="flex:1;padding:0 16px"><div class="cfm-slider-track"><div class="cfm-slider-thumb" style="left:30%"></div></div></div></div>` },
|
|
121
|
+
{ key: 'FILE_UPLOAD', title: 'File Upload', body: `<div class="cfm-dropzone" data-cfm-dropzone>Drop files here or click to upload</div>` },
|
|
122
|
+
{ key: 'TEXT_AND_MEDIA', title: 'Text & Media', body: `<div class="cfm-media">Media preview</div><p style="font-size:var(--cfm-media-caption-size,12px);color:var(--cfm-media-caption-color,#6b7280);margin-top:8px">Caption text</p>` },
|
|
123
|
+
{ key: 'HEATMAP', title: 'Heatmap', 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>` },
|
|
96
124
|
{ key: 'RANK_drag', title: 'Rank Drag', 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>` },
|
|
97
|
-
{ key: 'RANK_dropdown', title: 'Rank Dropdown', body: `<div
|
|
125
|
+
{ key: 'RANK_dropdown', title: 'Rank Dropdown', body: `<div class="cfm-rank-item"><span>Item A</span><select class="cfm-input cfm-rank-select" style="width:80px;margin-left:auto"><option>1</option><option>2</option></select></div>` },
|
|
98
126
|
];
|
|
99
127
|
|
|
100
128
|
function buildPreviewHtml(def, inlineCss, inlineJs) {
|
|
@@ -113,4 +141,4 @@ function buildPreviewHtml(def, inlineCss, inlineJs) {
|
|
|
113
141
|
</html>`;
|
|
114
142
|
}
|
|
115
143
|
|
|
116
|
-
module.exports = { PREVIEW_PAGE_DEFS, buildPreviewHtml, sharedStyles, PREVIEW_KEYS: PREVIEW_PAGE_DEFS.map((d) => d.key) };
|
|
144
|
+
module.exports = { PREVIEW_PAGE_DEFS, buildPreviewHtml, sharedStyles, PREVIEW_KEYS: PREVIEW_PAGE_DEFS.map((d) => d.key), TRAFFIC };
|
|
@@ -42,6 +42,7 @@ function flushCssVars(): void {
|
|
|
42
42
|
}
|
|
43
43
|
delete pendingVars[key];
|
|
44
44
|
}
|
|
45
|
+
applyThemeSideEffects();
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
function queueCssVars(vars: Record<string, string>): void {
|
|
@@ -56,6 +57,7 @@ function applyCssVarsImmediate(vars: Record<string, string>): void {
|
|
|
56
57
|
for (const [key, value] of Object.entries(vars)) {
|
|
57
58
|
root.style.setProperty(key, value);
|
|
58
59
|
}
|
|
60
|
+
applyThemeSideEffects();
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
function setDisplay(selector: string, visible: boolean): void {
|
|
@@ -217,6 +219,37 @@ function applyContentPatch(patch: ContentPatch): void {
|
|
|
217
219
|
(el as HTMLElement).textContent = hintMax;
|
|
218
220
|
});
|
|
219
221
|
}
|
|
222
|
+
if (patch.previewState?.multiStatement !== undefined) {
|
|
223
|
+
const show = Boolean(patch.previewState.multiStatement);
|
|
224
|
+
document.querySelectorAll('[data-cfm-matrix-row-extra]').forEach((el) => {
|
|
225
|
+
(el as HTMLElement).style.display = show ? '' : 'none';
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
applyThemeSideEffects();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const TRAFFIC_BADGE = ['#ef4444', '#f97316', '#eab308', '#84cc16', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7'];
|
|
232
|
+
|
|
233
|
+
function readCssVar(name: string, fallback: string): string {
|
|
234
|
+
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function applyThemeSideEffects(): void {
|
|
238
|
+
const mode = readCssVar('--cfm-number-label-mode', 'traffic');
|
|
239
|
+
const mono = readCssVar('--cfm-number-mono-color', '#9ca3af');
|
|
240
|
+
document.querySelectorAll('[data-cfm-number-badge]').forEach((el) => {
|
|
241
|
+
if (el.classList.contains('selected')) return;
|
|
242
|
+
const i = parseInt(el.getAttribute('data-cfm-number-badge') ?? '', 10);
|
|
243
|
+
if (Number.isNaN(i)) return;
|
|
244
|
+
let bg = TRAFFIC_BADGE[i] ?? '#9ca3af';
|
|
245
|
+
if (mode === 'monochrome') bg = mono;
|
|
246
|
+
if (mode === 'individual') bg = readCssVar(`--cfm-number-color-${i}`, '#9ca3af');
|
|
247
|
+
(el as HTMLElement).style.backgroundColor = bg;
|
|
248
|
+
});
|
|
249
|
+
const dropStyle = readCssVar('--cfm-upload-border-style', 'dashed');
|
|
250
|
+
document.querySelectorAll('[data-cfm-dropzone], .cfm-dropzone').forEach((el) => {
|
|
251
|
+
(el as HTMLElement).style.borderStyle = dropStyle;
|
|
252
|
+
});
|
|
220
253
|
}
|
|
221
254
|
|
|
222
255
|
function setLiveEditMode(on: boolean): void {
|