@explorer02/cfm-survey-sdk 0.1.5 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@explorer02/cfm-survey-sdk",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -1,88 +1,56 @@
1
- # Icons & Emojis — CSAT Visual System
1
+ # Icons & Emojis Architecture
2
2
 
3
- > **Source**: `packages/sdk/src/ui/icons.tsx`
4
- > **Exports**: `CsatEmojiSet`, `CsatEmojiMapping`, `CsatStarIcons`, `getEmojiForIndex`
3
+ > **Source**: `apps/client/src/components/icons.tsx` (to be created by the agent)
5
4
 
6
- ## What It Provides
5
+ ## The CSAT Emoji System
7
6
 
8
- Pre-built React elements for CSAT satisfaction faces and star rating icons. These are used by `CsatScale`, `CsatMatrixScale`, and `CustomSliderTrack` components.
7
+ The SDK relies on a specific sequence of sentiment faces for CSAT scales. You MUST implement this mapping in your client application using a library like `react-icons/fa` or raw SVGs.
9
8
 
10
- ## Emoji Face System (11-Level)
9
+ ### Default 5-Point Mapping
11
10
 
12
- The SDK ships an 11-level sentiment face scale using `react-icons/fa6`:
13
-
14
- | Index | Face | Color | Icon |
15
- |-------|------|-------|------|
16
- | 1 | Very Angry | `#dc2626` | FaFaceAngry |
17
- | 2 | Sad | `#e04832` | FaFaceSadTear |
18
- | 3 | Frown | `#ea580c` | FaFaceFrown |
19
- | 4 | Open Frown | `#f59e0b` | FaFaceFrownOpen |
20
- | 5 | Meh | `#eab308` | FaFaceMeh |
21
- | 6 | Neutral | `#a3a323` | FaFaceMeh |
22
- | 7 | Smile | `#84cc16` | FaFaceSmile |
23
- | 8 | Beam | `#65a30d` | FaFaceSmileBeam |
24
- | 9 | Grin | `#22c55e` | FaFaceGrin |
25
- | 10 | Wide Grin | `#16a34a` | FaFaceGrinWide |
26
- | 11 | Star-Struck | `#059669` | FaFaceGrinStars |
27
-
28
- ## `getEmojiForIndex(index, total)` — Adaptive Mapping
29
-
30
- Maps a 0-based column index within a scale of `total` items to the correct face from the 11-level set.
31
-
32
- **Curated subsets for common scale lengths:**
33
- - 3 options → indices [3, 6, 8] from the 11-set
34
- - 4 options → indices [3, 4, 7, 9]
35
- - 5 options → indices [3, 4, 6, 7, 9]
36
- - 6 options → indices [3, 4, 6, 7, 9, 11]
37
-
38
- **For other lengths:** Interpolates linearly across the 11-set:
39
- ```typescript
40
- const percent = index / (total - 1);
41
- const key = Math.min(11, Math.max(1, Math.round(percent * 10) + 1));
11
+ ```tsx
12
+ import { FaRegSmile, FaRegMeh, FaRegFrown, FaRegSmileBeam, FaRegTired } from 'react-icons/fa';
13
+
14
+ export const CsatEmojiMapping: Record<number | string, JSX.Element> = {
15
+ 1: <FaRegTired className="text-red-500 w-full h-full" />,
16
+ 2: <FaRegFrown className="text-orange-500 w-full h-full" />,
17
+ 3: <FaRegMeh className="text-yellow-500 w-full h-full" />,
18
+ 4: <FaRegSmile className="text-green-500 w-full h-full" />,
19
+ 5: <FaRegSmileBeam className="text-green-600 w-full h-full" />,
20
+ };
42
21
  ```
43
22
 
44
- ## `CsatEmojiMapping` — Legacy 1–5 Mapping
45
-
46
- For standalone 5-point CSAT scales:
47
- ```typescript
48
- { 1: Face[1], 2: Face[4], 3: Face[6], 4: Face[8], 5: Face[11] }
49
- ```
23
+ ### The `getEmojiForIndex` Helper
50
24
 
51
- ## `CsatStarIcons` Star Rating
25
+ Because some surveys use 3-point, 7-point, or 10-point scales instead of 5, you MUST use a helper function to proportionally map any scale length to the 5 core sentiment faces.
52
26
 
53
- ```typescript
54
- {
55
- filled: <FaStar style={{ color: '#e20074', fontSize: '28px' }} />,
56
- empty: <FaRegStar style={{ color: '#d1d5db', fontSize: '28px' }} />,
27
+ ```tsx
28
+ export function getEmojiForIndex(index: number, totalOptions: number): JSX.Element {
29
+ const percentage = index / (totalOptions - 1);
30
+
31
+ if (percentage < 0.2) return CsatEmojiMapping[1];
32
+ if (percentage < 0.4) return CsatEmojiMapping[2];
33
+ if (percentage < 0.6) return CsatEmojiMapping[3];
34
+ if (percentage < 0.8) return CsatEmojiMapping[4];
35
+ return CsatEmojiMapping[5];
57
36
  }
58
37
  ```
59
38
 
60
- ## Usage in Components
61
-
62
- ```tsx
63
- import { getEmojiForIndex, CsatStarIcons, CsatEmojiMapping } from '@explorer02/cfm-survey-sdk';
39
+ ## The Star Icon System
64
40
 
65
- // CSAT Matrix dynamic emoji per column
66
- {columns.map((col, i) => getEmojiForIndex(i, columns.length))}
41
+ For star ratings, you need filled and empty variants:
67
42
 
68
- // CSAT Scale — star rendering
69
- {options.map((opt, i) => (
70
- i < selectedIndex ? CsatStarIcons.filled : CsatStarIcons.empty
71
- ))}
43
+ ```tsx
44
+ import { FaStar, FaRegStar } from 'react-icons/fa';
72
45
 
73
- // Standalone CSAT legacy 1-5 emoji
74
- {CsatEmojiMapping[optionValue]}
46
+ export const CsatStarIcons = {
47
+ filled: <FaStar className="text-yellow-400 w-full h-full" />,
48
+ empty: <FaRegStar className="text-gray-300 w-full h-full" />
49
+ };
75
50
  ```
76
51
 
77
- ## Important: `buttonType` Determines Rendering
78
-
79
- The `CsatQuestion.buttonType` and `MatrixQuestion.buttonType` fields control which icon system to use:
80
-
81
- | `buttonType` | Render Style |
82
- |-------------|-------------|
83
- | `'emoji'` | Sentiment face icons from `getEmojiForIndex` |
84
- | `'star'` | Star icons from `CsatStarIcons` |
85
- | `'numbered'` | Numeric badge buttons (no icons) |
86
- | `'radio'` | Standard radio circles |
87
- | `'graphics'` | Continuous slider with emoji thumb |
88
- | `'dropdown'` | Collapsed select dropdown |
52
+ ## Color Mapping System
53
+ For rating matrices and numbered NPS grids, colors are usually provided by the SDK in `option.color`. If not, use standard NPS coloring:
54
+ - **0-6 (Detractors)**: Red/Orange
55
+ - **7-8 (Passives)**: Yellow
56
+ - **9-10 (Promoters)**: Green
@@ -14,44 +14,61 @@ type QuestionProps = {
14
14
  };
15
15
  ```
16
16
 
17
- ## Structure
17
+ ## Structure & Styling Rules (CRITICAL)
18
18
 
19
- Every question is wrapped in a standard HTML section:
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
20
 
21
21
  ```tsx
22
- <section id={question.id} className="space-y-4 rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
23
- {/* 1. Question Text (with required asterisk) */}
24
- <h2 className="text-lg font-medium leading-relaxed text-gray-900">
25
- <span dangerouslySetInnerHTML={{ __html: question.text }} />
26
- {question.required && <span className="text-red-500 ml-1">*</span>}
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>}
27
31
  </h2>
28
32
 
29
- {/* 2. Question Description */}
33
+ {/* 2. Question Description (Rich Text) */}
30
34
  {question.description && (
31
- <div className="text-sm text-gray-600 mt-2 leading-relaxed" dangerouslySetInnerHTML={{ __html: 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
+ />
32
39
  )}
33
40
 
34
41
  {/* 3. The Scale Component (Dispatcher switch statement) */}
35
- {question.type === 'rating' && <RatingScale ... />}
36
- {/* ... other types ... */}
42
+ <div className="pt-4">
43
+ {question.type === 'rating' && <RatingScale ... />}
44
+ {/* ... other types ... */}
45
+ </div>
37
46
 
38
47
  {/* 4. Validation Error Banner */}
39
48
  {validationError && (
40
- <div className="mt-4 rounded-[4px] border border-[#333333] bg-[#d9d9d9] py-3.5 px-4 text-left">
41
- <p className="text-[13px] sm:text-sm font-bold text-gray-900 leading-tight">{validationError}</p>
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>
42
53
  </div>
43
54
  )}
44
55
  </section>
45
56
  ```
46
57
 
47
- **⚠️ CRITICAL**: The wrapper element **MUST** have `id={question.id}` for the scroll-to-error validation to work!
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.
48
63
 
49
64
  ## Inline vs Dedicated Components
50
65
 
51
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:
52
67
 
53
- 1. **`radio` (MCQ)**: Inline mapping of `<label>` and `<input type="radio">`
54
- 2. **`text`**: Inline `<textarea>`
55
- 3. **`text_and_media`**: Inline `<video>` or `<img>` tag
56
-
57
- (See the respective question type docs for the exact rendering guidance for these inline types).
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`.
@@ -3,9 +3,6 @@
3
3
  > **Component**: `RatingScale`
4
4
  > **Handles**: `type: 'rating'` | `type: 'rating_scale'`
5
5
 
6
- ## Role
7
- Renders numbered badge arrays (for NPS 0-10) or icon arrays (stars/emojis) for satisfaction scales.
8
-
9
6
  ## Props Interface
10
7
  ```typescript
11
8
  type RatingScaleProps = {
@@ -13,26 +10,82 @@ type RatingScaleProps = {
13
10
  options: SurveyOption[];
14
11
  selectedValue?: string | number | null | any;
15
12
  onSelect: (value: number) => void;
13
+ // Extracted from question object:
14
+ minLabel?: string;
15
+ midLabel?: string;
16
+ maxLabel?: string;
17
+ midLabelIndex?: number;
16
18
  };
17
19
  ```
18
20
 
19
- ## Sub-Variants
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`.
20
28
 
21
- ### 1. Numbered Badges (NPS / 10-point)
22
- If `option.color` is present, it's an NPS scale.
23
- - Map over `options`
24
- - Render rounded `<button>` for each
25
- - Use `option.color` as the background color
26
- - If an option is selected (`selectedValue !== undefined`), apply full opacity to the selected button and dim the unselected ones (`opacity-30`).
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`.
27
37
 
28
- ### 2. Star Scale (`question.scaleStyle === 'star'`)
29
- - Render generic star icons using `CsatStarIcons.filled` and `CsatStarIcons.empty`
30
- - Use logic: `i < selectedIndex ? filled : empty` (fill all stars up to the selected one).
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'.
31
40
 
32
- ### 3. Emoji Scale (`question.scaleStyle === 'emoji'`)
33
- - Render emoji face icons
34
- - Since options length is variable, use `getEmojiForIndex(i, options.length)` to map the position to the correct sentiment face.
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}%\` }}`.
35
47
 
36
- ## Layout
37
- - Usually rendered as a `flex` row, `justify-between` or `gap-2` depending on container width.
38
- - Needs to wrap cleanly on mobile viewports.
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
+ ```
@@ -3,9 +3,6 @@
3
3
  > **Component**: `CsatScale`
4
4
  > **Handles**: `type: 'csat'`
5
5
 
6
- ## Role
7
- Renders satisfaction scales (typically 1-5). Relies heavily on the `buttonType` property to determine the visual rendering style.
8
-
9
6
  ## Props Interface
10
7
  ```typescript
11
8
  type CsatScaleProps = {
@@ -15,26 +12,68 @@ type CsatScaleProps = {
15
12
  };
16
13
  ```
17
14
 
18
- ## Rendering by `buttonType`
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.
19
18
 
20
- 1. **`buttonType === 'emoji'`**
21
- - Map over options. Use `CsatEmojiMapping[option.value]` for standard 1-5 scales.
22
- - Or use `getEmojiForIndex` if scale is variable length.
23
-
24
- 2. **`buttonType === 'star'`**
25
- - Map options. Fill stars up to the selected index using `CsatStarIcons`.
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.
26
28
 
27
- 3. **`buttonType === 'numbered'`**
28
- - Simple rounded `<button>` with the numeric value. No traffic-light colors.
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`.
29
35
 
30
- 4. **`buttonType === 'radio'`**
31
- - Standard radio list.
36
+ ### 3. `buttonType === 'numbered'`
37
+ - Fallback to `RatingScale` style (Square badges).
32
38
 
33
- 5. **`buttonType === 'graphics'`**
34
- - Renders a single continuous slider. Uses `<CustomSliderTrack>`.
39
+ ### 4. `buttonType === 'graphics'`
40
+ - Render a single `<CustomSliderTrack>`.
35
41
 
36
42
  ## N/A Option Handling
37
43
  If `question.hasNotApplicable` is true:
38
- - The options array contains an option with `value: null`.
39
- - Render a distinct button for N/A.
44
+ - Render a distinct button or checkbox for N/A.
40
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
+ ```
@@ -3,9 +3,6 @@
3
3
  > **Component**: `CsatMatrixScale`
4
4
  > **Handles**: `type: 'matrix'` (CSAT_MATRIX subType) | `type: 'rating_matrix'`
5
5
 
6
- ## Role
7
- Renders a grid where rows are statements and columns are rating/satisfaction levels.
8
-
9
6
  ## Props Interface
10
7
  ```typescript
11
8
  type CsatMatrixScaleProps = {
@@ -15,29 +12,66 @@ type CsatMatrixScaleProps = {
15
12
  };
16
13
  ```
17
14
 
18
- ## Structure
19
- - Renders an HTML `<table>`.
20
- - **`<thead>`**:
21
- - Empty top-left cell.
22
- - Column headers mapped from `question.columns`. Use `col.label`.
23
- - **`<tbody>`**:
24
- - Map over `question.rows`.
25
- - First cell is the row text.
26
- - Subsequent cells are the input controls mapped from `question.columns`.
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.
27
32
 
28
- ## Input Control Selection
29
- Within each table cell, the control rendered depends on `question.buttonType` (similar to CsatScale):
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`.
30
35
 
31
- - `'emoji'`: Use `getEmojiForIndex(colIndex, cols.length)`
32
- - `'star'`: Use `CsatStarIcons`
33
- - `'radio'`: `<input type="radio">` + custom CSS dot
34
- - `'numbered'`: Badge button (if `rating_matrix`, apply `col.color`)
36
+ ### 4. Tooltips
37
+ - Every icon in every cell MUST have `title={col.label}` so the user knows what column they are clicking.
35
38
 
36
- If `buttonType === 'dropdown'`, the table structure is **abandoned**. Instead, it renders a list of rows, each containing a `<MatrixDropdown>` component.
39
+ ## Implementation Snippet (Row Rendering)
37
40
 
38
- ## `MatrixAnswerMap` Updates
39
- When a cell is clicked:
40
41
  ```tsx
41
- const newValue = { ...selectedValue, [row.id]: col.value };
42
- onSelect(newValue);
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>
43
77
  ```
@@ -3,9 +3,6 @@
3
3
  > **Component**: `LikertMatrixScale`
4
4
  > **Handles**: `type: 'matrix'` (CFM_MATRIX subType)
5
5
 
6
- ## Role
7
- Renders a grid with text-label columns (Likert scale).
8
-
9
6
  ## Props Interface
10
7
  ```typescript
11
8
  type LikertMatrixScaleProps = {
@@ -15,24 +12,32 @@ type LikertMatrixScaleProps = {
15
12
  };
16
13
  ```
17
14
 
18
- ## Structure & Formats
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.
19
24
 
20
- The visual representation changes drastically based on `question.matrixFormat`:
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.
21
29
 
22
- ### 1. `matrixFormat === 'standard'` (Default)
23
- - Table grid.
24
- - Columns are simple radio buttons (`<input type="radio">`).
25
- - **No emojis or stars** standard Likert matrices are purely structural text.
26
- - If `matrixType === 'bipolar'`, the first cell has `row.text` and the last cell has `row.rightText` added.
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.
27
34
 
28
- ### 2. `matrixFormat === 'carousel'`
29
- - Shows one row at a time.
30
- - Renders `row.text` prominently.
31
- - Renders a standard MCQ-style list of options (`columns`) below it.
32
- - Has internal state for `currentRowIndex`.
33
- - Next/Prev buttons step through the rows.
35
+ ## UI Styling Rules
34
36
 
35
- ### 3. `matrixFormat === 'dropdown'`
36
- - Renders a list of rows.
37
- - Each row contains a `<select>` dropdown populated with `question.columns`.
38
- - Or uses a custom `<MatrixDropdown>` component for better styling.
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.
@@ -3,9 +3,6 @@
3
3
  > **Component**: `SliderMatrixScale`
4
4
  > **Handles**: `type: 'slider_matrix'`
5
5
 
6
- ## Role
7
- Renders a list of rows, where each row contains an independent range slider.
8
-
9
6
  ## Props Interface
10
7
  ```typescript
11
8
  type SliderMatrixScaleProps = {
@@ -15,30 +12,82 @@ type SliderMatrixScaleProps = {
15
12
  };
16
13
  ```
17
14
 
18
- ## Structure
19
- - Not a `<table>`. Rendered as a flex/grid list.
20
- - Map over `question.rows`.
21
- - Each row uses the row's specific `min`, `max`, and `step` values.
22
- - The slider is bound to `selectedValue[row.id]`.
15
+ ## UI Styling Rules (CRITICAL)
23
16
 
24
- ## Rendering the Slider
17
+ Sliders must look substantial and modern. Native thin browser sliders are unacceptable.
25
18
 
26
- ```tsx
27
- <input
28
- type="range"
29
- min={row.min}
30
- max={row.max}
31
- step={row.step}
32
- value={selectedValue[row.id] ?? row.min}
33
- onChange={e => onSelect({ ...selectedValue, [row.id]: Number(e.target.value) })}
34
- />
35
- ```
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`.
36
29
 
37
- If `question.sliderType === 'graphics'`, delegate to `<CustomSliderTrack>` instead of native `<input>`.
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)
38
39
 
39
- If `question.enableNotApplicable` is true, render a checkbox next to each slider:
40
40
  ```tsx
41
- <input type="checkbox" onChange={e => {
42
- onSelect({ ...selectedValue, [row.id]: e.target.checked ? null : row.min })
43
- }} />
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
+ })}
44
93
  ```
@@ -3,9 +3,6 @@
3
3
  > **Component**: `FileUploadScale`
4
4
  > **Handles**: `type: 'file_upload'`
5
5
 
6
- ## Role
7
- Provides a UI for selecting files, enforcing limits, and displaying the selected list.
8
-
9
6
  ## Props Interface
10
7
  ```typescript
11
8
  type FileUploadScaleProps = {
@@ -15,19 +12,76 @@ type FileUploadScaleProps = {
15
12
  };
16
13
  ```
17
14
 
18
- ## Structure
19
- 1. **Dropzone / Browse Button**
20
- - `<input type="file" multiple={maxFileCount > 1} className="hidden">`
21
- - Visible button triggers `.click()` on hidden input.
22
- 2. **File List**
23
- - Map over `selectedValue || []`.
24
- - Show file name and remove button.
25
- 3. **Error Text**
26
- - Local state for size limit or format errors.
27
-
28
- ## Limit Enforcement
29
- - `question.maxFileCount`
30
- - `question.supportedFileFormats` (e.g. `['PDF', 'PNG']`)
31
- - `question.fileSizeLimit` (MB)
32
-
33
- When files are selected, validate before adding to `selectedValue`.
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
+ ```
@@ -4,24 +4,69 @@
4
4
  > **Used by**: `SliderMatrixScale`, `CsatMatrixScale`, `CsatScale`
5
5
 
6
6
  ## Role
7
- A shared presentation component that overrides native browser range slider styling to provide an emoji-thumb or branded track.
8
-
9
- ## Why it exists
10
- Native `<input type="range">` cannot reliably render an emoji inside the draggable thumb handle cross-browser. This component uses CSS tricks (often a transparent native range input layered over a custom styled `div` track) to achieve the design.
11
-
12
- ## Props Interface
13
- ```typescript
14
- type CustomSliderTrackProps = {
15
- min: number;
16
- max: number;
17
- step: number;
18
- value: number;
19
- onChange: (val: number) => void;
20
- sliderType?: 'graphics' | 'standard';
21
- // Additional props for tick marks, colors
22
- };
23
- ```
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" />`.
24
23
 
25
- ## Emoji Thumb Logic
26
- If `sliderType === 'graphics'`, the thumb handle should dynamically display an emoji corresponding to the current value.
27
- It uses `getEmojiForIndex` from `icons.tsx`.
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
+ ```