@explorer02/cfm-survey-sdk 0.1.6 → 0.1.8
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/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/postinstall.js +18 -4
- package/templates/docs/03-client-components/01-survey-page.md +35 -57
- package/templates/docs/03-client-components/02-question.md +78 -47
- package/templates/docs/03-client-components/03-rating-scale.md +20 -83
- package/templates/docs/03-client-components/04-csat-scale.md +29 -70
- package/templates/docs/03-client-components/05-csat-matrix-scale.md +24 -68
- package/templates/docs/03-client-components/06-likert-matrix-scale.md +41 -40
- package/templates/docs/03-client-components/07-slider-matrix-scale.md +33 -81
- package/templates/docs/03-client-components/08-file-upload-scale.md +53 -84
- package/templates/docs/03-client-components/09-custom-slider-track.md +27 -63
- package/templates/docs/03-client-components/10-header-footer.md +18 -41
- package/templates/docs/index.md +31 -9
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
# Survey Page Orchestrator
|
|
1
|
+
# Survey Page Orchestrator Architectural Blueprint
|
|
2
2
|
|
|
3
|
-
> **
|
|
4
|
-
> **Role**: The main entry point that wires the UI to the `useSurveySDK` hook.
|
|
3
|
+
> **Target Component**: `SurveyPage`
|
|
4
|
+
> **Role**: The main entry point that wires the UI to the `useSurveySDK` hook, manages loading/error states, orchestrates pagination, and controls submission.
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## Core Responsibility
|
|
7
|
+
This component MUST act as the state machine for the entire survey flow. It should never render questions directly—it delegates that to `Question.tsx`.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
## Setup & Wiring (CRITICAL)
|
|
10
|
+
|
|
11
|
+
The component MUST invoke `useSurveySDK`, passing the **instanceId** from the client prompt.
|
|
9
12
|
|
|
10
13
|
```tsx
|
|
11
14
|
const SURVEY_PLACEHOLDERS = { FIRST_NAME: 'Customer' }; // Must be outside component!
|
|
@@ -24,52 +27,40 @@ export default function SurveyPage() {
|
|
|
24
27
|
|
|
25
28
|
const survey = surveyQueryResults.data;
|
|
26
29
|
|
|
27
|
-
// ...
|
|
30
|
+
// ... state machine rendering
|
|
28
31
|
}
|
|
29
32
|
```
|
|
30
33
|
|
|
31
|
-
## The 5
|
|
34
|
+
## The 5 State Machine Renders (MANDATORY)
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
You MUST structure your render logic to strictly handle these 5 states in order to avoid UI crashes when data is not yet available.
|
|
34
37
|
|
|
35
38
|
### 1. Loading State
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return <LoadingView /> // Header, Footer, and a "Loading..." message
|
|
39
|
-
}
|
|
40
|
-
```
|
|
39
|
+
If `surveyQueryResults.isLoading` is true, render a Skeleton or Spinner.
|
|
40
|
+
*Include your CSS-only Header and Footer around the spinner.*
|
|
41
41
|
|
|
42
42
|
### 2. Error State
|
|
43
|
-
|
|
44
|
-
if (surveyQueryResults.error) {
|
|
45
|
-
return <ErrorView error={surveyQueryResults.error.message} />
|
|
46
|
-
}
|
|
47
|
-
```
|
|
43
|
+
If `surveyQueryResults.error` is true, render a red Error banner containing `surveyQueryResults.error.message`.
|
|
48
44
|
|
|
49
45
|
### 3. Empty State
|
|
50
|
-
|
|
51
|
-
if (!survey) {
|
|
52
|
-
return <EmptyView /> // Header, Footer, and a "No survey data" message
|
|
53
|
-
}
|
|
54
|
-
```
|
|
46
|
+
If `!survey`, render a generic "Survey not found" message.
|
|
55
47
|
|
|
56
48
|
### 4. Submitted State
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return <SubmitSummaryView /> // Header, Footer, and a "Thank you" message
|
|
60
|
-
}
|
|
61
|
-
```
|
|
49
|
+
If `submitSurveyResults.data` exists, the survey is complete.
|
|
50
|
+
*Render the Thank You Screen (e.g., "Thank you for your feedback!"). Do not render the question list.*
|
|
62
51
|
|
|
63
52
|
### 5. Active Survey State
|
|
64
|
-
|
|
65
|
-
1. `Header`
|
|
66
|
-
2. `ProgressBar` (
|
|
67
|
-
3. `LanguageSelector` (
|
|
68
|
-
4. The
|
|
69
|
-
5.
|
|
53
|
+
If the above 4 conditions are met, render the active survey page:
|
|
54
|
+
1. `Header` (Logo)
|
|
55
|
+
2. `ProgressBar` (Pass `state.progressPercentage`)
|
|
56
|
+
3. `LanguageSelector` (Pass `survey.languages`, `survey.language`, `setSelectedLanguage`)
|
|
57
|
+
4. **The Question Map** (See below)
|
|
58
|
+
5. **Navigation Buttons** (See below)
|
|
70
59
|
6. `Footer`
|
|
71
60
|
|
|
72
|
-
## Question
|
|
61
|
+
## The Question Map
|
|
62
|
+
|
|
63
|
+
You MUST iterate over `state.currentQuestions` and dispatch to `Question.tsx`.
|
|
73
64
|
|
|
74
65
|
```tsx
|
|
75
66
|
<div className="space-y-12">
|
|
@@ -88,26 +79,13 @@ The main render path. Must include:
|
|
|
88
79
|
</div>
|
|
89
80
|
```
|
|
90
81
|
|
|
91
|
-
## Navigation Buttons
|
|
82
|
+
## Navigation Buttons Logic
|
|
92
83
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
{/* Next/Submit Button */}
|
|
102
|
-
<button
|
|
103
|
-
onClick={() => onAction({ type: 'NEXT' })}
|
|
104
|
-
disabled={submitSurveyResults.isLoading}
|
|
105
|
-
>
|
|
106
|
-
{submitSurveyResults.isLoading
|
|
107
|
-
? (survey.language?.startsWith('de') ? 'Wird gesendet...' : 'Submitting...')
|
|
108
|
-
: state.currentPageIndex < survey.pages.length - 1
|
|
109
|
-
? (survey.language?.startsWith('de') ? 'Weiter' : 'Next')
|
|
110
|
-
: (survey.language?.startsWith('de') ? 'Absenden' : 'Submit')
|
|
111
|
-
}
|
|
112
|
-
</button>
|
|
113
|
-
```
|
|
84
|
+
You MUST conditionally render "Back" and "Next/Submit" buttons.
|
|
85
|
+
|
|
86
|
+
- **Back Button**: Hide if `state.currentPageIndex === 0`.
|
|
87
|
+
- `onClick={() => onAction({ type: 'PREVIOUS' })}`
|
|
88
|
+
- **Next/Submit Button**:
|
|
89
|
+
- `disabled={submitSurveyResults.isLoading}`
|
|
90
|
+
- If `state.currentPageIndex < survey.pages.length - 1`, label is "Next", action is `type: 'NEXT'`.
|
|
91
|
+
- If on the last page, label is "Submit", action is `type: 'NEXT'` (the SDK intercepts NEXT on the last page to trigger submission automatically!).
|
|
@@ -1,50 +1,98 @@
|
|
|
1
|
-
# Question Dispatcher
|
|
1
|
+
# Question Dispatcher Architectural Blueprint
|
|
2
2
|
|
|
3
|
-
> **
|
|
4
|
-
> **Role**: Wrapper card component that routes to the correct scale component based on question
|
|
3
|
+
> **Target Component**: `Question`
|
|
4
|
+
> **Role**: Wrapper card component that routes to the correct scale component based on `question.type`.
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## Core Responsibility
|
|
7
|
+
Render a visually separated container (card) for each question. Handle rich text parsing, required asterisks, description text, and error validation messages. Most importantly, it MUST act as an exhaustive switch statement dispatching to ALL supported question types.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
## Exhaustive Dispatcher Logic (CRITICAL)
|
|
10
|
+
|
|
11
|
+
The agent MUST handle EVERY SINGLE ONE of these `question.type` values. Failure to handle them will result in blank UI or broken surveys.
|
|
12
|
+
|
|
13
|
+
1. **`rating`**: Renders `RatingScale`. This covers NPS, Stars, Emoji, and generic numeric scales.
|
|
14
|
+
2. **`csat`**: Renders `CsatScale`. This covers CSAT specific faces/icons.
|
|
15
|
+
3. **`radio`**: Renders inline MCQ radio buttons. (Can be implemented inside this file or separately).
|
|
16
|
+
4. **`text`**: Renders an inline `<textarea>` or `<input>`.
|
|
17
|
+
5. **`matrix`**:
|
|
18
|
+
- If `subType === 'CFM_MATRIX'`, render `LikertMatrixScale`
|
|
19
|
+
- If `subType === 'CSAT_MATRIX'` or `'RATING_MATRIX'`, render `CsatMatrixScale`
|
|
20
|
+
6. **`slider_matrix`**: Renders `SliderMatrixScale`
|
|
21
|
+
7. **`file_upload`**: Renders `FileUploadScale`
|
|
22
|
+
8. **`text_and_media`**: Renders inline `<video>` or `<img>` based on `question.mediaUrl`.
|
|
23
|
+
- *Note: `text_and_media` questions may NOT have user input. Do not render a scale for them.*
|
|
24
|
+
|
|
25
|
+
## Container UI Structure
|
|
26
|
+
|
|
27
|
+
Every question MUST be wrapped in a standard HTML `<section>` to visually separate it.
|
|
28
|
+
|
|
29
|
+
1. **Wrapper styling**:
|
|
30
|
+
- Border: `border border-gray-200`
|
|
31
|
+
- Radius: `rounded-xl`
|
|
32
|
+
- Background: `bg-white`
|
|
33
|
+
- Padding: `p-6 md:p-8`
|
|
34
|
+
- Shadow & Hover: `shadow-sm transition-all hover:shadow-md`
|
|
35
|
+
- **Crucial**: ID must be set to `question.id` (`id={question.id}`) so scroll-to-error works!
|
|
36
|
+
|
|
37
|
+
2. **Heading (`question.text`)**:
|
|
38
|
+
- MUST use `dangerouslySetInnerHTML={{ __html: question.text }}` because text from Sprinklr contains rich HTML (bold, links, italics).
|
|
39
|
+
- If `question.required === true`, append a red asterisk `*`.
|
|
40
|
+
|
|
41
|
+
3. **Description (`question.description`)**:
|
|
42
|
+
- Render conditionally if `question.description` exists.
|
|
43
|
+
- MUST use `dangerouslySetInnerHTML`.
|
|
44
|
+
- Style with slightly smaller text, greyish color (e.g., `text-sm text-gray-600 mt-2`).
|
|
45
|
+
|
|
46
|
+
4. **Validation Error Banner**:
|
|
47
|
+
- If `validationError` prop is passed, render a highly visible banner below the scale.
|
|
48
|
+
- Example: Red border-left, red background, bold text.
|
|
16
49
|
|
|
17
|
-
##
|
|
50
|
+
## Hover Tooltips & Micro-interactions
|
|
51
|
+
- Focus states on text inputs MUST use the brand primary color (e.g., magenta `#e20074` for Telekom).
|
|
52
|
+
- Radio button wrappers should have a subtle background hover effect (e.g., `hover:bg-pink-50`).
|
|
18
53
|
|
|
19
|
-
|
|
54
|
+
## Example Architectural Skeleton
|
|
20
55
|
|
|
21
56
|
```tsx
|
|
22
|
-
<section
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
>
|
|
26
|
-
{/* 1. Question Text (Rich Text with required asterisk) */}
|
|
57
|
+
<section id={question.id} className="space-y-6 rounded-xl border border-gray-200 bg-white p-6 md:p-8 shadow-sm transition-all hover:shadow-md">
|
|
58
|
+
|
|
59
|
+
{/* HEADING */}
|
|
27
60
|
<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
61
|
<span dangerouslySetInnerHTML={{ __html: question.text }} className="prose prose-sm max-w-none" />
|
|
30
62
|
{question.required && <span className="text-[#e20074] shrink-0">*</span>}
|
|
31
63
|
</h2>
|
|
32
64
|
|
|
33
|
-
{/*
|
|
65
|
+
{/* DESCRIPTION */}
|
|
34
66
|
{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
|
-
/>
|
|
67
|
+
<div className="text-sm text-gray-600 mt-2 leading-relaxed prose prose-sm max-w-none" dangerouslySetInnerHTML={{ __html: question.description }} />
|
|
39
68
|
)}
|
|
40
69
|
|
|
41
|
-
{/*
|
|
70
|
+
{/* DISPATCHER */}
|
|
42
71
|
<div className="pt-4">
|
|
43
72
|
{question.type === 'rating' && <RatingScale ... />}
|
|
44
|
-
{
|
|
73
|
+
{question.type === 'csat' && <CsatScale ... />}
|
|
74
|
+
{question.type === 'slider_matrix' && <SliderMatrixScale ... />}
|
|
75
|
+
{question.type === 'file_upload' && <FileUploadScale ... />}
|
|
76
|
+
{question.type === 'matrix' && question.subType === 'CFM_MATRIX' && <LikertMatrixScale ... />}
|
|
77
|
+
{question.type === 'matrix' && (question.subType === 'CSAT_MATRIX' || question.subType === 'RATING_MATRIX') && <CsatMatrixScale ... />}
|
|
78
|
+
|
|
79
|
+
{question.type === 'radio' && (
|
|
80
|
+
// Inline Radio implementation
|
|
81
|
+
// Make sure to match brand hover colors!
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
{question.type === 'text' && (
|
|
85
|
+
// Inline Textarea implementation
|
|
86
|
+
// Handle character limit `question.maxCharacterCount`
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{question.type === 'text_and_media' && (
|
|
90
|
+
// Display media (image/video) based on question properties
|
|
91
|
+
// This handles CONTENT blocks.
|
|
92
|
+
)}
|
|
45
93
|
</div>
|
|
46
|
-
|
|
47
|
-
{/*
|
|
94
|
+
|
|
95
|
+
{/* ERROR BANNER */}
|
|
48
96
|
{validationError && (
|
|
49
97
|
<div className="mt-6 rounded-lg border-l-4 border-l-[#e20074] bg-red-50 p-4">
|
|
50
98
|
<p className="text-sm font-bold text-gray-900 leading-tight flex items-center gap-2">
|
|
@@ -52,23 +100,6 @@ Every question is wrapped in a standard HTML section. It **MUST** have a border
|
|
|
52
100
|
</p>
|
|
53
101
|
</div>
|
|
54
102
|
)}
|
|
103
|
+
|
|
55
104
|
</section>
|
|
56
105
|
```
|
|
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`.
|
|
@@ -1,91 +1,28 @@
|
|
|
1
|
-
# Rating Scale
|
|
1
|
+
# Rating Scale Architectural Blueprint
|
|
2
2
|
|
|
3
|
-
> **Component**: `RatingScale`
|
|
4
|
-
> **Handles**: `type: 'rating'
|
|
3
|
+
> **Target Component**: `RatingScale`
|
|
4
|
+
> **Handles**: `type: 'rating'`, `type: 'rating_scale'`
|
|
5
5
|
|
|
6
|
-
##
|
|
7
|
-
|
|
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
|
-
```
|
|
6
|
+
## Core Responsibility
|
|
7
|
+
Render a horizontal sequence of numbered badges.
|
|
20
8
|
|
|
21
|
-
##
|
|
9
|
+
## Configuration Matrix
|
|
10
|
+
You MUST handle the following configurations dynamically:
|
|
22
11
|
|
|
23
|
-
|
|
12
|
+
### 1. Color Grading (NPS)
|
|
13
|
+
- Check if `option.color` exists. If so, apply it to the badge background. This is crucial for NPS scales (Red 0-6, Yellow 7-8, Green 9-10).
|
|
24
14
|
|
|
25
|
-
###
|
|
26
|
-
-
|
|
27
|
-
-
|
|
15
|
+
### 2. Label Positioning (`minLabel`, `midLabel`, `maxLabel`)
|
|
16
|
+
- **Architectural Requirement**: Wrap the badge sequence and the labels in a relative container.
|
|
17
|
+
- Place labels underneath the badges using absolute positioning.
|
|
18
|
+
- **Mid Label Calculation**: You must calculate the exact center position: `style={{ left: \`\${(midLabelIndex / (options.length - 1)) * 100}%\` }}`.
|
|
28
19
|
|
|
29
|
-
|
|
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`.
|
|
20
|
+
## UI/UX Rules (CRITICAL)
|
|
37
21
|
|
|
38
|
-
|
|
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'.
|
|
22
|
+
The UI must exactly match enterprise standards:
|
|
40
23
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
```
|
|
24
|
+
1. **Shape**: Badges MUST be square with rounded corners (`rounded-lg w-10 h-10 sm:w-12 sm:h-12`). Do NOT use full circles (`rounded-full`).
|
|
25
|
+
2. **Unselected State**: If any item is selected, unselected items must dim (`opacity-30`).
|
|
26
|
+
3. **Selected State**: The selected badge MUST pop out prominently: `border-2 border-[#e20074] shadow-md scale-105 opacity-100 font-bold`.
|
|
27
|
+
4. **Hover State**: `hover:scale-105 hover:border-[#e20074] transition-all cursor-pointer`.
|
|
28
|
+
5. **Tooltips**: Every badge MUST have `title={option.label}` applied for accessibility and semantic context (e.g. hovering '10' shows 'Extremely Likely').
|
|
@@ -1,79 +1,38 @@
|
|
|
1
|
-
# CSAT
|
|
1
|
+
# CSAT & Rating Architectural Blueprints
|
|
2
2
|
|
|
3
|
-
> **
|
|
4
|
-
> **Handles**: `type: 'csat'`
|
|
3
|
+
> **Target Components**: `CsatScale`, `RatingScale`, `CsatMatrixScale`
|
|
4
|
+
> **Handles**: `type: 'csat'`, `'rating'`, `'rating_scale'`, CSAT matrices.
|
|
5
5
|
|
|
6
|
-
##
|
|
7
|
-
|
|
8
|
-
type CsatScaleProps = {
|
|
9
|
-
question: SurveyQuestion & { type: 'csat' };
|
|
10
|
-
selectedValue?: string | number | null;
|
|
11
|
-
onSelect: (value: number | null) => void;
|
|
12
|
-
};
|
|
13
|
-
```
|
|
6
|
+
## Core Responsibility
|
|
7
|
+
Render sentiment arrays (Faces, Stars, Numbers) ensuring that active selections are unmistakably highlighted using bounding boxes or strong border accents, NEVER just by changing the SVG fill color.
|
|
14
8
|
|
|
15
|
-
##
|
|
9
|
+
## State Management & Mapping
|
|
10
|
+
- **Emojis**: `buttonType === 'emoji'`
|
|
11
|
+
- Map values using `getEmojiForIndex(index, totalLength)` from your icons utility.
|
|
12
|
+
- **Stars**: `buttonType === 'star'`
|
|
13
|
+
- Active stars (index <= selectedValue) get `CsatStarIcons.filled`.
|
|
14
|
+
- **Numbers**: Default for rating scales.
|
|
15
|
+
- Apply `option.color` to the background if provided by the SDK (NPS tracking).
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
## UX Bounding Box Rules (CRITICAL)
|
|
18
18
|
|
|
19
|
-
|
|
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.
|
|
19
|
+
The UI must exactly match the "Active Box" standard.
|
|
28
20
|
|
|
29
|
-
###
|
|
30
|
-
- **
|
|
31
|
-
- **
|
|
32
|
-
-
|
|
33
|
-
- Previous filled stars get `border-transparent p-2 text-[#e20074]`.
|
|
34
|
-
- Empty stars get `opacity-30`.
|
|
21
|
+
### For Emojis & Stars:
|
|
22
|
+
- **Unselected**: `inline-flex p-2 border-2 border-transparent opacity-40 hover:opacity-100 hover:scale-110 transition-all cursor-pointer`.
|
|
23
|
+
- **Selected**: The specific selected icon MUST be wrapped in a prominent square box with rounded corners.
|
|
24
|
+
- **CSS**: `border-2 border-[#e20074] rounded-lg p-2 bg-pink-50 shadow-sm scale-110 opacity-100`.
|
|
35
25
|
|
|
36
|
-
###
|
|
37
|
-
-
|
|
26
|
+
### For Numbered Badges (Rating):
|
|
27
|
+
- **Shape**: MUST be square with rounded corners (`rounded-lg w-12 h-12`). Do not use full circles.
|
|
28
|
+
- **Selected**: `border-2 border-[#e20074] opacity-100 font-bold scale-105 shadow-md`.
|
|
29
|
+
- **Unselected**: `opacity-30 border-transparent`.
|
|
38
30
|
|
|
39
|
-
|
|
40
|
-
-
|
|
31
|
+
## Bounding Boxes inside Matrices
|
|
32
|
+
When emojis or stars are rendered inside `CsatMatrixScale`, the exact same bounding box logic applies. The table cell `<td>` should center the icon, and the icon wrapper `<div>` or `<button>` should toggle between `border-transparent` and `border-[#e20074]` based on the active state.
|
|
41
33
|
|
|
42
|
-
##
|
|
43
|
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
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
|
-
```
|
|
34
|
+
## Tooltips & Label Positions
|
|
35
|
+
- **Tooltips**: Every single emoji, star, and badge MUST have `title={label}` applied so the user sees the semantic text string on hover.
|
|
36
|
+
- **Labels (`minLabel`, `midLabel`, `maxLabel`)**:
|
|
37
|
+
- Must be rendered absolutely below the track.
|
|
38
|
+
- `midLabel` must use inline styles to calculate its exact centered position: `style={{ left: \`\${(midLabelIndex / (options.length - 1)) * 100}%\` }}`.
|
|
@@ -1,77 +1,33 @@
|
|
|
1
|
-
# CSAT Matrix
|
|
1
|
+
# CSAT Matrix Architectural Blueprint
|
|
2
2
|
|
|
3
|
-
> **Component**: `CsatMatrixScale`
|
|
4
|
-
> **Handles**: `type: 'matrix'` (CSAT_MATRIX
|
|
3
|
+
> **Target Component**: `CsatMatrixScale`
|
|
4
|
+
> **Handles**: `type: 'matrix'` (CSAT_MATRIX and RATING_MATRIX subTypes)
|
|
5
5
|
|
|
6
|
-
##
|
|
7
|
-
|
|
8
|
-
type CsatMatrixScaleProps = {
|
|
9
|
-
question: SurveyQuestion & { type: 'matrix' | 'rating_matrix' };
|
|
10
|
-
selectedValue: MatrixAnswerMap;
|
|
11
|
-
onSelect: (value: MatrixAnswerMap) => void;
|
|
12
|
-
};
|
|
13
|
-
```
|
|
6
|
+
## Core Responsibility
|
|
7
|
+
Render a grid where columns represent satisfaction scales (Emojis, Stars, or Rating Badges).
|
|
14
8
|
|
|
15
|
-
##
|
|
9
|
+
## Data Mapping & State Management
|
|
10
|
+
- `selectedValue`: Expects `MatrixAnswerMap` shape.
|
|
11
|
+
- Iterate over `question.rows`. Within each row, iterate over `question.columns`.
|
|
12
|
+
- Render the correct icon/badge in the `<td>` based on `question.buttonType`.
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
## Configuration Matrix
|
|
15
|
+
You MUST handle:
|
|
16
|
+
- **`hasNotApplicable`**: If true, the last column might be `null`. Render a distinct UI (e.g., standard radio or gray badge) for the N/A column.
|
|
17
|
+
- **`reverseScaleOrder`**: Ensure your column mapping respects this logic if implemented in the UI.
|
|
18
18
|
|
|
19
|
-
|
|
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.
|
|
19
|
+
## UI/UX Bounding Box Rules (CRITICAL)
|
|
24
20
|
|
|
25
|
-
|
|
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`.
|
|
21
|
+
Matrices are difficult to read and click accurately. You MUST implement these strict UI rules:
|
|
29
22
|
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
1. **Row Tracking**:
|
|
24
|
+
- Every data `<tr>` MUST have `hover:bg-gray-50 transition-colors duration-150`.
|
|
32
25
|
|
|
33
|
-
|
|
34
|
-
-
|
|
26
|
+
2. **The "Active Bounding Box"**:
|
|
27
|
+
- For Emojis and Stars inside the matrix, do NOT just change the SVG fill.
|
|
28
|
+
- The interactive wrapper inside the `<td>` MUST toggle a pink bounding box.
|
|
29
|
+
- **Selected Cell**: `border-2 border-[#e20074] rounded-lg p-2 bg-pink-50 shadow-sm scale-110 opacity-100 cursor-pointer`.
|
|
30
|
+
- **Unselected Cell**: `border-2 border-transparent p-2 opacity-40 hover:opacity-100 hover:scale-110 cursor-pointer`.
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
- Every
|
|
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
|
-
```
|
|
32
|
+
3. **Tooltips**:
|
|
33
|
+
- Every single interactive cell MUST have `title={col.label}` so the user sees exactly what column they are in without scrolling up to the table header.
|