@explorer02/cfm-survey-sdk 0.1.4 → 0.1.6
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/postinstall.js +30 -18
- package/templates/AGENT.md +10 -2341
- package/templates/docs/01-sdk-core/01-fetch-survey.md +68 -0
- package/templates/docs/01-sdk-core/02-survey-mapper.md +85 -0
- package/templates/docs/01-sdk-core/03-question-mappers.md +114 -0
- package/templates/docs/01-sdk-core/04-pagination.md +72 -0
- package/templates/docs/01-sdk-core/05-validation.md +66 -0
- package/templates/docs/01-sdk-core/06-submit-survey.md +90 -0
- package/templates/docs/01-sdk-core/07-language-handling.md +111 -0
- package/templates/docs/01-sdk-core/08-icons-and-emojis.md +56 -0
- package/templates/docs/01-sdk-core/README.md +53 -0
- package/templates/docs/02-question-types/01-rating.md +52 -0
- package/templates/docs/02-question-types/02-radio.md +26 -0
- package/templates/docs/02-question-types/03-text.md +26 -0
- package/templates/docs/02-question-types/04-csat.md +54 -0
- package/templates/docs/02-question-types/05-rating-scale.md +26 -0
- package/templates/docs/02-question-types/06-slider.md +30 -0
- package/templates/docs/02-question-types/07-matrix-cfm.md +43 -0
- package/templates/docs/02-question-types/08-matrix-csat.md +29 -0
- package/templates/docs/02-question-types/09-matrix-rating.md +28 -0
- package/templates/docs/02-question-types/10-slider-matrix.md +40 -0
- package/templates/docs/02-question-types/11-file-upload.md +34 -0
- package/templates/docs/02-question-types/12-text-and-media.md +35 -0
- package/templates/docs/02-question-types/README.md +74 -0
- package/templates/docs/03-client-components/01-survey-page.md +113 -0
- package/templates/docs/03-client-components/02-question.md +74 -0
- package/templates/docs/03-client-components/03-rating-scale.md +91 -0
- package/templates/docs/03-client-components/04-csat-scale.md +79 -0
- package/templates/docs/03-client-components/05-csat-matrix-scale.md +77 -0
- package/templates/docs/03-client-components/06-likert-matrix-scale.md +43 -0
- package/templates/docs/03-client-components/07-slider-matrix-scale.md +93 -0
- package/templates/docs/03-client-components/08-file-upload-scale.md +87 -0
- package/templates/docs/03-client-components/09-custom-slider-track.md +72 -0
- package/templates/docs/03-client-components/10-header-footer.md +46 -0
- package/templates/docs/03-client-components/11-progress-bar.md +31 -0
- package/templates/docs/03-client-components/12-language-selector.md +41 -0
- package/templates/docs/03-client-components/13-matrix-dropdown.md +42 -0
- package/templates/docs/03-client-components/README.md +63 -0
- package/templates/docs/04-critical-rules/01-import-rules.md +51 -0
- package/templates/docs/04-critical-rules/02-action-dispatching.md +56 -0
- package/templates/docs/04-critical-rules/03-scroll-navigation.md +37 -0
- package/templates/docs/04-critical-rules/04-logo-branding.md +42 -0
- package/templates/docs/04-critical-rules/05-troubleshooting.md +46 -0
- package/templates/docs/04-critical-rules/README.md +29 -0
- package/templates/docs/index.md +129 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Question Dispatcher
|
|
2
|
+
|
|
3
|
+
> **Source**: `apps/client/src/components/Question.tsx`
|
|
4
|
+
> **Role**: Wrapper card component that routes to the correct scale component based on question type.
|
|
5
|
+
|
|
6
|
+
## Props Interface
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
type QuestionProps = {
|
|
10
|
+
question: SurveyQuestion;
|
|
11
|
+
selectedValue?: AnswerValue;
|
|
12
|
+
validationError?: string;
|
|
13
|
+
onSelect: (value: AnswerValue) => void;
|
|
14
|
+
};
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Structure & Styling Rules (CRITICAL)
|
|
18
|
+
|
|
19
|
+
Every question is wrapped in a standard HTML section. It **MUST** have a border around the entire question block to separate them visually, and it **MUST** handle rich text (HTML) provided by the SDK safely.
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
<section
|
|
23
|
+
id={question.id}
|
|
24
|
+
className="space-y-6 rounded-xl border border-gray-200 bg-white p-6 md:p-8 shadow-sm transition-all hover:shadow-md"
|
|
25
|
+
>
|
|
26
|
+
{/* 1. Question Text (Rich Text with required asterisk) */}
|
|
27
|
+
<h2 className="text-lg md:text-xl font-semibold leading-relaxed text-gray-900 flex items-start gap-1">
|
|
28
|
+
{/* dangerouslySetInnerHTML handles strong, em, and link tags from the SDK */}
|
|
29
|
+
<span dangerouslySetInnerHTML={{ __html: question.text }} className="prose prose-sm max-w-none" />
|
|
30
|
+
{question.required && <span className="text-[#e20074] shrink-0">*</span>}
|
|
31
|
+
</h2>
|
|
32
|
+
|
|
33
|
+
{/* 2. Question Description (Rich Text) */}
|
|
34
|
+
{question.description && (
|
|
35
|
+
<div
|
|
36
|
+
className="text-sm text-gray-600 mt-2 leading-relaxed prose prose-sm max-w-none"
|
|
37
|
+
dangerouslySetInnerHTML={{ __html: question.description }}
|
|
38
|
+
/>
|
|
39
|
+
)}
|
|
40
|
+
|
|
41
|
+
{/* 3. The Scale Component (Dispatcher switch statement) */}
|
|
42
|
+
<div className="pt-4">
|
|
43
|
+
{question.type === 'rating' && <RatingScale ... />}
|
|
44
|
+
{/* ... other types ... */}
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
{/* 4. Validation Error Banner */}
|
|
48
|
+
{validationError && (
|
|
49
|
+
<div className="mt-6 rounded-lg border-l-4 border-l-[#e20074] bg-red-50 p-4">
|
|
50
|
+
<p className="text-sm font-bold text-gray-900 leading-tight flex items-center gap-2">
|
|
51
|
+
<span className="text-[#e20074]">⚠</span> {validationError}
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
</section>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**⚠️ CRITICAL RULES**:
|
|
59
|
+
1. **DOM ID**: The wrapper element **MUST** have `id={question.id}` for the scroll-to-error validation to work!
|
|
60
|
+
2. **Borders**: The wrapper **MUST** have `border border-gray-200 rounded-xl`.
|
|
61
|
+
3. **Rich Text**: `question.text` and `question.description` MUST be rendered using `dangerouslySetInnerHTML` because the SDK sends formatted HTML. Do not render them as raw strings.
|
|
62
|
+
4. **Spacing**: Use `space-y-6` and `p-6 md:p-8` for generous internal padding.
|
|
63
|
+
|
|
64
|
+
## Inline vs Dedicated Components
|
|
65
|
+
|
|
66
|
+
While complex scales (like Matrix, Slider, FileUpload) get their own dedicated files (e.g. `CsatMatrixScale.tsx`), some simpler types are handled directly inline within `Question.tsx` to reduce boilerplate:
|
|
67
|
+
|
|
68
|
+
1. **`radio` (MCQ)**:
|
|
69
|
+
- Each option is a card: `border rounded-lg p-4 flex items-center gap-3 cursor-pointer transition-colors`.
|
|
70
|
+
- Hover state: `hover:border-[#e20074] hover:bg-pink-50`.
|
|
71
|
+
- Selected state: `border-[#e20074] bg-pink-50`.
|
|
72
|
+
- Custom radio dot: `<div className={`h-5 w-5 rounded-full border-2 ${isSelected ? 'border-[#e20074]' : 'border-gray-300'} flex items-center justify-center`}><div className={`h-2.5 w-2.5 rounded-full bg-[#e20074] ${isSelected ? 'block' : 'hidden'}`} /></div>`
|
|
73
|
+
2. **`text`**: Inline `<textarea>` or `<input type="text">` with `border-gray-300 focus:border-[#e20074] focus:ring-[#e20074] rounded-lg w-full`.
|
|
74
|
+
3. **`text_and_media`**: Inline `<video>` or `<img>` tag with `rounded-lg`.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Rating Scale Component
|
|
2
|
+
|
|
3
|
+
> **Component**: `RatingScale`
|
|
4
|
+
> **Handles**: `type: 'rating'` | `type: 'rating_scale'`
|
|
5
|
+
|
|
6
|
+
## Props Interface
|
|
7
|
+
```typescript
|
|
8
|
+
type RatingScaleProps = {
|
|
9
|
+
questionId: string;
|
|
10
|
+
options: SurveyOption[];
|
|
11
|
+
selectedValue?: string | number | null | any;
|
|
12
|
+
onSelect: (value: number) => void;
|
|
13
|
+
// Extracted from question object:
|
|
14
|
+
minLabel?: string;
|
|
15
|
+
midLabel?: string;
|
|
16
|
+
maxLabel?: string;
|
|
17
|
+
midLabelIndex?: number;
|
|
18
|
+
};
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## UI Styling & Interaction Rules (CRITICAL)
|
|
22
|
+
|
|
23
|
+
The UI must exactly match these specifications:
|
|
24
|
+
|
|
25
|
+
### 1. Square Rounded Buttons
|
|
26
|
+
- Do **NOT** use circles for `rating` badges. Use squares with rounded corners.
|
|
27
|
+
- Base classes: `flex h-10 w-10 sm:h-12 sm:w-12 items-center justify-center rounded-lg text-sm sm:text-base font-medium transition-all duration-200 cursor-pointer`.
|
|
28
|
+
|
|
29
|
+
### 2. Colors & Hover States
|
|
30
|
+
- `options` may contain a `color` property (NPS scale). Use it for the background.
|
|
31
|
+
- If no color is provided, use a default gray background.
|
|
32
|
+
- **Unselected State**: `opacity-30` (if any option is selected, dim the others), or just standard opacity with a subtle border.
|
|
33
|
+
- **Hover State**: `hover:scale-105 hover:shadow-md hover:border-[#e20074] hover:border-2`.
|
|
34
|
+
- **Selected State**:
|
|
35
|
+
- Apply `opacity-100`.
|
|
36
|
+
- Apply a strong prominent border: `border-2 border-[#e20074] shadow-md`.
|
|
37
|
+
|
|
38
|
+
### 3. Tooltips
|
|
39
|
+
- Every button **MUST** have `title={option.label}` so users can see the exact semantic meaning (e.g., "Extremely Likely") when hovering over the number '10'.
|
|
40
|
+
|
|
41
|
+
### 4. Label Positioning
|
|
42
|
+
- Place `minLabel`, `midLabel`, and `maxLabel` underneath the button track.
|
|
43
|
+
- Container for labels: `relative mt-3 h-8 w-full text-xs text-gray-500`.
|
|
44
|
+
- Left label (`minLabel`): `absolute left-0`.
|
|
45
|
+
- Right label (`maxLabel`): `absolute right-0`.
|
|
46
|
+
- Middle label (`midLabel`): `absolute transform -translate-x-1/2` with `style={{ left: \`\${(midLabelIndex / (options.length - 1)) * 100}%\` }}`.
|
|
47
|
+
|
|
48
|
+
## Implementation Snippet
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
<div className="w-full">
|
|
52
|
+
<div className="flex w-full justify-between gap-1 sm:gap-2">
|
|
53
|
+
{options.map((option, i) => {
|
|
54
|
+
const isSelected = selectedValue === option.value;
|
|
55
|
+
const isAnySelected = selectedValue !== undefined && selectedValue !== null;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<button
|
|
59
|
+
key={option.id}
|
|
60
|
+
type="button"
|
|
61
|
+
onClick={() => onSelect(option.value as number)}
|
|
62
|
+
title={option.label} // Tooltip required
|
|
63
|
+
className={`
|
|
64
|
+
flex h-10 w-10 sm:h-12 sm:w-12 items-center justify-center rounded-lg font-semibold transition-all
|
|
65
|
+
${isSelected ? 'border-2 border-[#e20074] scale-105 shadow-md opacity-100 z-10' : 'border border-transparent'}
|
|
66
|
+
${!isSelected && isAnySelected ? 'opacity-40' : 'opacity-100'}
|
|
67
|
+
hover:border-[#e20074] hover:scale-105 hover:opacity-100
|
|
68
|
+
`}
|
|
69
|
+
style={{ backgroundColor: option.color || '#f3f4f6', color: '#000000' }}
|
|
70
|
+
>
|
|
71
|
+
{option.value}
|
|
72
|
+
</button>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{/* Label Positioning */}
|
|
78
|
+
<div className="relative mt-3 h-6 w-full text-xs font-medium text-gray-500">
|
|
79
|
+
{minLabel && <span className="absolute left-0">{minLabel}</span>}
|
|
80
|
+
{midLabel && (
|
|
81
|
+
<span
|
|
82
|
+
className="absolute transform -translate-x-1/2 text-center"
|
|
83
|
+
style={{ left: `${(midLabelIndex! / (options.length - 1)) * 100}%` }}
|
|
84
|
+
>
|
|
85
|
+
{midLabel}
|
|
86
|
+
</span>
|
|
87
|
+
)}
|
|
88
|
+
{maxLabel && <span className="absolute right-0 text-right">{maxLabel}</span>}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
```
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# CSAT Scale Component
|
|
2
|
+
|
|
3
|
+
> **Component**: `CsatScale`
|
|
4
|
+
> **Handles**: `type: 'csat'`
|
|
5
|
+
|
|
6
|
+
## Props Interface
|
|
7
|
+
```typescript
|
|
8
|
+
type CsatScaleProps = {
|
|
9
|
+
question: SurveyQuestion & { type: 'csat' };
|
|
10
|
+
selectedValue?: string | number | null;
|
|
11
|
+
onSelect: (value: number | null) => void;
|
|
12
|
+
};
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## UI Styling & Interaction Rules (CRITICAL)
|
|
16
|
+
|
|
17
|
+
The visual rendering heavily depends on `buttonType`. All variants MUST implement strict hover states, tooltips, and distinct active state bounding boxes.
|
|
18
|
+
|
|
19
|
+
### 1. `buttonType === 'emoji'`
|
|
20
|
+
- **Icons**: Use `getEmojiForIndex(index, total)` or map `CsatEmojiMapping`.
|
|
21
|
+
- **Hover State**: `hover:scale-110 transition-transform cursor-pointer`.
|
|
22
|
+
- **Active State Bounding Box**:
|
|
23
|
+
- DO NOT just change the emoji color.
|
|
24
|
+
- The selected emoji MUST be wrapped in a prominent pink square box with rounded corners.
|
|
25
|
+
- Wrapper CSS: `border-2 border-[#e20074] rounded-lg p-2 bg-pink-50 shadow-sm scale-110`.
|
|
26
|
+
- Unselected CSS: `border-2 border-transparent p-2 opacity-50 hover:opacity-100`.
|
|
27
|
+
- **Tooltips**: `title={option.label}` on every button.
|
|
28
|
+
|
|
29
|
+
### 2. `buttonType === 'star'`
|
|
30
|
+
- **Icons**: Use `CsatStarIcons.filled` and `CsatStarIcons.empty`.
|
|
31
|
+
- **Active State Bounding Box**:
|
|
32
|
+
- The *last* selected star (the one matching the `selectedValue`) receives the pink bounding box: `border-2 border-[#e20074] rounded-lg p-2 bg-pink-50 scale-110`.
|
|
33
|
+
- Previous filled stars get `border-transparent p-2 text-[#e20074]`.
|
|
34
|
+
- Empty stars get `opacity-30`.
|
|
35
|
+
|
|
36
|
+
### 3. `buttonType === 'numbered'`
|
|
37
|
+
- Fallback to `RatingScale` style (Square badges).
|
|
38
|
+
|
|
39
|
+
### 4. `buttonType === 'graphics'`
|
|
40
|
+
- Render a single `<CustomSliderTrack>`.
|
|
41
|
+
|
|
42
|
+
## N/A Option Handling
|
|
43
|
+
If `question.hasNotApplicable` is true:
|
|
44
|
+
- Render a distinct button or checkbox for N/A.
|
|
45
|
+
- Selecting it passes `null` to `onSelect`.
|
|
46
|
+
- Style it distinctly from the main scale (e.g., standard text button or checkbox).
|
|
47
|
+
|
|
48
|
+
## Label Positioning
|
|
49
|
+
- Place `minLabel` and `maxLabel` underneath the emoji/star track, just like RatingScale.
|
|
50
|
+
- `relative mt-4 flex w-full justify-between text-xs text-gray-500 font-medium`.
|
|
51
|
+
|
|
52
|
+
## Implementation Snippet (Emoji Variant)
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
<div className="flex w-full justify-between items-center gap-2">
|
|
56
|
+
{options.map((option, i) => {
|
|
57
|
+
const isSelected = selectedValue === option.value;
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<button
|
|
61
|
+
key={option.id}
|
|
62
|
+
onClick={() => onSelect(option.value as number)}
|
|
63
|
+
title={option.label} // Required Tooltip
|
|
64
|
+
className={`
|
|
65
|
+
flex items-center justify-center transition-all duration-200
|
|
66
|
+
${isSelected
|
|
67
|
+
? 'border-2 border-[#e20074] rounded-lg p-1 sm:p-2 bg-pink-50 scale-110 shadow-sm z-10'
|
|
68
|
+
: 'border-2 border-transparent p-1 sm:p-2 opacity-50 hover:opacity-100 hover:scale-105'
|
|
69
|
+
}
|
|
70
|
+
`}
|
|
71
|
+
>
|
|
72
|
+
<div className="w-8 h-8 sm:w-10 sm:h-10">
|
|
73
|
+
{getEmojiForIndex(i, options.length)}
|
|
74
|
+
</div>
|
|
75
|
+
</button>
|
|
76
|
+
);
|
|
77
|
+
})}
|
|
78
|
+
</div>
|
|
79
|
+
```
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# CSAT Matrix Scale Component
|
|
2
|
+
|
|
3
|
+
> **Component**: `CsatMatrixScale`
|
|
4
|
+
> **Handles**: `type: 'matrix'` (CSAT_MATRIX subType) | `type: 'rating_matrix'`
|
|
5
|
+
|
|
6
|
+
## Props Interface
|
|
7
|
+
```typescript
|
|
8
|
+
type CsatMatrixScaleProps = {
|
|
9
|
+
question: SurveyQuestion & { type: 'matrix' | 'rating_matrix' };
|
|
10
|
+
selectedValue: MatrixAnswerMap;
|
|
11
|
+
onSelect: (value: MatrixAnswerMap) => void;
|
|
12
|
+
};
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## UI Structure & Hover Rules (CRITICAL)
|
|
16
|
+
|
|
17
|
+
The matrix must be rendered as an HTML `<table>` with strict hover tracking and active selection bounding boxes.
|
|
18
|
+
|
|
19
|
+
### 1. Table Layout
|
|
20
|
+
- `w-full text-left border-collapse`.
|
|
21
|
+
- **Headers (`<thead>`)**:
|
|
22
|
+
- `th` elements must be `p-4 text-sm font-semibold text-gray-600 text-center`.
|
|
23
|
+
- Leftmost header is empty.
|
|
24
|
+
|
|
25
|
+
### 2. Row Hover Tracking
|
|
26
|
+
- Matrix grids are hard to read. You **MUST** add `hover:bg-gray-50 transition-colors` to every `<tr>` in the `<tbody>`.
|
|
27
|
+
- Cells: `td` elements must be `p-4 border-b border-gray-100 text-center`.
|
|
28
|
+
- Leftmost cell (row text): `text-sm font-medium text-gray-800 text-left w-1/3`.
|
|
29
|
+
|
|
30
|
+
### 3. Active State Bounding Boxes (Emojis & Stars)
|
|
31
|
+
Just like `CsatScale`, when `buttonType === 'emoji'` or `'star'`, you cannot just swap the icon. The selected cell must highlight the selection prominently.
|
|
32
|
+
|
|
33
|
+
- **Unselected Icon Wrapper**: `inline-flex p-1 border-2 border-transparent opacity-40 hover:opacity-100 hover:scale-110 transition-all cursor-pointer`.
|
|
34
|
+
- **Selected Icon Wrapper**: `inline-flex p-1 border-2 border-[#e20074] rounded-md bg-pink-50 scale-110 shadow-sm opacity-100 cursor-pointer`.
|
|
35
|
+
|
|
36
|
+
### 4. Tooltips
|
|
37
|
+
- Every icon in every cell MUST have `title={col.label}` so the user knows what column they are clicking.
|
|
38
|
+
|
|
39
|
+
## Implementation Snippet (Row Rendering)
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
<tbody>
|
|
43
|
+
{question.rows.map((row) => (
|
|
44
|
+
<tr key={row.id} className="hover:bg-gray-50 transition-colors group">
|
|
45
|
+
<td className="p-4 border-b border-gray-100 text-sm font-medium text-gray-800 text-left w-1/3">
|
|
46
|
+
{row.text}
|
|
47
|
+
</td>
|
|
48
|
+
{question.columns.map((col, colIdx) => {
|
|
49
|
+
const isSelected = selectedValue[row.id] === col.value;
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<td key={col.id} className="p-4 border-b border-gray-100 text-center">
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
onClick={() => onSelect({ ...selectedValue, [row.id]: col.value })}
|
|
56
|
+
title={col.label} // Tooltip required
|
|
57
|
+
className={`
|
|
58
|
+
inline-flex items-center justify-center transition-all duration-200
|
|
59
|
+
${isSelected
|
|
60
|
+
? 'border-2 border-[#e20074] rounded-md p-1.5 bg-pink-50 scale-110 shadow-sm'
|
|
61
|
+
: 'border-2 border-transparent p-1.5 opacity-40 group-hover:opacity-70 hover:opacity-100 hover:scale-110'
|
|
62
|
+
}
|
|
63
|
+
`}
|
|
64
|
+
>
|
|
65
|
+
<div className="w-6 h-6 sm:w-8 sm:h-8">
|
|
66
|
+
{/* Render icon based on buttonType */}
|
|
67
|
+
{question.buttonType === 'emoji' && getEmojiForIndex(colIdx, question.columns.length)}
|
|
68
|
+
{question.buttonType === 'star' && (isSelected ? CsatStarIcons.filled : CsatStarIcons.empty)}
|
|
69
|
+
</div>
|
|
70
|
+
</button>
|
|
71
|
+
</td>
|
|
72
|
+
);
|
|
73
|
+
})}
|
|
74
|
+
</tr>
|
|
75
|
+
))}
|
|
76
|
+
</tbody>
|
|
77
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Likert Matrix Scale Component
|
|
2
|
+
|
|
3
|
+
> **Component**: `LikertMatrixScale`
|
|
4
|
+
> **Handles**: `type: 'matrix'` (CFM_MATRIX subType)
|
|
5
|
+
|
|
6
|
+
## Props Interface
|
|
7
|
+
```typescript
|
|
8
|
+
type LikertMatrixScaleProps = {
|
|
9
|
+
question: SurveyQuestion & { type: 'matrix' };
|
|
10
|
+
selectedValue: MatrixAnswerMap;
|
|
11
|
+
onSelect: (value: MatrixAnswerMap) => void;
|
|
12
|
+
};
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Matrix Configurations (CRITICAL)
|
|
16
|
+
|
|
17
|
+
The SDK provides several boolean flags that alter the grid layout. You MUST implement logic for these:
|
|
18
|
+
|
|
19
|
+
### 1. `question.transposeTable`
|
|
20
|
+
If `true`, swap the axes:
|
|
21
|
+
- The `columns` (labels like "Strongly Agree") become the left-hand row headers.
|
|
22
|
+
- The `rows` (statements) become the top column headers.
|
|
23
|
+
- Rendering logic must map over columns first, then rows.
|
|
24
|
+
|
|
25
|
+
### 2. `question.repeatScale`
|
|
26
|
+
If `true`, the `<thead>` (the column labels) should be injected periodically in the `<tbody>`.
|
|
27
|
+
- Example: `if (rowIndex > 0 && rowIndex % 5 === 0) return <RepeatedHeaderRow />`
|
|
28
|
+
- This helps users maintain context on very long grids.
|
|
29
|
+
|
|
30
|
+
### 3. `question.matrixType === 'bipolar'`
|
|
31
|
+
- The first cell contains `row.text`.
|
|
32
|
+
- The last cell contains `row.rightText`.
|
|
33
|
+
- The radio buttons span the cells in between.
|
|
34
|
+
|
|
35
|
+
## UI Styling Rules
|
|
36
|
+
|
|
37
|
+
- **Row Hovers**: Every `<tr>` must have `hover:bg-gray-50 transition-colors`.
|
|
38
|
+
- **Radio Buttons**:
|
|
39
|
+
- Do NOT use plain browser radios if possible. Use accent-colored styling:
|
|
40
|
+
`accent-[#e20074] w-5 h-5 cursor-pointer`.
|
|
41
|
+
- Alternatively, build custom CSS circles that fill with pink when selected.
|
|
42
|
+
- **Labels**: Header labels (`th`) should be `text-xs font-semibold text-gray-500 uppercase tracking-wider text-center`.
|
|
43
|
+
- **Tooltips**: Native `title` on every radio button mapping to the column label.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Slider Matrix Scale Component
|
|
2
|
+
|
|
3
|
+
> **Component**: `SliderMatrixScale`
|
|
4
|
+
> **Handles**: `type: 'slider_matrix'`
|
|
5
|
+
|
|
6
|
+
## Props Interface
|
|
7
|
+
```typescript
|
|
8
|
+
type SliderMatrixScaleProps = {
|
|
9
|
+
question: SurveyQuestion & { type: 'slider_matrix' };
|
|
10
|
+
selectedValue: MatrixAnswerMap;
|
|
11
|
+
onSelect: (value: MatrixAnswerMap) => void;
|
|
12
|
+
};
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## UI Styling Rules (CRITICAL)
|
|
16
|
+
|
|
17
|
+
Sliders must look substantial and modern. Native thin browser sliders are unacceptable.
|
|
18
|
+
|
|
19
|
+
### 1. Thick Track Styling
|
|
20
|
+
The slider track must be visually prominent:
|
|
21
|
+
- Height: `h-2` or `h-3`.
|
|
22
|
+
- Rounded corners: `rounded-full`.
|
|
23
|
+
- **Dynamic Fill**: The track up to the thumb MUST be pink (`bg-[#e20074]`). The track after the thumb MUST be light gray (`bg-gray-200`).
|
|
24
|
+
- You can achieve this using CSS linear-gradients tied to the value percentage, or by using a custom overlay div.
|
|
25
|
+
|
|
26
|
+
### 2. Thumb Styling
|
|
27
|
+
- Solid pink circle: `w-5 h-5 rounded-full bg-[#e20074] shadow-md border-2 border-white`.
|
|
28
|
+
- Hover state: `hover:scale-125 transition-transform`.
|
|
29
|
+
|
|
30
|
+
### 3. Value Display
|
|
31
|
+
- The exact selected value (e.g., `7`) **MUST** be displayed visibly.
|
|
32
|
+
- Typical layout: Place it at the far right of the slider track in a small bold badge.
|
|
33
|
+
|
|
34
|
+
### 4. Ticks and Labels
|
|
35
|
+
- `row.ticks`: If provided (e.g., `10`), render tiny tick marks beneath the track at intervals.
|
|
36
|
+
- `row.minLabel` & `row.maxLabel`: Render underneath the far left and far right of the track.
|
|
37
|
+
|
|
38
|
+
## Implementation Snippet (Custom Slider Row)
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
{question.rows.map(row => {
|
|
42
|
+
const val = selectedValue[row.id] ?? row.min;
|
|
43
|
+
const percentage = ((val - row.min) / (row.max - row.min)) * 100;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div key={row.id} className="flex flex-col gap-4 py-6 border-b border-gray-100">
|
|
47
|
+
<p className="text-sm font-medium text-gray-800">{row.text}</p>
|
|
48
|
+
|
|
49
|
+
<div className="flex items-center gap-4">
|
|
50
|
+
<div className="relative w-full flex items-center h-6">
|
|
51
|
+
{/* Track Background */}
|
|
52
|
+
<div className="absolute w-full h-2 bg-gray-200 rounded-full" />
|
|
53
|
+
|
|
54
|
+
{/* Track Fill (Pink) */}
|
|
55
|
+
<div
|
|
56
|
+
className="absolute h-2 bg-[#e20074] rounded-l-full"
|
|
57
|
+
style={{ width: `${percentage}%` }}
|
|
58
|
+
/>
|
|
59
|
+
|
|
60
|
+
{/* Native Input (Invisible overlay for mechanics) */}
|
|
61
|
+
<input
|
|
62
|
+
type="range"
|
|
63
|
+
min={row.min}
|
|
64
|
+
max={row.max}
|
|
65
|
+
step={row.step}
|
|
66
|
+
value={val}
|
|
67
|
+
onChange={e => onSelect({ ...selectedValue, [row.id]: Number(e.target.value) })}
|
|
68
|
+
className="absolute w-full h-full opacity-0 cursor-pointer z-10"
|
|
69
|
+
title={`Value: ${val}`}
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
{/* Visual Thumb */}
|
|
73
|
+
<div
|
|
74
|
+
className="absolute w-5 h-5 bg-[#e20074] border-2 border-white rounded-full shadow-md pointer-events-none transition-transform"
|
|
75
|
+
style={{ left: `calc(${percentage}% - 10px)` }}
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
{/* Value Display Badge */}
|
|
80
|
+
<div className="shrink-0 w-8 text-center font-bold text-[#e20074]">
|
|
81
|
+
{val}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Min/Max Labels */}
|
|
86
|
+
<div className="flex justify-between text-xs text-gray-500 font-medium px-1">
|
|
87
|
+
<span>{row.minLabel}</span>
|
|
88
|
+
<span>{row.maxLabel}</span>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
93
|
+
```
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# File Upload Scale Component
|
|
2
|
+
|
|
3
|
+
> **Component**: `FileUploadScale`
|
|
4
|
+
> **Handles**: `type: 'file_upload'`
|
|
5
|
+
|
|
6
|
+
## Props Interface
|
|
7
|
+
```typescript
|
|
8
|
+
type FileUploadScaleProps = {
|
|
9
|
+
question: SurveyQuestion & { type: 'file_upload' };
|
|
10
|
+
selectedValue?: any[]; // Expected to be UploadedFile[]
|
|
11
|
+
onSelect: (value: any[]) => void;
|
|
12
|
+
};
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## UI Styling Rules (CRITICAL)
|
|
16
|
+
|
|
17
|
+
The file upload component must be highly polished, featuring a drag-and-drop zone and a discrete list of uploaded files.
|
|
18
|
+
|
|
19
|
+
### 1. Dropzone Area
|
|
20
|
+
- **Border**: Thick dashed border. `border-2 border-dashed border-gray-300 rounded-xl`.
|
|
21
|
+
- **Background & Spacing**: `bg-gray-50 p-8 text-center cursor-pointer transition-colors`.
|
|
22
|
+
- **Hover State**: `hover:border-[#e20074] hover:bg-pink-50 hover:shadow-sm`.
|
|
23
|
+
- **Content**:
|
|
24
|
+
- Render a large prominent cloud upload icon (e.g., from `react-icons`).
|
|
25
|
+
- Render the instruction text (`question.uploadMessage` or "Click or drag files here to upload").
|
|
26
|
+
- Render limits text below it in smaller font: `Max files: ${maxFileCount} | Limit: ${fileSizeLimit}MB | Formats: ${supportedFileFormats.join(', ')}`.
|
|
27
|
+
|
|
28
|
+
### 2. Uploaded Files List
|
|
29
|
+
When files are selected (`selectedValue.length > 0`), render them as a list of distinct cards below the dropzone:
|
|
30
|
+
- **Card**: `flex items-center justify-between p-3 mt-3 border border-gray-200 rounded-lg bg-white shadow-sm`.
|
|
31
|
+
- **File Info**: Display a small file icon, the file name (`truncate` class if long), and the file size.
|
|
32
|
+
- **Remove Button**: A discrete trash can icon button. `text-red-500 hover:bg-red-50 rounded-md p-2 transition-colors`.
|
|
33
|
+
|
|
34
|
+
## Implementation Snippet
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
<div className="space-y-4">
|
|
38
|
+
{/* Hidden Input */}
|
|
39
|
+
<input
|
|
40
|
+
type="file"
|
|
41
|
+
ref={fileInputRef}
|
|
42
|
+
className="hidden"
|
|
43
|
+
multiple={question.maxFileCount > 1}
|
|
44
|
+
accept={question.supportedFileFormats?.map(f => `.${f.toLowerCase()}`).join(',')}
|
|
45
|
+
onChange={handleFileChange}
|
|
46
|
+
/>
|
|
47
|
+
|
|
48
|
+
{/* Dropzone */}
|
|
49
|
+
<div
|
|
50
|
+
onClick={() => fileInputRef.current?.click()}
|
|
51
|
+
className="border-2 border-dashed border-gray-300 rounded-xl bg-gray-50 hover:bg-pink-50 hover:border-[#e20074] transition-colors p-8 text-center cursor-pointer flex flex-col items-center justify-center gap-2 group"
|
|
52
|
+
>
|
|
53
|
+
<div className="w-12 h-12 rounded-full bg-white flex items-center justify-center text-gray-400 group-hover:text-[#e20074] shadow-sm mb-2">
|
|
54
|
+
{/* Insert SVG Cloud Icon Here */}
|
|
55
|
+
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /></svg>
|
|
56
|
+
</div>
|
|
57
|
+
<p className="text-sm font-semibold text-gray-700 group-hover:text-[#e20074]">
|
|
58
|
+
{question.uploadMessage || "Click or drag files here to upload"}
|
|
59
|
+
</p>
|
|
60
|
+
<p className="text-xs text-gray-500">
|
|
61
|
+
Supported formats: {question.supportedFileFormats?.join(', ')} (Max {question.fileSizeLimit}MB)
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{/* Uploaded Files List */}
|
|
66
|
+
{selectedValue && selectedValue.length > 0 && (
|
|
67
|
+
<ul className="space-y-2 mt-4">
|
|
68
|
+
{selectedValue.map((file, i) => (
|
|
69
|
+
<li key={i} className="flex items-center justify-between p-3 border border-gray-200 rounded-lg bg-white shadow-sm">
|
|
70
|
+
<div className="flex items-center gap-3 overflow-hidden">
|
|
71
|
+
<span className="text-gray-400">📄</span>
|
|
72
|
+
<span className="text-sm font-medium text-gray-700 truncate">{file.name}</span>
|
|
73
|
+
{file.size && <span className="text-xs text-gray-400">({(file.size / 1024 / 1024).toFixed(2)} MB)</span>}
|
|
74
|
+
</div>
|
|
75
|
+
<button
|
|
76
|
+
onClick={() => onSelect(selectedValue.filter((_, index) => index !== i))}
|
|
77
|
+
className="text-red-500 hover:bg-red-50 p-2 rounded-md transition-colors"
|
|
78
|
+
title="Remove file"
|
|
79
|
+
>
|
|
80
|
+
🗑️
|
|
81
|
+
</button>
|
|
82
|
+
</li>
|
|
83
|
+
))}
|
|
84
|
+
</ul>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
```
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Custom Slider Track
|
|
2
|
+
|
|
3
|
+
> **Component**: `CustomSliderTrack`
|
|
4
|
+
> **Used by**: `SliderMatrixScale`, `CsatMatrixScale`, `CsatScale`
|
|
5
|
+
|
|
6
|
+
## Role
|
|
7
|
+
A shared presentation component that overrides native browser range slider styling to provide an emoji-thumb or a standard branded thick track.
|
|
8
|
+
|
|
9
|
+
## Custom Graphics Variant (`sliderType === 'graphics'`)
|
|
10
|
+
If the API specifies `graphics`, the slider thumb **MUST** dynamically display an emoji corresponding to the current value.
|
|
11
|
+
- The thumb must be large enough to hold an emoji (`w-8 h-8` or `w-10 h-10`).
|
|
12
|
+
- Use `getEmojiForIndex` from the SDK to map the current percentage/value to the correct sentiment face.
|
|
13
|
+
- The thumb must have a shadow and white background to pop out from the track: `bg-white rounded-full shadow-md border border-gray-200 flex items-center justify-center text-xl`.
|
|
14
|
+
|
|
15
|
+
## Track CSS Construction
|
|
16
|
+
Native `<input type="range">` cannot reliably render an emoji inside the draggable thumb handle cross-browser.
|
|
17
|
+
|
|
18
|
+
You MUST use the "invisible overlay" trick:
|
|
19
|
+
1. Render a visual background track `div` (the gray bar).
|
|
20
|
+
2. Render a visual fill track `div` (the pink bar, width bound to `percentage`).
|
|
21
|
+
3. Render a visual thumb `div` (the pink circle or the emoji, left bound to `percentage`).
|
|
22
|
+
4. **Overlay** a completely invisible native `<input type="range" className="absolute opacity-0 w-full h-full cursor-pointer z-10" />`.
|
|
23
|
+
|
|
24
|
+
## Implementation Snippet
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
export default function CustomSliderTrack({ min, max, step, value, onChange, sliderType }) {
|
|
28
|
+
const percentage = ((value - min) / (max - min)) * 100;
|
|
29
|
+
|
|
30
|
+
// Determine emoji if graphics variant
|
|
31
|
+
const emoji = sliderType === 'graphics'
|
|
32
|
+
? getEmojiForIndex(value - min, (max - min) / step + 1)
|
|
33
|
+
: null;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="relative w-full flex items-center h-8">
|
|
37
|
+
{/* Background Track */}
|
|
38
|
+
<div className="absolute w-full h-2 bg-gray-200 rounded-full" />
|
|
39
|
+
|
|
40
|
+
{/* Filled Track */}
|
|
41
|
+
<div
|
|
42
|
+
className="absolute h-2 bg-[#e20074] rounded-l-full"
|
|
43
|
+
style={{ width: `${percentage}%` }}
|
|
44
|
+
/>
|
|
45
|
+
|
|
46
|
+
{/* Invisible Native Input */}
|
|
47
|
+
<input
|
|
48
|
+
type="range"
|
|
49
|
+
min={min}
|
|
50
|
+
max={max}
|
|
51
|
+
step={step}
|
|
52
|
+
value={value}
|
|
53
|
+
onChange={e => onChange(Number(e.target.value))}
|
|
54
|
+
className="absolute w-full h-full opacity-0 cursor-pointer z-10"
|
|
55
|
+
title={`Value: ${value}`}
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
{/* Custom Thumb */}
|
|
59
|
+
<div
|
|
60
|
+
className={`absolute pointer-events-none transition-transform flex items-center justify-center ${
|
|
61
|
+
sliderType === 'graphics'
|
|
62
|
+
? 'w-10 h-10 bg-white rounded-full shadow-md border border-gray-200 text-2xl -ml-5'
|
|
63
|
+
: 'w-5 h-5 bg-[#e20074] border-2 border-white rounded-full shadow-md -ml-2.5 hover:scale-125'
|
|
64
|
+
}`}
|
|
65
|
+
style={{ left: `${percentage}%` }}
|
|
66
|
+
>
|
|
67
|
+
{sliderType === 'graphics' && emoji}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Header, Footer & Branding
|
|
2
|
+
|
|
3
|
+
> **Components**: `Header`, `Footer`
|
|
4
|
+
> **Role**: Branding shells around the survey content.
|
|
5
|
+
|
|
6
|
+
## Strict Logo Requirement
|
|
7
|
+
|
|
8
|
+
**⚠️ CRITICAL RULE**: You must NOT use `<img>` tags pointing to external Telekom or Sprinklr URLs. You must NOT download static images into the `public/` folder.
|
|
9
|
+
|
|
10
|
+
All logos must be constructed **purely using Tailwind CSS** geometric shapes.
|
|
11
|
+
|
|
12
|
+
## The Header (Abstract Brand)
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
export default function Header() {
|
|
16
|
+
return (
|
|
17
|
+
<header className="flex h-16 items-center justify-between border-b px-6">
|
|
18
|
+
{/* CSS-only Abstract Logo */}
|
|
19
|
+
<div className="flex items-center gap-2">
|
|
20
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-[#e20074]">
|
|
21
|
+
<div className="h-4 w-4 rounded-full bg-white" />
|
|
22
|
+
</div>
|
|
23
|
+
<span className="text-xl font-bold tracking-tight text-gray-900">
|
|
24
|
+
YourBrand
|
|
25
|
+
</span>
|
|
26
|
+
</div>
|
|
27
|
+
</header>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## The Footer
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
export default function Footer() {
|
|
36
|
+
return (
|
|
37
|
+
<footer className="border-t py-6 text-center text-sm text-gray-500">
|
|
38
|
+
<p>Powered by Sprinklr CFM SDK</p>
|
|
39
|
+
</footer>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Customization
|
|
45
|
+
|
|
46
|
+
The client may provide a prompt asking for specific colors ("make it dark mode", "use blue"). Update the CSS classes in `SurveyPage`, `Header`, and `Footer` to match their request. Do not try to reverse-engineer their brand logo exactly — stick to clean abstract CSS geometry unless you have an explicit SVG path.
|